2025. 3. 26. 16:53ㆍ같이 공부합시다 - Flutter/Dart & Flutter 기초부터 실전까지
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
✅ 정리
| 구성 요소 | 설명 |
| PostApi | Dio로 API 요청 담당 (실제 HTTP 통신) |
| postProvider | Riverpod으로 요청 상태를 관리 (로딩, 성공, 에러) |
| PostScreen | UI에서 상태에 따라 결과 출력 (when 메서드 사용) |
💬 실무에서는 Dio와 Riverpod 조합으로 다음과 같은 시나리오를 처리합니다:
- 로그인 후 토큰 자동 적용
- 에러에 따라 다른 알림 처리 (ex. 401은 로그인 화면으로 이동)
- JSON 데이터를 Model 클래스로 변환 후 UI에 렌더링
Flutter 앱을 기획부터 출시까지 직접 개발하는 1인 개발자에게 매우 유용한 패턴이라고 합니다.
📌 내용 요약
✅ Dio는 Flutter에서 API 요청을 더 유연하고 강력하게 처리할 수 있는 라이브러리입니다.
✅ 기본 GET/POST 요청은 물론, 인터셉터, 타임아웃, 공통 헤더 등 실무에 필수적인 기능을 제공합니다.
✅ http보다 구조화된 API 통신이 가능하고, 테스트 및 유지보수도 용이합니다.
✅ 향후 Retrofit 패턴과 결합하면 더 체계적인 API 레이어를 구성할 수 있습니다.
'같이 공부합시다 - Flutter > Dart & Flutter 기초부터 실전까지' 카테고리의 다른 글
| 📌 Day 18: Flutter JSON 데이터 처리 (jsonDecode, jsonEncode) (0) | 2025.03.28 |
|---|---|
| 📌 Day 16: Flutter HTTP 패키지를 활용한 API 요청 (GET, POST, Json 데이터 처리, FutureProvider< (1) | 2025.03.19 |
| 📌 Day 15: Riverpod을 활용한 간단한 CRUD 구현 (0) | 2025.03.17 |
| 📌 Day 14: Riverpod 상태 관리 패턴 (Provider와 비교하며 배우는 실전 적용법) (1) | 2025.03.07 |
| 📌 Day 13: Provider 상태 관리 적용 테스트 (Counter 앱, 다크모드까지) (1) | 2025.03.06 |