🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

FutureProvider로 API 호출하기 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 11. · 12 Views

FutureProvider로 API 호출하기 완벽 가이드

Riverpod의 FutureProvider를 사용하여 비동기 API 호출을 처리하는 방법을 배웁니다. 로딩, 에러, 성공 상태를 우아하게 처리하고 Pull-to-refresh까지 구현해봅니다.


목차

  1. JSONPlaceholder_API_소개
  2. FutureProvider_선언
  3. 로딩_상태_표시하기
  4. 에러_상태_처리하기
  5. Pull_to_refresh_구현
  6. 로딩 인디케이터가 자동으로 사라집니다
  7. 전체_코드와_실행_결과

1. JSONPlaceholder API 소개

김개발 씨는 Flutter로 첫 프로젝트를 시작했습니다. 서버에서 데이터를 받아와 화면에 표시해야 하는데, 어떻게 시작해야 할지 막막했습니다.

선배 박시니어 씨가 "일단 테스트용 API로 연습해보세요"라고 조언했습니다.

JSONPlaceholder는 무료로 제공되는 테스트용 REST API입니다. 마치 운전을 배울 때 연습장에서 시작하는 것처럼, API 호출을 배울 때 이용할 수 있는 안전한 연습 환경입니다.

실제 백엔드 없이도 GET, POST, PUT, DELETE 요청을 연습할 수 있습니다.

다음 코드를 살펴봅시다.

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

// JSONPlaceholder에서 제공하는 Post 모델
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'] as int,
      title: json['title'] as String,
      body: json['body'] as String,
    );
  }
}

[도입 - 실무 상황 스토리] 김개발 씨는 Flutter 입문 3주 차입니다. UI 컴포넌트는 어느 정도 다룰 수 있게 되었지만, 서버와의 통신은 처음이었습니다.

"진짜 서버를 만들어야 하나요?" 걱정스러운 얼굴로 물었습니다. 박시니어 씨가 웃으며 대답했습니다.

"아니요, 연습할 때는 JSONPlaceholder를 쓰면 돼요. 무료고, 가입도 필요 없어요." [개념 설명 - 비유로 쉽게] 그렇다면 JSONPlaceholder란 정확히 무엇일까요?

쉽게 비유하자면, JSONPlaceholder는 마치 운전 연습장과 같습니다. 실제 도로에 나가기 전에 안전한 공간에서 운전을 연습하는 것처럼, 실제 서버를 구축하기 전에 API 호출을 연습할 수 있는 환경입니다.

실수해도 아무 문제가 없고, 언제든지 사용할 수 있습니다. [왜 필요한가 - 문제 상황] 예전에는 API 연습을 하려면 어땠을까요?

개발자들은 백엔드 서버를 직접 구축하거나, 동료에게 테스트 서버를 요청해야 했습니다. 서버 설정에 시간이 오래 걸렸고, 데이터베이스도 준비해야 했습니다.

더 큰 문제는 초보자가 실수로 데이터를 망가뜨릴 수 있다는 점이었습니다. [해결책 - 개념의 등장] 바로 이런 문제를 해결하기 위해 JSONPlaceholder가 등장했습니다.

JSONPlaceholder를 사용하면 즉시 API 호출을 시작할 수 있습니다. 또한 실제 REST API와 동일한 구조를 제공하므로 실전 감각도 익힐 수 있습니다.

무엇보다 무료이며 가입이나 인증 없이 바로 사용할 수 있다는 큰 이점이 있습니다. [API 엔드포인트 소개] JSONPlaceholder는 다양한 리소스를 제공합니다.

가장 많이 사용하는 것은 /posts 엔드포인트입니다. 게시글 데이터를 다룰 수 있습니다.

그 외에도 /users, /comments, /albums, /photos, /todos 등 실제 서비스에서 흔히 사용하는 데이터 타입을 모두 지원합니다. [모델 클래스 설계] 위의 코드를 살펴보면 Post 클래스를 정의했습니다.

id, title, body 세 가지 필드를 가지고 있습니다. 실제 API 응답 JSON 구조와 정확히 일치합니다.

fromJson 팩토리 생성자는 JSON 데이터를 Dart 객체로 변환하는 역할을 합니다. [타입 안정성] Dart는 타입 안정성이 강한 언어입니다.

json['id'] as int처럼 명시적으로 타입을 지정하면, 런타임 에러를 방지할 수 있습니다. 만약 서버에서 예상과 다른 타입이 오면 즉시 에러가 발생하여 문제를 빨리 찾을 수 있습니다.

[실무 활용 사례] 실제 현업에서는 어떻게 활용할까요? 예를 들어 뉴스 앱을 개발한다고 가정해봅시다.

백엔드 팀이 아직 API를 완성하지 못한 상황에서 JSONPlaceholder의 /posts를 이용해 UI를 먼저 구현할 수 있습니다. 나중에 실제 API 주소만 변경하면 됩니다.

[HTTP 패키지] 코드에서 package:http/http.dart를 import했습니다. 이것은 Flutter에서 HTTP 요청을 보낼 때 가장 많이 사용하는 공식 패키지입니다.

pubspec.yaml에 http: ^1.1.0을 추가해야 사용할 수 있습니다. [JSON 파싱] dart:convert 패키지는 JSON 데이터를 처리합니다.

jsonDecode() 함수로 JSON 문자열을 Dart Map으로 변환할 수 있습니다. 반대로 jsonEncode()는 Dart 객체를 JSON 문자열로 변환합니다.

[정리] 다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 안심했습니다.

"이렇게 편한 도구가 있었군요!" JSONPlaceholder를 이용하면 백엔드 없이도 API 통신을 연습할 수 있습니다. 여러분도 오늘 배운 내용을 바탕으로 다음 단계로 넘어가 봅시다.

실전 팁

💡 - JSONPlaceholder는 https://jsonplaceholder.typicode.com 에서 무료로 사용 가능합니다

  • 실제 데이터는 변경되지 않으므로 POST/PUT/DELETE 요청도 안전하게 연습할 수 있습니다
  • Postman 같은 API 테스트 도구로 먼저 응답 구조를 확인한 후 코드를 작성하면 실수를 줄일 수 있습니다

2. FutureProvider 선언

김개발 씨는 이제 실제로 API를 호출해야 합니다. setState를 써야 하나, StatefulWidget으로 바꿔야 하나 고민하던 중 박시니어 씨가 "Riverpod의 FutureProvider를 쓰세요"라고 조언했습니다.

FutureProvider는 비동기 작업의 결과를 자동으로 관리해주는 Provider입니다. 마치 배달 앱에서 주문 상태를 자동으로 추적하는 것처럼, API 호출의 로딩-성공-에러 상태를 알아서 관리합니다.

StatefulWidget이나 setState 없이도 깔끔하게 비동기 데이터를 다룰 수 있습니다.

다음 코드를 살펴봅시다.

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

// FutureProvider 선언 - 게시글 목록을 가져옵니다
final postsProvider = FutureProvider<List<Post>>((ref) async {
  // API 호출
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/posts'),
  );

  // 응답 상태 코드 확인
  if (response.statusCode == 200) {
    // JSON 배열을 List<Post>로 변환
    final List<dynamic> jsonList = jsonDecode(response.body);
    return jsonList.map((json) => Post.fromJson(json)).toList();
  } else {
    throw Exception('게시글을 불러오는데 실패했습니다');
  }
});

[도입 - 실무 상황 스토리] 김개발 씨는 이제 진짜 코드를 작성할 차례입니다. 손을 들어 박시니어 씨에게 물었습니다.

"StatefulWidget으로 만들어야 하나요? setState로 loading 변수를 바꾸고..." 박시니어 씨가 고개를 저었습니다.

"그렇게 하면 코드가 복잡해져요. FutureProvider를 쓰면 훨씬 간단해집니다." [개념 설명 - 비유로 쉽게] 그렇다면 FutureProvider란 정확히 무엇일까요?

쉽게 비유하자면, FutureProvider는 마치 배달 앱의 주문 추적 시스템과 같습니다. 여러분이 음식을 주문하면 "준비 중" → "배달 중" → "배달 완료" 상태를 자동으로 추적해줍니다.

이처럼 FutureProvider도 API 호출의 "로딩" → "성공" → "에러" 상태를 자동으로 관리해줍니다. [왜 필요한가 - 문제 상황] FutureProvider가 없던 시절에는 어땠을까요?

개발자들은 isLoading, error, data 같은 상태 변수를 직접 관리해야 했습니다. setState를 여러 번 호출하고, 에러 처리 로직을 일일이 작성했습니다.

더 큰 문제는 메모리 누수나 disposed된 위젯에 setState를 호출하는 실수가 빈번했다는 점입니다. [해결책 - 개념의 등장] 바로 이런 문제를 해결하기 위해 FutureProvider가 등장했습니다.

FutureProvider를 사용하면 상태 관리가 자동화됩니다. 또한 위젯이 dispose되어도 안전하게 동작합니다.

무엇보다 코드가 선언적이고 읽기 쉬워진다는 큰 이점이 있습니다. [코드 분석 - 단계별 설명] 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 FutureProvider<List<Post>>로 Provider를 선언합니다. 제네릭 타입으로 List<Post>를 지정하여 타입 안정성을 확보합니다.

(ref) async 부분은 비동기 함수를 정의하는 것인데, ref 객체로 다른 Provider를 읽을 수도 있습니다. [HTTP 요청] http.get()으로 API를 호출합니다.

await 키워드를 사용하여 응답을 기다립니다. 네트워크 요청은 시간이 걸리므로 반드시 비동기로 처리해야 합니다.

Uri.parse()로 URL 문자열을 Uri 객체로 변환합니다. [상태 코드 체크] response.statusCode == 200으로 성공 여부를 확인합니다.

HTTP 상태 코드 200은 "OK"를 의미합니다. 만약 404나 500 같은 에러 코드가 오면 Exception을 던집니다.

이렇게 하면 FutureProvider가 자동으로 에러 상태로 전환됩니다. [JSON 파싱] jsonDecode(response.body)로 JSON 문자열을 파싱합니다.

결과는 List<dynamic> 타입입니다. 그 다음 map() 메서드로 각 JSON 객체를 Post.fromJson()을 통해 Post 객체로 변환합니다.

마지막으로 toList()로 Iterable을 List로 변환합니다. [에러 처리] 상태 코드가 200이 아니면 Exception을 던집니다.

이것이 FutureProvider의 핵심입니다. Exception이 발생하면 Provider가 자동으로 에러 상태가 되고, 위젯에서는 AsyncValue.error로 처리할 수 있습니다.

[실무 활용 사례] 실제 현업에서는 어떻게 활용할까요? 예를 들어 쇼핑몰 앱을 개발한다고 가정해봅시다.

상품 목록을 불러오는 API를 FutureProvider로 감싸면, 자동으로 로딩 스피너를 표시하고, 에러가 발생하면 재시도 버튼을 보여줄 수 있습니다. 코드 몇 줄로 복잡한 상태 관리가 해결됩니다.

[주의사항] 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 Provider 내부에서 setState를 호출하는 것입니다.

FutureProvider는 이미 상태 관리를 하고 있으므로 중복입니다. 따라서 Provider 로직과 위젯 로직을 명확히 분리해야 합니다.

[정리] 다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"이렇게 간단하게 되는군요!" FutureProvider를 제대로 이해하면 복잡한 비동기 로직을 깔끔하게 관리할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - FutureProvider는 한 번만 실행되며 결과를 캐싱합니다. 새로고침이 필요하면 ref.invalidate() 또는 ref.refresh()를 사용하세요

  • 타임아웃을 설정하려면 http.get().timeout(Duration(seconds: 10))처럼 사용하세요
  • 여러 API를 동시에 호출할 때는 Future.wait()를 활용하면 효율적입니다

3. 로딩 상태 표시하기

김개발 씨가 FutureProvider를 만들었습니다. 이제 화면에 표시할 차례인데, "로딩 중일 때는 뭘 보여줘야 하죠?" 궁금해졌습니다.

박시니어 씨가 "AsyncValue를 사용하면 돼요"라고 알려주었습니다.

AsyncValue는 비동기 작업의 세 가지 상태를 표현하는 타입입니다. 마치 신호등이 빨강-노랑-초록으로 상태를 보여주는 것처럼, AsyncValue는 로딩-에러-데이터 상태를 명확하게 구분합니다.

when 메서드로 각 상태에 맞는 UI를 쉽게 렌더링할 수 있습니다.

다음 코드를 살펴봅시다.

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

class PostsScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // postsProvider를 watch하여 상태를 구독합니다
    final asyncPosts = ref.watch(postsProvider);

    // AsyncValue의 상태에 따라 UI를 분기합니다
    return Scaffold(
      appBar: AppBar(title: Text('게시글 목록')),
      body: asyncPosts.when(
        // 로딩 중: CircularProgressIndicator 표시
        loading: () => Center(child: CircularProgressIndicator()),
        // 에러 발생: 에러 메시지 표시
        error: (error, stack) => Center(child: Text('에러: $error')),
        // 데이터 로드 성공: ListView 표시
        data: (posts) => ListView.builder(
          itemCount: posts.length,
          itemBuilder: (context, index) {
            final post = posts[index];
            return ListTile(
              title: Text(post.title),
              subtitle: Text(post.body),
            );
          },
        ),
      ),
    );
  }
}

[도입 - 실무 상황 스토리] 김개발 씨는 Provider는 만들었지만 화면을 어떻게 구성해야 할지 막막했습니다. "로딩 중일 때, 에러일 때, 성공했을 때...

경우의 수가 너무 많아요." 박시니어 씨가 모니터를 가리키며 말했습니다. "걱정 마세요.

AsyncValue.when을 쓰면 모든 경우를 깔끔하게 처리할 수 있어요." [개념 설명 - 비유로 쉽게] 그렇다면 AsyncValue란 정확히 무엇일까요? 쉽게 비유하자면, AsyncValue는 마치 신호등과 같습니다.

빨간불일 때는 멈추고, 노란불일 때는 주의하고, 초록불일 때는 진행하듯이, AsyncValue도 로딩-에러-데이터 세 가지 상태를 명확히 구분합니다. 각 상태에 따라 적절한 UI를 보여주면 됩니다.

[왜 필요한가 - 문제 상황] AsyncValue가 없던 시절에는 어땠을까요? 개발자들은 if-else 문을 여러 겹 중첩해서 사용했습니다.

`if (isLoading) { ... } else if (error != null) { ...

} else { ... }` 같은 코드가 반복되었습니다.

더 큰 문제는 상태 체크를 빠뜨리면 null 에러가 발생한다는 점이었습니다. [해결책 - 개념의 등장] 바로 이런 문제를 해결하기 위해 AsyncValue가 등장했습니다.

AsyncValue를 사용하면 모든 상태를 타입 안전하게 처리할 수 있습니다. 또한 when 메서드가 모든 케이스를 강제하므로 빠뜨릴 수가 없습니다.

무엇보다 코드가 읽기 쉽고 유지보수하기 좋다는 큰 이점이 있습니다. [ConsumerWidget] 코드를 보면 ConsumerWidget을 사용했습니다.

일반 StatelessWidget 대신 ConsumerWidget을 상속하면 build 메서드에서 WidgetRef ref 파라미터를 받을 수 있습니다. 이 ref로 Provider를 읽을 수 있습니다.

ConsumerWidget은 Riverpod의 핵심 위젯입니다. [ref.watch의 역할] ref.watch(postsProvider)는 Provider를 구독합니다.

Provider의 값이 변경되면 위젯이 자동으로 리빌드됩니다. 이것이 반응형 프로그래밍의 핵심입니다.

개발자가 setState를 호출할 필요가 없습니다. [when 메서드의 마법] asyncPosts.when()이 핵심입니다.

세 개의 콜백을 받습니다. loading은 데이터를 기다리는 중, error는 에러가 발생했을 때, data는 성공했을 때 호출됩니다.

Dart 컴파일러가 세 가지를 모두 구현하도록 강제하므로 실수를 방지할 수 있습니다. [loading 콜백] 로딩 중일 때는 CircularProgressIndicator를 표시합니다.

사용자는 화면 중앙에서 빙글빙글 도는 로딩 스피너를 보게 됩니다. 이것은 "데이터를 가져오는 중이니 잠시만 기다려주세요"라는 시각적 피드백입니다.

[error 콜백] 에러가 발생하면 에러 메시지를 표시합니다. error 파라미터에는 실제 Exception 객체가 담겨 있고, stack에는 스택 트레이스가 있습니다.

개발 중에는 자세한 에러 정보를 보여주고, 프로덕션에서는 사용자 친화적인 메시지로 바꾸는 것이 좋습니다. [data 콜백] 데이터 로드에 성공하면 ListView.builder로 목록을 그립니다.

posts 파라미터는 List<Post> 타입입니다. itemBuilder에서 각 게시글을 ListTile로 렌더링합니다.

제목과 본문을 표시하는 간단한 UI입니다. [실무 활용 사례] 실제 현업에서는 어떻게 활용할까요?

예를 들어 인스타그램 같은 SNS 앱을 개발한다고 가정해봅시다. 피드를 불러올 때 로딩 중에는 Skeleton UI를 보여주고, 에러가 발생하면 "인터넷 연결을 확인하세요" 메시지를 표시하고, 성공하면 게시글을 보여줍니다.

AsyncValue.when으로 모든 경우를 우아하게 처리할 수 있습니다. [주의사항] 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 when 대신 직접 asyncPosts.value에 접근하는 것입니다. 이렇게 하면 로딩이나 에러 상태일 때 null 에러가 발생할 수 있습니다.

따라서 항상 when이나 maybeWhen을 사용하는 것이 안전합니다. [정리] 다시 김개발 씨의 이야기로 돌아가 봅시다.

코드를 작성하고 핫 리로드를 누르자 화면에 로딩 스피너가 나타났다가, 잠시 후 게시글 목록이 표시되었습니다. "와, 진짜 되네요!" AsyncValue.when을 제대로 이해하면 복잡한 상태 분기를 간단하게 처리할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 로딩 스피너 대신 Skeleton UI를 사용하면 더 나은 사용자 경험을 제공할 수 있습니다

  • maybeWhen을 사용하면 특정 상태만 처리하고 나머지는 orElse로 처리할 수 있습니다
  • error 콜백에서는 Sentry나 Firebase Crashlytics로 에러를 자동 리포팅하는 것이 좋습니다

4. 에러 상태 처리하기

김개발 씨가 앱을 테스트하던 중 비행기 모드로 전환했습니다. 화면에 "에러: SocketException: ..." 같은 개발자용 메시지가 그대로 표시되었습니다.

"사용자가 보기엔 너무 불친절한데요?" 박시니어 씨가 웃으며 "에러 처리를 개선해봅시다"라고 말했습니다.

에러 처리는 사용자 경험의 핵심입니다. 마치 카페에서 품절된 메뉴를 "재고 없음 error code 404"라고 말하지 않고 "죄송합니다, 오늘은 준비가 안 되었어요"라고 친절하게 안내하는 것처럼, 기술적인 에러를 사용자 친화적인 메시지로 바꿔야 합니다.

재시도 버튼을 제공하면 더 좋습니다.

다음 코드를 살펴봅시다.

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

class PostsScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncPosts = ref.watch(postsProvider);

    return Scaffold(
      appBar: AppBar(title: Text('게시글 목록')),
      body: asyncPosts.when(
        loading: () => Center(child: CircularProgressIndicator()),
        error: (error, stack) => Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.error_outline, size: 64, color: Colors.red),
              SizedBox(height: 16),
              // 사용자 친화적인 에러 메시지
              Text(
                _getErrorMessage(error),
                style: TextStyle(fontSize: 16),
                textAlign: TextAlign.center,
              ),
              SizedBox(height: 24),
              // 재시도 버튼
              ElevatedButton.icon(
                onPressed: () => ref.refresh(postsProvider),
                icon: Icon(Icons.refresh),
                label: Text('다시 시도'),
              ),
            ],
          ),
        ),
        data: (posts) => ListView.builder(
          itemCount: posts.length,
          itemBuilder: (context, index) {
            final post = posts[index];
            return ListTile(
              title: Text(post.title),
              subtitle: Text(post.body),
            );
          },
        ),
      ),
    );
  }

  // 에러 타입에 따라 적절한 메시지 반환
  String _getErrorMessage(Object error) {
    if (error is SocketException) {
      return '인터넷 연결을 확인해주세요';
    } else if (error is HttpException) {
      return '서버에 문제가 발생했습니다\n잠시 후 다시 시도해주세요';
    } else if (error is FormatException) {
      return '데이터 형식이 올바르지 않습니다';
    } else {
      return '알 수 없는 오류가 발생했습니다\n${error.toString()}';
    }
  }
}

[도입 - 실무 상황 스토리] 김개발 씨는 앱을 열심히 테스트하고 있었습니다. 그런데 비행기 모드로 전환하자 화면에 "SocketException: Failed host lookup..." 같은 끔찍한 메시지가 나타났습니다.

옆에서 지켜보던 박시니어 씨가 웃었습니다. "사용자는 SocketException이 뭔지 몰라요.

친절하게 안내해야죠." [개념 설명 - 비유로 쉽게] 그렇다면 사용자 친화적인 에러 처리란 정확히 무엇일까요? 쉽게 비유하자면, 마치 카페에서 품절된 메뉴를 안내하는 것과 같습니다.

"ERROR: ITEM_NOT_FOUND"라고 말하지 않고 "죄송합니다, 오늘은 준비가 안 되었어요"라고 친절하게 말합니다. 이처럼 기술적인 에러를 일반 사용자가 이해할 수 있는 언어로 번역해야 합니다.

[왜 필요한가 - 문제 상황] 에러 처리를 제대로 하지 않으면 어떻게 될까요? 사용자는 "뭔가 잘못됐는데 어떻게 해야 할지 모르겠다"며 앱을 삭제할 수 있습니다.

특히 기술적인 용어가 가득한 에러 메시지는 공포감을 줍니다. 더 큰 문제는 사용자가 문제를 해결할 방법을 제시하지 않으면 그냥 포기한다는 점입니다.

[해결책 - 개념의 등장] 바로 이런 문제를 해결하기 위해 친절한 에러 UI가 필요합니다. 적절한 아이콘과 메시지로 무엇이 잘못되었는지 쉽게 설명합니다.

또한 재시도 버튼을 제공하여 사용자가 직접 문제를 해결할 수 있게 돕습니다. 무엇보다 불안감 대신 "괜찮아, 다시 해보면 돼"라는 안정감을 준다는 큰 이점이 있습니다.

[에러 UI 구성] 위의 코드를 보면 error 콜백에서 Column으로 UI를 구성했습니다. 에러 아이콘, 메시지, 재시도 버튼을 세로로 배치합니다.

mainAxisAlignment.center로 화면 중앙에 정렬하여 사용자의 시선을 집중시킵니다. [아이콘의 역할] Icons.error_outline로 시각적 피드백을 제공합니다.

사용자는 글을 읽기 전에 아이콘을 먼저 봅니다. 빨간색 에러 아이콘은 "뭔가 잘못되었다"는 것을 직관적으로 전달합니다.

크기를 64로 설정하여 충분히 눈에 띄게 만들었습니다. [_getErrorMessage 함수] 에러 타입별로 적절한 메시지를 반환하는 헬퍼 함수입니다.

SocketException은 네트워크 연결 문제를 의미하므로 "인터넷 연결을 확인해주세요"라고 안내합니다. HttpException은 서버 문제이므로 "잠시 후 다시 시도해주세요"라고 말합니다.

각 에러에 맞는 해결책을 제시하는 것이 핵심입니다. [타입 체크] error is SocketException처럼 타입을 체크합니다.

Dart의 is 키워드는 런타임 타입 체크를 수행합니다. if-else if 체인으로 여러 에러 타입을 순차적으로 확인합니다.

마지막 else는 예상하지 못한 에러를 처리합니다. [재시도 기능] **ref.refresh(postsProvider)**로 Provider를 새로고침합니다.

사용자가 "다시 시도" 버튼을 누르면 Provider가 처음부터 다시 실행됩니다. 네트워크가 복구되었다면 이번에는 성공할 수 있습니다.

이것이 바로 사용자에게 제어권을 돌려주는 것입니다. [ElevatedButton.icon] 버튼에 아이콘과 텍스트를 함께 표시합니다.

Icons.refresh로 새로고침 아이콘을 추가했습니다. 아이콘과 텍스트를 함께 사용하면 버튼의 의미가 더 명확해집니다.

사용자는 "이 버튼을 누르면 다시 시도하는구나"라고 직관적으로 이해합니다. [실무 활용 사례] 실제 현업에서는 어떻게 활용할까요?

예를 들어 은행 앱을 개발한다고 가정해봅시다. 송금 중 네트워크 에러가 발생하면 "거래 중 연결이 끊어졌습니다.

잔액을 확인한 후 다시 시도해주세요"처럼 구체적으로 안내합니다. 추가로 고객센터 전화번호를 제공하면 더욱 좋습니다.

[주의사항] 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 에러에 대해 동일한 메시지를 보여주는 것입니다.

"오류가 발생했습니다"만 반복하면 사용자는 뭘 어떻게 해야 할지 모릅니다. 따라서 에러 타입에 따라 구체적인 안내를 제공해야 합니다.

[정리] 다시 김개발 씨의 이야기로 돌아가 봅시다. 에러 처리를 개선한 후 다시 비행기 모드로 테스트했습니다.

이번에는 "인터넷 연결을 확인해주세요"라는 친절한 메시지가 나타났습니다. "훨씬 좋아졌어요!" 사용자 친화적인 에러 처리를 제대로 구현하면 앱의 품질이 크게 향상됩니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 프로덕션 환경에서는 자세한 에러 내용을 숨기고, 개발 환경에서만 표시하는 것이 보안상 안전합니다

  • 에러 발생 시 자동으로 Sentry나 Firebase Crashlytics에 리포팅하면 문제를 빠르게 파악할 수 있습니다
  • 재시도 횟수를 제한하거나 exponential backoff를 적용하면 서버 부하를 줄일 수 있습니다

5. Pull to refresh 구현

김개발 씨는 앱을 잘 만들었지만 한 가지 아쉬운 점이 있었습니다. "게시글이 업데이트되면 어떻게 새로고침하죠?" 박시니어 씨가 "아래로 당겨서 새로고침하는 기능을 추가해봅시다"라고 제안했습니다.

Pull-to-refresh는 모바일 앱에서 가장 직관적인 새로고침 방법입니다. 마치 자판기에서 음료를 뽑을 때 레버를 당기는 것처럼, 화면을 아래로 당기면 새로운 데이터를 가져옵니다.

RefreshIndicator 위젯과 ref.refresh를 조합하면 쉽게 구현할 수 있습니다.

다음 코드를 살펴봅시다.

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

class PostsScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncPosts = ref.watch(postsProvider);

    return Scaffold(
      appBar: AppBar(title: Text('게시글 목록')),
      body: asyncPosts.when(
        loading: () => Center(child: CircularProgressIndicator()),
        error: (error, stack) => Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.error_outline, size: 64, color: Colors.red),
              SizedBox(height: 16),
              Text('인터넷 연결을 확인해주세요'),
              SizedBox(height: 24),
              ElevatedButton.icon(
                onPressed: () => ref.refresh(postsProvider),
                icon: Icon(Icons.refresh),
                label: Text('다시 시도'),
              ),
            ],
          ),
        ),
        data: (posts) => RefreshIndicator(
          // 아래로 당기면 실행되는 콜백
          onRefresh: () async {
            // Provider를 새로고침하고 완료될 때까지 대기
            return ref.refresh(postsProvider.future);
          },
          child: ListView.builder(
            itemCount: posts.length,
            itemBuilder: (context, index) {
              final post = posts[index];
              return ListTile(
                leading: CircleAvatar(child: Text('${post.id}')),
                title: Text(post.title),
                subtitle: Text(
                  post.body,
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

[도입 - 실무 상황 스토리] 김개발 씨는 앱을 친구에게 보여주고 있었습니다. 친구가 물었습니다.

"새 게시글이 올라오면 어떻게 확인해요?" 김개발 씨는 당황했습니다. "앱을 껐다 켜야...

하나?" 박시니어 씨가 옆에서 웃으며 말했습니다. "아니죠.

Pull-to-refresh를 추가하면 됩니다. 인스타그램처럼 아래로 쭉 당기면 새로고침되게요." [개념 설명 - 비유로 쉽게] 그렇다면 Pull-to-refresh란 정확히 무엇일까요?

쉽게 비유하자면, 마치 자판기에서 음료를 뽑을 때 레버를 당기는 것과 같습니다. 아래로 당기는 제스처가 "새로운 걸 주세요"라는 신호가 됩니다.

이것은 iOS와 Android에서 모두 친숙한 패턴이므로 사용자가 설명 없이도 직관적으로 사용할 수 있습니다. [왜 필요한가 - 문제 상황] Pull-to-refresh가 없으면 어떻게 될까요?

사용자는 앱을 완전히 종료하고 다시 켜야 합니다. 또는 설정 메뉴에서 새로고침 버튼을 찾아야 합니다.

이것은 매우 불편합니다. 더 큰 문제는 사용자가 최신 정보를 놓칠 수 있다는 점입니다.

[해결책 - 개념의 등장] 바로 이런 문제를 해결하기 위해 Pull-to-refresh가 등장했습니다. Pull-to-refresh를 사용하면 언제든지 쉽게 새로고침할 수 있습니다.

또한 제스처 기반이므로 버튼 없이도 자연스럽게 동작합니다. 무엇보다 Twitter, Instagram, Facebook 등 주요 앱들이 모두 사용하는 익숙한 패턴이라는 큰 이점이 있습니다.

[RefreshIndicator 위젯] 위의 코드를 보면 RefreshIndicator로 ListView를 감쌌습니다. RefreshIndicator는 Flutter가 기본 제공하는 위젯입니다.

자식 위젯이 스크롤 가능하면 자동으로 pull-to-refresh 제스처를 감지합니다. 사용자가 아래로 당기면 원형 프로그레스 인디케이터가 나타납니다.

[onRefresh 콜백] onRefresh는 사용자가 아래로 당길 때 실행되는 비동기 함수입니다. 반환 타입이 Future<void>입니다.

이 Future가 완료될 때까지 로딩 인디케이터가 표시됩니다. 따라서 데이터를 가져오는 작업이 끝나면 자동으로 인디케이터가 사라집니다.

[ref.refresh의 마법] ref.refresh(postsProvider.future)가 핵심입니다. ref.refresh()는 Provider를 무효화하고 다시 실행시킵니다.

.future를 붙이면 Future가 반환되므로 await로 기다릴 수 있습니다. 이렇게 하면 RefreshIndicator가 데이터 로딩이 끝날 때까지 정확히 대기합니다.

[ListView.builder 개선] 게시글 목록을 더 보기 좋게 만들었습니다. leadingCircleAvatar를 추가하여 게시글 ID를 표시합니다.

subtitle에는 본문을 2줄까지만 보여주고 나머지는 말줄임표로 처리합니다. TextOverflow.ellipsis가 이 역할을 합니다.

[사용자 경험] 실제로 사용자가 경험하는 플로우는 이렇습니다.


5. 로딩 인디케이터가 자동으로 사라집니다

실전 팁

💡 - RefreshIndicator의 color 속성으로 로딩 인디케이터 색상을 앱 테마에 맞게 변경할 수 있습니다

  • strokeWidth 속성으로 인디케이터의 두께를 조절할 수 있습니다
  • 무한 스크롤과 함께 사용할 때는 새로고침 시 페이지를 1로 리셋하는 로직을 추가해야 합니다

6. 전체 코드와 실행 결과

김개발 씨는 지금까지 배운 내용을 모두 합쳤습니다. "이제 완성된 코드를 정리해볼까요?" 박시니어 씨가 고개를 끄덕이며 "네, pubspec.yaml 설정부터 전체 코드까지 한 번에 봅시다"라고 말했습니다.

전체 프로젝트 구성은 개별 부품을 하나로 조립하는 과정입니다. 마치 레고 블록을 하나씩 쌓아 완성된 작품을 만드는 것처럼, 의존성 설정-모델-Provider-UI를 모두 통합합니다.

실행 결과를 통해 실제로 동작하는 모습을 확인할 수 있습니다.

다음 코드를 살펴봅시다.

// pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.4.0
  http: ^1.1.0

// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FutureProvider Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: PostsScreen(),
    );
  }
}

// 모델 정의
class Post {
  final int id;
  final String title;
  final String body;

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

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'] as int,
      title: json['title'] as String,
      body: json['body'] as String,
    );
  }
}

// Provider 정의
final postsProvider = FutureProvider<List<Post>>((ref) async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/posts'),
  );

  if (response.statusCode == 200) {
    final List<dynamic> jsonList = jsonDecode(response.body);
    return jsonList.map((json) => Post.fromJson(json)).toList();
  } else {
    throw Exception('게시글을 불러오는데 실패했습니다');
  }
});

// UI 정의
class PostsScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncPosts = ref.watch(postsProvider);

    return Scaffold(
      appBar: AppBar(title: Text('게시글 목록')),
      body: asyncPosts.when(
        loading: () => Center(child: CircularProgressIndicator()),
        error: (error, stack) => Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.error_outline, size: 64, color: Colors.red),
              SizedBox(height: 16),
              Text('인터넷 연결을 확인해주세요'),
              SizedBox(height: 24),
              ElevatedButton.icon(
                onPressed: () => ref.refresh(postsProvider),
                icon: Icon(Icons.refresh),
                label: Text('다시 시도'),
              ),
            ],
          ),
        ),
        data: (posts) => RefreshIndicator(
          onRefresh: () async => ref.refresh(postsProvider.future),
          child: ListView.builder(
            itemCount: posts.length,
            itemBuilder: (context, index) {
              final post = posts[index];
              return ListTile(
                leading: CircleAvatar(child: Text('${post.id}')),
                title: Text(post.title),
                subtitle: Text(
                  post.body,
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

[도입 - 실무 상황 스토리] 김개발 씨는 지금까지 작성한 코드를 복습하고 있었습니다. "각 부분은 이해했는데, 전체적으로 어떻게 연결되는 거죠?" 파일이 여러 개로 나뉘어 있어 헷갈렸습니다.

박시니어 씨가 화면을 공유하며 말했습니다. "좋은 질문이에요.

처음부터 끝까지 한 번에 정리해봅시다." [프로젝트 구조] Flutter 프로젝트의 구조는 체계적입니다. 루트에는 pubspec.yaml이 있습니다.

이것은 프로젝트 설정 파일로, 의존성 패키지를 선언합니다. lib 폴더에는 main.dart를 비롯한 모든 Dart 코드가 들어갑니다.

[pubspec.yaml 설정] 가장 먼저 필요한 패키지를 추가해야 합니다. flutter_riverpod은 상태 관리를 위한 패키지입니다.

버전 2.4.0 이상을 사용합니다. http 패키지는 네트워크 요청을 위한 것입니다.

버전 1.1.0 이상이면 됩니다. 이 두 패키지가 이번 프로젝트의 핵심 의존성입니다.

[main 함수] 모든 Flutter 앱은 main() 함수에서 시작합니다. runApp()으로 앱을 실행하는데, 여기서 중요한 것은 ProviderScope로 감싸는 것입니다.

ProviderScope는 Riverpod의 모든 Provider를 관리하는 루트 위젯입니다. 이것이 없으면 Provider를 사용할 수 없습니다.

[MyApp 위젯] MyApp은 앱의 기본 설정을 담당합니다. MaterialApp으로 앱의 테마와 홈 화면을 설정합니다.

primarySwatch: Colors.blue로 기본 색상을 파란색으로 지정했습니다. home: PostsScreen()으로 시작 화면을 PostsScreen으로 설정합니다.

[파일 분리 전략] 실제 프로젝트에서는 코드를 파일별로 분리하는 것이 좋습니다. 예를 들어 models/post.dart에 Post 클래스를, providers/posts_provider.dart에 postsProvider를, screens/posts_screen.dart에 PostsScreen을 각각 분리합니다.

지금은 학습을 위해 한 파일에 모았지만, 실무에서는 반드시 분리해야 합니다. [의존성 흐름] 코드의 의존성 흐름을 살펴봅시다.

main()이 ProviderScope를 생성하고, MyApp이 PostsScreen을 렌더링하고, PostsScreen이 postsProvider를 watch하고, postsProvider가 http 패키지로 API를 호출하고, Post 모델로 데이터를 변환합니다. 이것이 데이터의 흐름입니다.

[실행 결과 - 1단계] 앱을 실행하면 가장 먼저 로딩 화면이 나타납니다. 화면 중앙에 CircularProgressIndicator가 빙글빙글 돕니다.

이것은 asyncPosts.whenloading 콜백이 실행된 것입니다. 사용자는 "데이터를 가져오는 중이구나"라고 인식합니다.

[실행 결과 - 2단계] 1-2초 후 게시글 목록이 나타납니다. JSONPlaceholder API에서 100개의 게시글을 가져옵니다.

각 게시글은 ListTile로 표시되며, 왼쪽에 ID가 담긴 CircleAvatar, 가운데에 제목, 아래에 본문 미리보기가 보입니다. 스크롤하면 모든 게시글을 볼 수 있습니다.

[실행 결과 - Pull-to-refresh] 화면을 아래로 당기면 새로고침됩니다. 원형 프로그레스 인디케이터가 상단에 나타나고, API를 다시 호출합니다.

완료되면 인디케이터가 사라지며 화면이 업데이트됩니다. 이것은 RefreshIndicator가 동작한 것입니다.

[실행 결과 - 에러 테스트] 비행기 모드로 전환하고 앱을 재시작해봅시다. 에러 아이콘과 "인터넷 연결을 확인해주세요" 메시지가 나타납니다.

"다시 시도" 버튼을 누르면 다시 API를 호출합니다. 비행기 모드를 해제한 후 버튼을 누르면 정상적으로 데이터가 로드됩니다.

[실무 활용 사례] 이 패턴은 거의 모든 앱에 적용할 수 있습니다. 쇼핑몰 앱이라면 상품 목록을, 뉴스 앱이라면 기사 목록을, SNS 앱이라면 피드를 이런 방식으로 구현합니다.

FutureProvider와 AsyncValue.when 패턴만 익히면 대부분의 리스트 화면을 만들 수 있습니다. [다음 단계] 이제 기본을 마스터했습니다.

다음으로 배워야 할 것은 StateProvider로 상태를 변경하거나, StreamProvider로 실시간 데이터를 처리하거나, Pagination으로 무한 스크롤을 구현하는 것입니다. 하지만 오늘 배운 FutureProvider가 모든 것의 기초입니다.

[정리] 다시 김개발 씨의 이야기로 돌아가 봅시다. 완성된 앱을 실행하자 게시글 목록이 잘 나타났습니다.

아래로 당겨서 새로고침도 되고, 비행기 모드에서는 에러 메시지도 잘 나타났습니다. "드디어 제대로 된 앱을 만들었어요!" FutureProvider로 API 호출을 마스터하면 Flutter 개발의 큰 산을 넘은 것입니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 실제 앱에서는 환경변수로 API URL을 관리하여 개발/프로덕션 환경을 분리하세요

  • dio 패키지를 사용하면 인터셉터, 타임아웃, 캐싱 등 고급 기능을 쉽게 추가할 수 있습니다
  • FutureProvider 대신 AsyncNotifierProvider를 사용하면 더 복잡한 상태 변경 로직을 처리할 수 있습니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Flutter#FutureProvider#Riverpod#AsyncValue#API호출#Flutter,Riverpod

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

Riverpod 3.0 쇼핑 앱 종합 프로젝트 완벽 가이드

Flutter와 Riverpod 3.0을 활용한 실무 수준의 쇼핑 앱 개발 과정을 단계별로 학습합니다. 상품 목록, 장바구니, 주문, 인증, 검색 기능까지 모든 핵심 기능을 구현하며 상태 관리의 실전 노하우를 익힙니다.

Riverpod 3.0 Retry 자동 재시도 완벽 가이드

Riverpod 3.0에 새로 추가된 Retry 기능을 활용하여 네트워크 오류나 일시적인 실패 상황에서 자동으로 재시도하는 방법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 예제와 함께 설명합니다.

Riverpod 3.0 requireValue로 Provider 결합하기

Riverpod 3.0에 새로 추가된 requireValue를 활용하여 여러 Provider의 데이터를 효율적으로 결합하는 방법을 배웁니다. 비동기 데이터를 마치 동기 데이터처럼 다루는 실전 패턴을 소개합니다.

Flutter 3.0 Offline 데이터 영속화 완벽 가이드

Flutter 3.0에서 새롭게 추가된 Offline 데이터 영속화 기능을 배웁니다. Storage 인터페이스부터 SharedPreferences 활용, 실전 예제까지 실무에서 바로 사용할 수 있는 패턴을 배워봅시다.

Riverpod 3.0 Mutation으로 폼 제출 완벽 가이드

Riverpod 3.0의 새로운 Mutation 기능으로 로그인과 회원가입 폼을 우아하게 처리하는 방법을 배웁니다. 로딩 상태, 에러 처리, 성공 처리까지 실무에서 바로 쓸 수 있는 패턴을 익혀보세요.