📌 Day 17: Dio + Riverpod 완벽 활용 가이드! (Flutter API 통신 실전 패턴 총정리)

2025. 3. 26. 16:53같이 공부합시다 - Flutter/Dart & Flutter 기초부터 실전까지

728x90

 
Flutter로 앱을 개발하면서 REST API를 연동하는 기능은 거의 모든 실무 프로젝트에 필수인 것 같습니다. 특히 신입 개발자 포트폴리오에서도 API 통신 + 상태 관리 조합(Dio + Riverpod) 은 큰 강점이 된다고 합니다.
 
✅ Dio는 http 패키지보다 더 강력하고 유연한 Flutter용 HTTP 클라이언트 라이브러리입니다.
✅ 요청/응답 인터셉터, 에러 핸들링, 로깅, 시간 초과 설정 등 API 통신을 더 편리하게 처리할 수 있습니다.
✅ 실무에서는 http보다 Dio를 선호하는 경우가 많습니다.
 
 
 
 


🔔 주제

🔸 Dio 패키지 설치 및 설정
🔸 GET 요청 및 응답 처리
🔸 POST 요청 (데이터 전송)
🔸 요청/응답 로깅 및 에러 처리
🔸 실무에서 많이 사용하는 옵션들
 
 
 
 
 


1️⃣ Dio 패키지 설치


📌 설치 명령어

flutter pub add dio


📌 pubspec.yaml에 의존성 확인

dependencies:
  dio: ^5.8.0+1 # 2025-03-26 기준 최신 버전

 
 
 
 
 
 


2️⃣ Dio 기본 사용법


📌 Dio 인스턴스 생성 및 GET 요청 예제

import 'package:dio/dio.dart';

final dio = Dio();

Future fetchPost() async {
  try {
    final response = await dio.get('<https://jsonplaceholder.typicode.com/posts/1>');
    print('제목: ${response.data['title']}');
  } catch (e) {
    print('에러 발생: $e');
  }
}

void main() {  
	fetchPost();
}


🔎 코드 설명

  • Dio() → Dio 인스턴스 생성
  • get(url) → GET 요청
  • response.data → JSON 형태의 응답 데이터 접근
  • try-catch → 네트워크 에러를 안전하게 처리


🔻 Dio 인스턴스 생성 및 GET 요청 예제 실행 결과

Restarted application in 84ms.
flutter: 제목 : sunt aut facere repellat provident occaecati excepturi optio reprehenderit

 
 
 
 


3️⃣ POST 요청 예제

Future createPost() async {
  try {
    final response = await dio.post(
      '<https://jsonplaceholder.typicode.com/posts>',
      data: {
        "title": "Dio 테스트",
        "body": "POST 요청 테스트",
        "userId": 1,
      },
    );
    print('응답: ${response.data}');
  } catch (e) {
    print('에러 발생: $e');
  }
}

void main() {  
	createPost();
}

 
🔎 코드 설명

  • post(url, data: {}) → POST 요청
  • data → 서버로 보낼 JSON 데이터를 Dart Map 형태로 작성

 
🔻 POST 요청 예제 실행 결과

Restarted application in 72ms.
flutter: 응답: {title: Dio 테스트, body: Post 요청 테스트, userId: 1, id: 101}

 
 
 
 


4️⃣ Dio에 옵션 및 인터셉터 추가

 

📌 개념 요약

  • 옵션(BaseOptions): Dio 인스턴스를 만들 때 공통 설정을 미리 지정해 놓는 기능
  • 인터셉터(Interceptor): 요청을 보내거나 응답을 받을 때 중간에 가로채서 가공하거나, 로깅하거나, 처리하는 기능

 

📍 BaseOptions는 왜 필요한가?

  • 내 앱에서 매번 같은 API 주소(https://api.example.com)를 사용해 호출하고 있음
  • 매 요청마다 headers나 timeout을 매번 적는 건 너무 번거로움

 

🔧 해결 방법: BaseOptions로 공통 설정을 묶어놓기

final dio = Dio(
  BaseOptions(
    baseUrl: '<https://api.example.com>',  // ✔ 모든 요청 앞에 자동으로 붙음
    connectTimeout: Duration(seconds: 5),
    receiveTimeout: Duration(seconds: 5),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer yourTokenHere', // ✔ 로그인 토큰 같은 공통 헤더
    },
  ),
);

✅ 이렇게 설정하면 이후 GET/POST 요청 시 baseUrl과 headers를 반복하지 않아도 됩니다.

 

📍 로깅 인터셉터는 왜 필요한가?

  • API 요청이 실패했는데 왜 실패했는지 알 수가 없음
  • 디버깅 중, 요청 내용이나 응답 내용을 확인하고 싶음

 

🔧 해결 방법: 요청/응답을 터미널에 로그로 출력

dio.interceptors.add(
  LogInterceptor(
    request: true,
    requestBody: true,
    responseBody: true,
    error: true,
  ),
);

 
어떤 URL로 요청했는지, 보낸 데이터, 받은 응답이 콘솔에 출력됩니다.
실무에서는 개발 환경에서만 로깅을 활성화하고, 운영에서는 끄는 방식으로 사용합니다.
 
 
 
 
 
 


5️⃣ 예외 처리 & 공통 응답 핸들링

 

📌 개념 요약

  • 네트워크 통신은 언제든 실패할 수 있습니다.
  • 실패했을 때 앱이 강제 종료되지 않도록 안전하게 예외를 처리해야 합니다.

 

📍 1. 왜 try-catch로 감싸야 하나요?

  • 서버가 꺼져 있거나, 인터넷이 끊겨 있는 경우 처리
  • 서버 응답이 404, 500 등의 오류 코드인 경우 처리

 

🔧 해결 방법: DioException을 이용한 에러 핸들링

try {
  final response = await dio.get('/posts/9999');
  print(response.data);
} on DioException catch (e) {
  if (e.response != null) {
    print('응답 에러: ${e.response?.statusCode}, ${e.response?.data}');
  } else {
    print('요청 실패 (서버 응답 없음): ${e.message}');
  }
}

 
사용자가 요청을 잘못하거나, 서버가 문제 있어도 앱이 죽지 않게 막아줍니다.
에러에 따라 사용자에게 토스트, 다이얼로그 등 알림 처리도 가능해집니다.
 
 
 
 
 


6️⃣ Dio + Riverpod을 연동하여 API 요청을 상태 기반으로 관리하는 예제

 
✅ 목표

  • Dio를 이용해 API 요청 (GET)
  • Riverpod의 FutureProvider를 이용해 상태 관리
  • 데이터를 UI에 출력 + 로딩/에러 상태 처리

 
✅ 구성 파일

lib/
├── main.dart
├── model/post.dart             ← 데이터 모델 정의
├── data/post_api.dart          ← Dio API 요청 정의
├── provider/post_provider.dart ← Riverpod Provider 정의
└── screen/post_screen.dart     ← UI 출력

 

📁 model/post.dart

// JSON 데이터를 Dart 객체로 변환 (모델 정의)
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'],
      title: json['title'],
      body: json['body'],
    );
  }
}

 
 

📁 data/post_api.dart

import 'package:dio/dio.dart';
import '../model/post.dart';

// Dio로 API 요청 수행
class PostApi {
  final Dio _dio = Dio(BaseOptions(
    baseUrl: '<https://jsonplaceholder.typicode.com>',
    connectTimeout: const Duration(seconds: 5),
    receiveTimeout: const Duration(seconds: 5),
    headers: {'Content-Type': 'application/json'},
  ));

  Future fetchPost(int id) async {
    try {
      final response = await _dio.get('/posts/$id');
      return Post.fromJson(response.data);
    } on DioException catch (e) {
      if (e.response != null) {
        throw '서버 에러: ${e.response?.statusCode}';
      } else {
        throw '연결 에러: ${e.message}';
      }
    }
  }
}

final postApi = PostApi(); // 외부에서 접근할 수 있도록 인스턴스 생성

 
 

📁 provider/post_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../data/post_api.dart';
import '../model/post.dart';

// Riverpod FutureProvider: Post 데이터 요청
final postProvider = FutureProvider<Post>((ref) async {
  return await postApi.fetchPost(1); // id=1인 게시물 요청
});

 
 

📁 screen/post_screen.dart

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

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

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

    return Scaffold(
      appBar: AppBar(title: const Text('📬 Riverpod + Dio 실습')),
      body: postAsync.when(
        data: (post) => Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('✅ 제목:', style: Theme.of(context).textTheme.titleLarge),
              Text(post.title, style: const TextStyle(fontSize: 18)),
              const SizedBox(height: 20),
              Text('📄 내용:', style: Theme.of(context).textTheme.titleMedium),
              Text(post.body),
            ],
          ),
        ),
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (err, stack) => Center(child: Text('❌ 오류: $err')),
      ),
    );
  }
}

 
 

📁 main.dart

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod + Dio 실습',
      theme: ThemeData(primarySwatch: Colors.indigo),
      home: const PostScreen(),
    );
  }
}

 
 

✅ 실행 결과

성공 시:

✅ 제목:
sunt aut facere repellat provident occaecati excepturi optio reprehenderit

📄 내용:
quia et suscipit...

 
🔻 실행 결과 성공 시 화면

 


 
실패 시:

❌ 오류: 서버 에러: 404

 
 
 
 
 
 


✅ 정리

구성 요소 설명
PostApiDio로 API 요청 담당 (실제 HTTP 통신)
postProviderRiverpod으로 요청 상태를 관리 (로딩, 성공, 에러)
PostScreenUI에서 상태에 따라 결과 출력 (when 메서드 사용)

 
💬 실무에서는 Dio와 Riverpod 조합으로 다음과 같은 시나리오를 처리합니다:

  • 로그인 후 토큰 자동 적용
  • 에러에 따라 다른 알림 처리 (ex. 401은 로그인 화면으로 이동)
  • JSON 데이터를 Model 클래스로 변환 후 UI에 렌더링

 
Flutter 앱을 기획부터 출시까지 직접 개발하는 1인 개발자에게 매우 유용한 패턴이라고 합니다.
 
 
 
 
 


📌 내용 요약

 
Dio는 Flutter에서 API 요청을 더 유연하고 강력하게 처리할 수 있는 라이브러리입니다.
기본 GET/POST 요청은 물론, 인터셉터, 타임아웃, 공통 헤더 등 실무에 필수적인 기능을 제공합니다.
http보다 구조화된 API 통신이 가능하고, 테스트 및 유지보수도 용이합니다.
향후 Retrofit 패턴과 결합하면 더 체계적인 API 레이어를 구성할 수 있습니다.

728x90