📌 Day 16: Flutter HTTP 패키지를 활용한 API 요청 (GET, POST, Json 데이터 처리, FutureProvider<

2025. 3. 19. 15:18같이 공부합시다 - Flutter/Dart & Flutter 기초부터 실전까지

728x90

 
Flutter 앱에서 외부 서버(API)와 데이터를 주고받기 위해 http 패키지를 사용합니다.
 
GET 요청을 사용하여 서버에서 데이터를 가져오고,
POST 요청을 사용하여 데이터를 서버로 보낼 수 있습니다.
 
JSON 데이터를 처리하는 방법과 비동기 프로그래밍(async/await),
그리고 비동기 데이터를 처리하는 방식에 대해 알아봅시다.
 
 

Flutter HTTP API 데이터 가져오기


 
 
 


🔔 주제

🔸 HTTP 패키지 설치 및 기본 설정
🔸 GET 요청을 사용하여 데이터 가져오기
🔸 POST 요청을 사용하여 데이터 보내기
🔸 JSON 데이터 변환 및 처리
🔸 에러 처리 및 API 응답 관리
 
 
 
 
 


1️⃣ HTTP 패키지 설치 및 설정

 
📌 http 패키지 설치
Flutter에서 HTTP 요청을 보내려면 http 패키지를 추가해야 합니다.
 
터미널에서 아래 명령어 실행:

flutter pub add http

 
pubspec.yaml에 http 패키지가 추가되었는지 확인:

dependencies:
  flutter:
    sdk: flutter
  http: ^1.3.0  # 2025-03-17 기준 최신

 
HTTP 패키지를 사용하려면 import 필요:

import 'package:http/http.dart' as http;
import 'dart:convert'; // JSON 데이터를 처리하기 위해 필요

 
 
 
 
 
 


2️⃣ GET 요청: 서버에서 데이터 가져오기

 
📌 GET 요청이란?

 
📌 기본 GET 요청 코드

Future fetchData() async {
  final url = Uri.parse("<https://jsonplaceholder.typicode.com/posts/1>");

  final response = await http.get(url);

  if (response.statusCode == 200) {
    // 서버 응답이 성공하면 데이터를 출력
    final data = jsonDecode(response.body);
    print("제목: ${data['title']}");
  } else {
    print("데이터 불러오기 실패: ${response.statusCode}");
  }
}

 
🔔 코드 설명
✅ http.get(url) → 지정된 URL에서 데이터를 가져옵니다.
✅ await → 네트워크 요청이 완료될 때까지 기다립니다.
✅ jsonDecode(response.body) → JSON 데이터를 Dart 객체로 변환합니다.
✅ statusCode == 200 → 응답이 성공적(OK)인지 확인한 후 데이터 처리
 
🔔 코드 추가 설명

📌 Future 란?
🔸 Future는 미래의 어느 시점에 완료될 값을 나타내는 비동기 작업입니다.
🔸 Dart 언어에 내장되어 있는 클래스입니다.

💡 비유로 이해하기
➡ "택배 배송"을 생각해 봅시다.
1️⃣ 택배를 주문하면 → 즉시 받지는 못하지만, 언젠가는 도착함
2️⃣ 도착하기 전까지 → 다른 일을 할 수 있음 (비동기 실행)
3️⃣ 도착하면 → 택배를 받아서 사용할 수 있음 (.then() 또는 await)

관련된 내용은 이미 한 번 다뤄봤었습니다.
아래 링크의 포스트 내용을 한 번 훑어봅시다.

. . . Uri.parse(”https://steadybuilder.tistory.com/79") . . .

📌 Day 2: Dart & Flutter 기초부터 실전까지! 비동기 프로그래밍 (async/await, Future)

📌 주제✅ Dart의 비동기 프로그래밍 이해하기✅ Future, async/await 활용법✅ Flutter에서 API 데이터를 비동기로 가져오는 방법 1️⃣ 비동기 프로그래밍 개념📌 비동기(Asynchronous)란?동기 : 일반적인

steadybuilder.tistory.com

 

📌 Uri.parse()는 왜 사용할까?
🔸 이 코드는 단순히 문자열 형태의 URL을 Uri 객체로 변환하는 역할을 합니다.

📌 Uri.parse()가 필요한 이유
🔸 http 패키지의 API 요청에서 String 이 아닌 Uri를 요구하기 때문 (파싱하지 않으면 오류!)
🔸 Uri는 URL을 더 안전하게 처리할 수 있음

📌 http 는 라이브러리 입니다. 
🔸 라이브러리 안에 정적 함수가 다양하게 포함되어 있습니다.
🔸get 뿐만 아니라 post, put, read, delete, head, patch 등이 있습니다.

 
 
 
 
 
 


3️⃣ POST 요청: 서버에 데이터 보내기

 
📌 POST 요청이란?

  • POST 요청은 서버에 데이터를 보내는 요청 방식입니다.
  • 예를 들어, 사용자가 입력한 게시물을 서버에 저장할 수 있습니다.

 
📌 기본 POST 요청 코드

Future sendData() async {
  final url = Uri.parse("<https://jsonplaceholder.typicode.com/posts>");

  final response = await http.post(
    url,
    headers: {"Content-Type": "application/json"},
    body: jsonEncode({"title": "Flutter 공부", "body": "HTTP 요청 연습", "userId": 1}),
  );

  if (response.statusCode == 201) {
    final data = jsonDecode(response.body);
    print("새로운 게시물이 추가됨: ${data['id']}");
  } else {
    print("데이터 전송 실패: ${response.statusCode}");
  }
}

 
🔔 코드 설명
✅ http.post(url, headers, body) → 서버에 데이터를 보냅니다.
✅ headers: {"Content-Type": "application/json"} → 서버에 JSON 형식으로 데이터를 보낸다고 명시합니다.
✅ body: jsonEncode({...}) → Dart 객체를 JSON 문자열로 변환하여 서버로 전송합니다.
✅ statusCode == 201 → POST 요청이 성공적으로 처리되었는지 확인
 
📌 실행 코드

import 'package:http/http.dart' as http;
import 'dart:convert';

Future fetchData() async {
  final url = Uri.parse("<https://jsonplaceholder.typicode.com/posts/1>");

  final response = await http.get(url);

  if (response.statusCode == 200) {
    final data = jsonDecode(response.body);
    print("제목 : ${data['title']}");
  } else {
    print("데이터 불러오기 실패: ${response.statusCode}");
  }
}

Future sendData() async {
  final url = Uri.parse("<https://jsonplaceholder.typicode.com/posts>");

  final response = await http.post(
    url,
    headers: {"Content-Type": "application/json"},
    body:
        jsonEncode({"title:": "Flutter 공부", "body": "HTTP 요청 연습", "userId": 1}),
  );

  if (response.statusCode == 201) {
    final data = jsonDecode(response.body);
    print("새로운 게시물이 추가됨: ${data['id']}");
  } else {
    print("데이터 전송 실패: ${response.statusCode}");
  }
}

void main() {
  fetchData();
  sendData();
}

 
🔻 실행 결과


 
 
 
 
 
 


4️⃣ GET 요청을 UI 에서 활용하기 (Riverpod 구현)

 

📌 API 호출을 위한 데이터 모델 (post_model.dart)

class Post {
  final int id;
  final String title;
  final String body;

  Post({required this.id, required this.title, required this.body});

  // JSON 데이터를 Dart 객체로 변환하는 메서드
  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}

 
 
 
🔸 JSON 데이터를 Post 객체로 변환 (Factory Constructor)

// JSON 데이터를 Dart 객체로 변환하는 메서드
factory Post.fromJson(Map<String, dynamic> json) {
  return Post(
    id: json['id'],
    title: json['title'],
    body: json['body'],
  );
}

 
💡 이게 왜 필요할까?
▪ 서버에서 JSON 데이터를 가져오면 바로 사용할 수 없습니다.
▪ Post 객체로 변환해야 Flutter 앱에서 쉽게 데이터를 다룰 수 있습니다.
▪ fromJson() 메서드는 JSON 데이터가 나중에 들어올 것을 대비해 미리 정의한 것입니다.
▪ 즉, "API 요청을 하면 이런 JSON 데이터가 올 것이다"라는 전제를 가지고 미리 작성하는 것입니다.
 
🔸 fromJson()이 하는 일
▪ API로부터 JSON 데이터를 받아서 Post 객체로 변환하는 역할을 합니다.
▪ 하지만 API 요청을 하기 전에는 데이터가 없으므로, 변환할 준비만 해두는 것입니다.
 
🔹 예제: API 요청 전 (아직 데이터 없음)

factory Post.fromJson(Map<String, dynamic> json) {
  return Post(
    id: json['id'],        // 나중에 받아올 JSON의 'id' 값
    title: json['title'],  // 나중에 받아올 JSON의 'title' 값
    body: json['body'],    // 나중에 받아올 JSON의 'body' 값
  );
}

현재 json 데이터는 없음
나중에 API 응답을 받아 json 값이 들어오면 변환할 준비가 되어 있음
 
 
 
 
 


📌 API 요청을 관리하는 Provider (post_provider.dart)

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'post_model.dart';

// API 데이터 요청 Provider
final postProvider = FutureProvider<list>((ref) async {
  final url = Uri.parse("<https://jsonplaceholder.typicode.com/posts>");
  final response = await http.get(url);

  if (response.statusCode == 200) {
    final List jsonData = jsonDecode(response.body);
    return jsonData.map((json) => Post.fromJson(json)).toList();
  } else {
    throw Exception("데이터 불러오기 실패");
  }
});

</list

 
🔔 Riverpod에서 FutureProvider를 사용하는 방법은 기존 Provider와 약간 다릅니다.


😎 [참고] Provider 관련
https://steadybuilder.tistory.com/90
https://steadybuilder.tistory.com/91
 
😎 [참고] Riverpod 관련
https://steadybuilder.tistory.com/92
https://steadybuilder.tistory.com/93


 

🔔 일반적인 Provider 등록 방법

 
🔹 기본적인 Provider 예제

final textProvider = Provider<String>((ref) {
  return "Hello, Riverpod!";
});

 
✔ Provider<T>() 함수는 Riverpod에서 "상태를 전역적으로 관리하는" Provider를 등록하는 함수입니다.
  ▪ 여기에서 T는 Provider가 관리할 데이터 타입을 의미합니다.
  ▪ Provider<String>이면 '문자열(String)' 을 관리하고, Provider<int>이면 '숫자(int)' 를 관리합니다.
 
(ref)Provider 내부에서 다른 Provider 와의 의존성을 관리하는 역할을 합니다.
  ▪ ref 를 레스토랑의 웨이터라고 생각해 봅시다.
  ▪ 손님(Screen)은 직접 주방(Provider)에 가지 않고, 웨이터(ref)에게 음식을 가져다달라고 요청합니다.
  ▪ ref.watch(menuProvider) : "웨이터(ref)에게 '메뉴판(Provider)'을 가져와 달라고 요청"하는 것과 같습니다.
  ▪ ref.read(orderProvider).placeOrder() : "웨이터(ref)를 통해 '주문 시스템(Provider)'에 요청"하는 것과 같습니다.
 
return 문에서 Provider가 관리할 데이터 반환
  ▪ Provider 내부에서 반환된 값이 이 Provider가 관리하는 상태 값이 됩니다. 

 
✔ Provider가 등록 완료됨
  ▪ textProvider 라는 이름으로 Provider<String>을 등록하면, 앱의 어느 곳에서든지 ref.watch(textProvider)를 통해 이 값을 사용할 수 있습니다.
  ▪ 이 Provider는 한 번 등록되면 값이 변하지 않습니다. (즉, const와 같은 역할)
 
🔹 textProvider UI 에서 사용 예시

class ExampleScreen extends ConsumerWidget {

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final text = ref.watch(textProvider);
    
    return Text(text);  // 출력: "Hello, Riverpod!"
  }
}

 
기본적인 Provider는 즉시 값을 반환하는 방식입니다.
하지만 비동기 데이터를 다룰 때는 FutureProvider가 필요합니다!

 

🔔 FutureProvider 란?

 
✔ 비동기 데이터(Future)를 관리할 때 사용하는 Provider 입니다.

즉, API 요청 결과 같은 데이터를 관리하는 데 사용합니다.
API 요청 후 데이터를 기다려야 하므로 비동기(async/await) 처리가 필수입니다.
 
📌 FutureProvider 등록 예제

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'post_model.dart';

// API 데이터 요청 Provider
final postProvider = FutureProvider<list>((ref) async {
  final url = Uri.parse("<https://jsonplaceholder.typicode.com/posts>");
  final response = await http.get(url);

  if (response.statusCode == 200) {
    final List jsonData = jsonDecode(response.body);
    return jsonData.map((json) => Post.fromJson(json)).toList();
  } else {
    throw Exception("데이터 불러오기 실패");
  }
});

</list

 
✅ FutureProvider<T>() 함수의 역할
FutureProvider<T>() 함수는 Riverpod 에서 "비동기 데이터를 관리하는" Provider 를 등록하는 함수입니다.
  ▪ 여기에서 T는 Provider가 관리할 데이터 타입을 의미합니다.
  ▪ FutureProvider<List<Post>>이면 비동기적으로 List<Post> 데이터를 관리합니다.
 
FutureProvider는 일반 Provider와 달리 API 요청 등 비동기 작업을 수행할 때 사용됩니다.
  ▪ 일반 Provider는 즉시 값을 반환하지만, FutureProvider는 비동기 작업이 끝날 때까지 기다려야 합니다.
 
✅ (ref)의 역할
(ref)는 Provider 내부에서 다른 Provider와의 의존성을 관리하는 역할을 합니다.
  ▪ ref를 공항의 안내 데스크라고 생각해 볼 수 있습니다.
  ▪ 여행객(Screen)은 직접 항공사(Provider)에 문의하지 않고, 안내 데스크(ref)에게 비행기 일정을 가져와 달라고 요청합니다.
  ▪ ref.watch(flightProvider) : 안내 데스크(ref)에 비행 일정(Provider)을 확인 하는 것
  ▪ ref.read(ticketProvider).bookFlight() : 안내 데스크(ref)를 통해 비행기 표 예매(Provider) 요청 하는 것
 
즉, ref는 다른 Provider와 연결될 수 있도록 도와주는 "중간 관리자" 역할을 합니다.
 
✅ return 문에서 비동기 데이터를 반환
Provider 내부에서 return 된 값이 이 FutureProvider가 관리하는 상태 값이 됨.
  ▪ API 요청을 수행하여 서버에서 데이터를 받아오고, JSON 데이터를 List<Post> 형태로 변환하여 반환함.
 
✅ return 코드 상세 분석

final url = Uri.parse("<https://jsonplaceholder.typicode.com/posts>");
final response = await http.get(url);
  
final List jsonData = jsonDecode(response.body);
return jsonData.map((json) => Post.fromJson(json)).toList();

 
jsonData → API로부터 받아온 JSON 리스트 (List<dynamic>)
.map((json) => Post.fromJson(json)) → 각각의 JSON을 Post 객체로 변환
     ⇒ (🔸 JSON 데이터를 Post 객체로 변환 (Factory Constructor) 참고. )
.toList() → 변환된 데이터를 리스트 형태(List<Post>)로 반환
 
✅ FutureProvider 등록 완료
postProvider라는 이름으로 FutureProvider<List<Post>>를 등록하면, 앱의 어느 곳에서든지 ref.watch(postProvider)를 통해 이 값을 가져올 수 있음.
  ▪ 데이터가 로드될 때까지 loading 상태를 유지하며, 완료되면 data 상태로 전환됨.
  ▪ 만약 API 요청이 실패하면 error 상태로 전환되어 오류 메시지를 출력할 수 있음.
 
 
 
 
 


📌 UI 화면에서 API 데이터를 가져와 표시 (post_screen.dart)

FutureProvider는 API 요청이 끝나기 전까지 로딩 상태를 관리해야 합니다.
이때 when()을 사용하면 데이터 상태에 따라 UI를 다르게 표시할 수 있음!
 

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'post_provider.dart';

class PostScreen extends ConsumerWidget {
  const PostScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final postAsyncValue = ref.watch(postProvider);

    return Scaffold(
      appBar: AppBar(title: const Text("API 데이터 가져오기")),
      body: postAsyncValue.when(
        data: (posts) => ListView.builder(
          itemCount: posts.length,
          itemBuilder: (context, index) {
            final post = posts[index];
            return ListTile(
              title: Text(post.title),
              subtitle: Text(post.body),
            );
          },
        ),
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (error, stackTrace) => Center(child: Text("데이터를 불러올 수 없습니다.")),
      ),
    );
  }
}

 
🔔 코드 설명
✅ ref.watch(postProvider) → postProvider에서 API 데이터를 가져옴
✅ when(data, loading, error) → 데이터 상태에 따라 UI를 다르게 표시
로딩 중(loading) → CircularProgressIndicator()를 표시
데이터 수신 완료(data) → ListView.builder()를 사용하여 데이터 출력
오류 발생(error) → 오류 메시지 출력
 
 
 
🔔 상세 코드 분석
 
 
✅ postAsyncValue 상세

final postAsyncValue = ref.watch(postProvider);

 
postProvider는 FutureProvider<List<Post>> 형태
ref.watch(postProvider)를 호출하면 postProvider의 상태를 가져옴
이때 반환되는 값은 비동기 데이터(AsyncValue<List<Post>>) . . . Async..Value ? 😱❓
 
🔸 AsyncValue<T>란?
FutureProvider는 비동기 데이터를 반환하는데, 비동기 작업은 시간이 걸리므로 즉시 값을 반환할 수 없음.
그래서 Riverpod에서는 AsyncValue<T> 타입을 사용하여 비동기 데이터를 감싸고, 상태를 추적함.

상태  설명
AsyncValue.loading()데이터를 가져오는 중 (로딩 상태)
AsyncValue.data(value)데이터 가져오기 완료 (value는 실제 값)
AsyncValue.error(error, stackTrace)데이터 가져오기 실패 (에러 발생)

 
즉, postAsyncValue는 "현재 데이터가 로딩 중인지, 완료되었는지, 에러가 발생했는지" 상태를 감싸고 관리하는 역할을 함.
 
 
✅ when()의 동작 원리

body: postAsyncValue.when(
  data: (posts) => ListView.builder(  // ✅ (posts) 리스트를 받음
    itemCount: posts.length,
    itemBuilder: (context, index) {
      final post = posts[index]; // ✅ (posts)의 데이터를 가져옴
      return ListTile(
        title: Text(post.title),
        subtitle: Text(post.body),
      );
    },
  ),
  loading: () => const Center(child: CircularProgressIndicator()),
  error: (error, stackTrace) => Center(child: Text("데이터를 불러올 수 없습니다.")),
)

 
when() 메서드는 AsyncValue<T> 상태를 감지하여 UI를 자동 업데이트
data: (posts) => → API 요청이 완료되면 FutureProvider<List<Post>>에서 반환된 List<Post> 데이터를 posts로 전달
즉, FutureProvider에서 가져온 데이터가 posts로 자동으로 전달됨
 
 
✅ when()을 사용하지 않고 풀어쓴 코드
만약 when()을 사용하지 않고 직접 분해해서 작성한다면 이렇게 …

Widget build(BuildContext context, WidgetRef ref) {
  final postAsyncValue = ref.watch(postProvider);

  if (postAsyncValue.isLoading) {
    return Center(child: CircularProgressIndicator()); // ✅ 로딩 중
  } else if (postAsyncValue.hasError) {
    return Center(child: Text("데이터를 불러올 수 없습니다.")); // ✅ 에러 발생
  } else if (postAsyncValue.hasValue) {
    final posts = postAsyncValue.value!; // ✅ 데이터 가져오기
    return ListView.builder(
      itemCount: posts.length,
      itemBuilder: (context, index) {
        final post = posts[index];
        return ListTile(
          title: Text(post.title),
          subtitle: Text(post.body),
        );
      },
    );
  } else {
    return SizedBox(); // 아무것도 없을 때
  }
}

when() 없이 if-else를 사용하여 상태를 직접 확인
postAsyncValue.hasValue일 때만 posts = postAsyncValue.value!를 통해 데이터 가져옴
즉, when()을 사용하면 이런 복잡한 if-else 코드를 간단하게 표현 가능!
 
 
 
 
 


📌 main.dart 작성

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'screens/post_screen.dart';

void main() {
  runApp(ProviderScope(child: const MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "SteadyBuilder's Flutter",
      theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
      home: PostScreen(),
    );
  }
}

 
 
 
 
 
 


📌 최종 실행 흐름

1️⃣ PostScreen이 실행되면 ref.watch(postProvider)를 호출
2️⃣ postProvider는 API에 GET 요청을 보냄
3️⃣ 서버에서 데이터를 받아오면 Post.fromJson()을 사용하여 Post 객체 리스트로 변환
4️⃣ 데이터가 UI에 표시됨 (로딩 중이면 로딩 화면 표시)
5️⃣ 만약 API 요청이 실패하면 오류 메시지를 표시
 
 
 
 
 


📌 최종 요약

http 패키지를 활용하여 API와 데이터를 주고받을 수 있다.
GET 요청을 사용하여 데이터를 가져오고, POST 요청을 사용하여 데이터를 서버에 보낼 수 있다.
JSON 데이터를 Dart 객체로 변환하여 활용할 수 있다.
Riverpod의 FutureProvider를 사용하면 API 데이터를 쉽게 관리할 수 있다.
Flutter의 비동기 UI(when())를 활용하여 로딩, 데이터 표시, 에러 처리를 할 수 있다.

728x90