HTTP 통신 — http, dio 패키지와 REST API 연동
HTTP 통신 — http, dio 패키지와 REST API 연동
앱에서 서버와 통신하는 것은 기본 중의 기본입니다. Flutter에서 REST API를 호출하는 두 가지 대표 패키지인 http와 dio를 비교하며 정리해보겠습니다.
http 패키지 (기본)
dependencies:
http: ^1.2.0
import 'package:http/http.dart' as http;
import 'dart:convert';
class ApiService {
static const _baseUrl = 'https://jsonplaceholder.typicode.com';
// GET 요청
Future<List<Post>> getPosts() async {
final response = await http.get(
Uri.parse('$_baseUrl/posts'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final List<dynamic> jsonList = jsonDecode(response.body);
return jsonList.map((json) => Post.fromJson(json)).toList();
} else {
throw Exception('게시글 로딩 실패: ${response.statusCode}');
}
}
// POST 요청
Future<Post> createPost(String title, String body) async {
final response = await http.post(
Uri.parse('$_baseUrl/posts'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'title': title,
'body': body,
'userId': 1,
}),
);
if (response.statusCode == 201) {
return Post.fromJson(jsonDecode(response.body));
} else {
throw Exception('게시글 생성 실패');
}
}
// PUT 요청
Future<Post> updatePost(int id, String title) async {
final response = await http.put(
Uri.parse('$_baseUrl/posts/$id'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'title': title}),
);
if (response.statusCode == 200) {
return Post.fromJson(jsonDecode(response.body));
} else {
throw Exception('업데이트 실패');
}
}
// DELETE 요청
Future<void> deletePost(int id) async {
final response = await http.delete(
Uri.parse('$_baseUrl/posts/$id'),
);
if (response.statusCode != 200) {
throw Exception('삭제 실패');
}
}
}
dio 패키지 (고급)
dio는 인터셉터, 취소 토큰, 파일 업로드 등 고급 기능을 제공합니다.
dependencies:
dio: ^5.4.0
import 'package:dio/dio.dart';
class DioApiService {
late final Dio _dio;
DioApiService() {
_dio = Dio(BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {'Content-Type': 'application/json'},
));
// 인터셉터 추가
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
));
}
// GET
Future<List<Post>> getPosts() async {
try {
final response = await _dio.get('/posts');
final List<dynamic> data = response.data;
return data.map((json) => Post.fromJson(json)).toList();
} on DioException catch (e) {
throw _handleError(e);
}
}
// POST
Future<Post> createPost(String title, String body) async {
try {
final response = await _dio.post('/posts', data: {
'title': title,
'body': body,
'userId': 1,
});
return Post.fromJson(response.data);
} on DioException catch (e) {
throw _handleError(e);
}
}
// 에러 처리 헬퍼
Exception _handleError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
return Exception('연결 시간 초과');
case DioExceptionType.receiveTimeout:
return Exception('응답 시간 초과');
case DioExceptionType.badResponse:
return Exception('서버 에러: ${e.response?.statusCode}');
case DioExceptionType.cancel:
return Exception('요청 취소됨');
default:
return Exception('네트워크 에러: ${e.message}');
}
}
}
http vs dio 비교
| 기능 | http | dio |
|---|---|---|
| 기본 CRUD | O | O |
| 인터셉터 | X | O |
| 요청 취소 | X | O |
| 파일 업로드 | 제한적 | 쉬움 |
| 타임아웃 | 제한적 | 상세 설정 |
| 의존성 크기 | 작음 | 큼 |
간단한 API 호출만 필요하면 http, 인터셉터나 파일 업로드 등 고급 기능이 필요하면 dio를 사용하세요.
인터셉터 활용 (dio)
// 인증 토큰 자동 첨부
class AuthInterceptor extends Interceptor {
final TokenStorage tokenStorage;
AuthInterceptor(this.tokenStorage);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final token = tokenStorage.accessToken;
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// 401이면 토큰 갱신 시도
if (err.response?.statusCode == 401) {
try {
await tokenStorage.refreshToken();
// 원래 요청 재시도
final response = await _retry(err.requestOptions);
handler.resolve(response);
return;
} catch (_) {
// 갱신 실패 시 로그아웃
}
}
handler.next(err);
}
}
// 인터셉터 등록
_dio.interceptors.add(AuthInterceptor(tokenStorage));
파일 업로드
Future<String> uploadImage(File imageFile) async {
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
imageFile.path,
filename: 'photo.jpg',
),
'description': '프로필 사진',
});
final response = await _dio.post(
'/upload',
data: formData,
onSendProgress: (sent, total) {
final progress = (sent / total * 100).toStringAsFixed(0);
print('업로드 진행: $progress%');
},
);
return response.data['url'];
}
FutureBuilder로 API 데이터 표시
class PostListScreen extends StatefulWidget {
const PostListScreen({super.key});
@override
State<PostListScreen> createState() => _PostListScreenState();
}
class _PostListScreenState extends State<PostListScreen> {
late Future<List<Post>> _postsFuture;
@override
void initState() {
super.initState();
_postsFuture = ApiService().getPosts();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('게시글')),
body: FutureBuilder<List<Post>>(
future: _postsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('에러: ${snapshot.error}'));
}
final posts = snapshot.data!;
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(posts[index].title),
subtitle: Text(
posts[index].body,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
);
},
);
},
),
);
}
}
데이터 모델
class Post {
final int id;
final String title;
final String body;
final int userId;
const Post({
required this.id,
required this.title,
required this.body,
required this.userId,
});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'] as int,
title: json['title'] as String,
body: json['body'] as String,
userId: json['userId'] as int,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'body': body,
'userId': userId,
};
}
}
정리
- 간단한 API 통신은
http패키지, 고급 기능이 필요하면dio를 사용하세요 - dio의 인터셉터로 인증 토큰 자동 첨부, 에러 로깅 등을 처리할 수 있습니다
- 에러 처리를 꼼꼼히 하고, 타임아웃을 반드시 설정하세요
FutureBuilder로 비동기 API 결과를 UI에 바인딩합니다- 실무에서는 Repository 패턴으로 API 호출을 추상화하는 것이 좋습니다
댓글 로딩 중...