[Flutter 공부] HTTP 통신과 RESTful API 연동
- Developer/Flutter
- 2025. 3. 11.
HTTP 통신과 RESTful API 연동
여러분, 훌륭한 Flutter 앱을 만들고도 서버와 데이터를 주고받지 못해 고민하고 계신가요? 오늘 이 글이 그 해결책이 될 겁니다!
안녕하세요, 개발의 시작에는 어떻게 서버랑 통신해야 할지 몰라서 헤매기도 했는데, 참 그리운 시절이네요.
Flutter에서는 통신을 어떻게 할까요? 저는 주로 Dio 라이브러리를 사용하는데, 작업 속도가 확 빨라졌죠. 그래서 오늘은 제가 경험한 Flutter HTTP 통신의 모든 것을 여러분과 나누려고 합니다.
목차
HTTP 통신 기초와 Flutter에서의 중요성
모바일 앱 개발에서 HTTP 통신은... 음, 어떻게 표현해야 할까요? 그냥 선택이 아니라 필수예요! 특히 요즘 앱들은 대부분 서버와 데이터를 주고받으며 동작하잖아요. 로그인, 데이터 조회, 이미지 업로드... 이 모든 게 HTTP 통신 없이는 불가능합니다.
HTTP(HyperText Transfer Protocol)는 인터넷에서 데이터를 주고받는 가장 기본적인 프로토콜이에요. 클라이언트(우리의 Flutter 앱)가 서버에 요청(request)을 보내고, 서버는 그에 대한 응답(response)을 반환하는 구조로 작동합니다. 이게 바로 우리가 앱에서 서버와 데이터를 주고받는 기본 원리죠.
Flutter에서 HTTP 통신은 기본적으로 비동기(async) 방식으로 이루어집니다. 즉, 서버에 요청을 보낸 후 응답이 올 때까지 앱이 멈추지 않고 다른 작업을 계속할 수 있어요. 이를 위해 Dart의 Future와 async/await 문법을 자주 사용하게 됩니다.
Flutter에서는 HTTP 통신을 위한 여러 옵션이 있어요. 기본 http 패키지부터 시작해서 dio, chopper 같은 강력한 써드파티 라이브러리까지 다양하죠. 그중에서도 오늘은 기본 개념과 함께 제가 실무에서 자주 사용하는 dio 라이브러리에 대해 자세히 알아볼 거예요.
RESTful API의 개념과 작동 원리
RESTful API는 현대 웹 서비스의 표준이라고 할 수 있어요. REST(Representational State Transfer)는 HTTP 프로토콜을 효율적으로 사용하기 위한 아키텍처 스타일로, 리소스(자원)를 URI로, 행위는 HTTP 메서드로, 표현은 응답 형식으로 나타내요.
제가 처음 API 연동을 배울 때는 이런 개념들이 너무 추상적으로 느껴졌어요. 근데 실제로 써보면 생각보다 간단해요. 예를 들어 사용자 정보를 다루는 API가 있다고 생각해 볼까요?
HTTP 메서드 | 엔드포인트 | 동작 |
---|---|---|
GET | /users | 모든 사용자 목록 조회 |
GET | /users/{id} | 특정 ID의 사용자 정보 조회 |
POST | /users | 새 사용자 생성 |
PUT | /users/{id} | 특정 사용자 정보 전체 수정 |
PATCH | /users/{id} | 특정 사용자 정보 일부 수정 |
DELETE | /users/{id} | 특정 사용자 삭제 |
위 표에서 볼 수 있듯이, RESTful API는 HTTP 메서드(GET, POST, PUT, PATCH, DELETE)를 사용해 CRUD(Create, Read, Update, Delete) 작업을 표현해요. 이렇게 표준화된 방식으로 API를 설계하면 클라이언트와 서버 간의 통신이 직관적이고 일관성 있게 이루어질 수 있죠.
실제 API 응답은 대부분 JSON 형식으로 오는데, 이는 Flutter에서 Map<String, dynamic> 형태로 쉽게 다룰 수 있어요. 나중에 이 JSON 데이터를 Dart 객체로 변환하는 방법도 알아볼 거예요.
Flutter에서 HTTP 통신 구현하기
이제 Flutter에서 실제로 HTTP 통신을 구현하는 방법을 알아볼게요. 크게 세 가지 방법이 있어요: Flutter 기본 http 패키지 사용하기, Dio 라이브러리 사용하기, 그리고 http 클라이언트를 직접 구현하기. 오늘은 가장 기본적인 http 패키지부터 살펴보고, 다음 섹션에서 Dio를 자세히 다룰게요.
먼저 pubspec.yaml 파일에 패키지를 추가해야 해요:
dependencies:
flutter:
sdk: flutter
http: ^1.1.0 # 최신 버전은 다를 수 있어요
그리고 다음 명령어로 패키지를 설치해요:
flutter pub get
이제 실제로 API 호출을 구현해볼게요. 아래 코드는 http 패키지를 사용해 데이터를 가져오는 기본 예제예요:
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<List> fetchUsers() async {
final response = await http.get(Uri.parse('https://api.example.com/users'));
if (response.statusCode == 200) {
// JSON 응답을 Dart 객체로 변환
List data = jsonDecode(response.body);
return data.map((json) => User.fromJson(json)).toList();
} else {
// 에러 처리
throw Exception('Failed to load users: ${response.statusCode}');
}
}
HTTP 통신 시 알아두면 좋은 기본 사항들이 몇 가지 있어요:
- 모든 HTTP 요청은 비동기(async)로 처리되므로 Future와 async/await를 사용해야 해요.
- HTTP 응답에는 상태 코드(statusCode)가 포함되어 요청 성공 여부를 알려줘요 (200: 성공, 400: 잘못된 요청, 401: 인증 실패, 404: 찾을 수 없음, 500: 서버 오류).
- JSON 파싱은 dart:convert 패키지의 jsonDecode 함수를 사용해요.
- 네트워크 요청에는 항상 예외 처리를 추가해야 해요 (타임아웃, 서버 오류 등).
- Android 앱은 인터넷 권한이 필요해요 (AndroidManifest.xml에 추가).
HTTP 메서드별로 어떻게 요청을 보내는지 간단히 살펴볼게요:
// GET 요청
final getResponse = await http.get(Uri.parse('https://api.example.com/users/1'));
// POST 요청 (데이터 생성)
final postResponse = await http.post(
Uri.parse('https://api.example.com/users'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'name': 'John', 'email': 'john@example.com'}),
);
// PUT 요청 (데이터 전체 수정)
final putResponse = await http.put(
Uri.parse('https://api.example.com/users/1'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'name': 'John Doe', 'email': 'john.doe@example.com'}),
);
// PATCH 요청 (데이터 일부 수정)
final patchResponse = await http.patch(
Uri.parse('https://api.example.com/users/1'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'name': 'John Doe'}),
);
// DELETE 요청 (데이터 삭제)
final deleteResponse = await http.delete(Uri.parse('https://api.example.com/users/1'));
이렇게 http 패키지를 사용해 기본적인 API 호출을 구현할 수 있어요. 하지만 실제 앱 개발에서는 더 강력한 기능(인터셉터, 타임아웃 설정, 재시도 등)이 필요한 경우가 많죠. 그래서 다음 섹션에서는 더 강력한 Dio 라이브러리에 대해 알아볼 거예요.
Dio 라이브러리 완벽 활용 가이드
Dio는 기본 http 패키지보다 훨씬 더 강력한 기능을 제공하는 HTTP 클라이언트 라이브러리예요. 처음 Dio를 접했을 때는 "뭐가 이렇게 복잡해?"라고 생각했는데, 써보니까 정말 편리하더라고요. 특히 인터셉터, 글로벌 설정, 요청 취소 같은 기능은 큰 프로젝트에서 정말 유용해요.
먼저 pubspec.yaml에 Dio 패키지를 추가해야 해요:
dependencies:
flutter:
sdk: flutter
dio: ^5.3.2 # 최신 버전은 다를 수 있어요
그리고 다음 명령어로 패키지를 설치해요:
flutter pub get
이제 Dio를 사용한 기본적인 API 호출 코드를 살펴볼게요:
import 'package:dio/dio.dart';
class ApiService {
final Dio _dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(milliseconds: 5000),
receiveTimeout: Duration(milliseconds: 3000),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
Future<List> getUsers() async {
try {
final response = await _dio.get('/users');
List data = response.data;
return data.map((json) => User.fromJson(json)).toList();
} on DioException catch (e) {
// Dio 예외 처리
throw _handleError(e);
} catch (e) {
// 일반 예외 처리
throw Exception('알 수 없는 오류가 발생했습니다: $e');
}
}
Exception _handleError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return Exception('서버 연결 시간이 초과되었습니다');
case DioExceptionType.badResponse:
return Exception('서버 응답 오류: ${e.response?.statusCode}');
case DioExceptionType.cancel:
return Exception('요청이 취소되었습니다');
default:
return Exception('네트워크 오류가 발생했습니다');
}
}
}
Dio의 가장 강력한 기능 중 하나는 인터셉터예요. 인터셉터를 사용하면 모든 요청이나 응답을 중간에 가로채서 처리할 수 있어요. 예를 들어, 모든 요청에 인증 토큰을 추가하거나 응답 데이터를 로깅하는 등의 작업을 할 수 있죠.
// 인터셉터 추가 예제
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// 요청이 전송되기 전 수행
options.headers['Authorization'] = 'Bearer $token';
print('요청 [${options.method}] ${options.uri}');
return handler.next(options);
},
onResponse: (response, handler) {
// 응답을 받은 후 수행
print('응답: [${response.statusCode}] ${response.data}');
return handler.next(response);
},
onError: (DioException e, handler) {
// 오류 발생 시 수행
print('오류: ${e.message}');
// 401 오류(인증 실패)면 토큰 갱신 후 재시도
if (e.response?.statusCode == 401) {
return _refreshTokenAndRetry(e.requestOptions, handler);
}
return handler.next(e);
},
),
);
Dio의 또 다른 장점은 다양한 요청 방식을 지원한다는 거예요. GET, POST부터 PUT, PATCH, DELETE까지 모든 HTTP 메서드를 쉽게 사용할 수 있죠:
// GET 요청
final getResponse = await _dio.get('/users');
// POST 요청
final postResponse = await _dio.post(
'/users',
data: {'name': 'John', 'email': 'john@example.com'},
);
// PUT 요청
final putResponse = await _dio.put(
'/users/1',
data: {'name': 'John Doe', 'email': 'john.doe@example.com'},
);
// PATCH 요청
final patchResponse = await _dio.patch(
'/users/1',
data: {'name': 'John Doe'},
);
// DELETE 요청
final deleteResponse = await _dio.delete('/users/1');
API 통신 에러 핸들링과 예외 처리
API 통신에서 가장 중요하지만 쉽게 간과되는 부분이 바로 에러 핸들링이에요. 네트워크 상태, 서버 장애, 인증 문제 등 여러 가지 이유로 API 호출이 실패할 수 있거든요. 이런 상황을 제대로 처리하지 않으면 앱이 갑자기 멈추거나 사용자에게 이상한 결과를 보여줄 수 있어요.
Flutter에서 API 오류를 처리하는 방법은 크게 두 가지인데요.
API 오류 처리는 사용자 경험에 직접적인 영향을 미칩니다. "알 수 없는 오류가 발생했습니다"보다는 "네트워크 연결이 불안정합니다. 다시 시도해 주세요."와 같이 구체적이고 행동 지향적인 메시지를 제공하는 것이 좋습니다.
오류 타입 | HTTP 상태 코드 | 사용자 친화적 메시지 | 권장 처리 방법 |
---|---|---|---|
네트워크 연결 실패 | - | "인터넷 연결을 확인해 주세요" | 재시도 버튼 제공, 오프라인 캐시 사용 |
타임아웃 | - | "서버 응답이 지연되고 있습니다" | 자동 재시도(횟수 제한) |
인증 오류 | 401 | "로그인이 필요합니다" | 토큰 갱신 후 재시도, 로그인 화면으로 이동 |
권한 부족 | 403 | "이 기능에 접근할 권한이 없습니다" | 업그레이드 유도, 권한 요청 안내 |
리소스 없음 | 404 | "요청하신 정보를 찾을 수 없습니다" | 대체 콘텐츠 제공, 뒤로 가기 옵션 |
서버 오류 | 500, 502, 503 | "일시적인 서버 오류가 발생했습니다" | 나중에 자동 재시도, 캐시된 데이터 사용 |
다음은 Dio를 사용한 좀 더 체계적인 에러 핸들링 예제입니다.
class ApiException implements Exception {
final String message;
final int? statusCode;
final String? statusMessage;
final dynamic data;
ApiException({
required this.message,
this.statusCode,
this.statusMessage,
this.data,
});
@override
String toString() => message;
}
class ApiService {
// ... 기존 코드 ...
Future _handleRequest(Future<Response> Function() request) async {
try {
final response = await request();
return response.data as T;
} on DioException catch (e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
throw ApiException(
message: '서버 연결 시간이 초과되었습니다. 네트워크 상태를 확인해 주세요.',
statusCode: e.response?.statusCode,
);
case DioExceptionType.badResponse:
// HTTP 상태 코드에 따른 처리
final statusCode = e.response?.statusCode;
switch (statusCode) {
case 400:
throw ApiException(
message: '잘못된 요청입니다.',
statusCode: statusCode,
data: e.response?.data,
);
case 401:
// 토큰 갱신 시도 후 실패하면 로그아웃 처리
try {
return await _refreshTokenAndRetry(e.requestOptions);
} catch (_) {
throw ApiException(
message: '로그인이 필요합니다.',
statusCode: statusCode,
);
}
case 403:
throw ApiException(
message: '접근 권한이 없습니다.',
statusCode: statusCode,
);
case 404:
throw ApiException(
message: '요청하신 정보를 찾을 수 없습니다.',
statusCode: statusCode,
);
case 500:
case 502:
case 503:
throw ApiException(
message: '서버 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.',
statusCode: statusCode,
);
default:
throw ApiException(
message: '알 수 없는 오류가 발생했습니다.',
statusCode: statusCode,
statusMessage: e.response?.statusMessage,
);
}
case DioExceptionType.cancel:
throw ApiException(message: '요청이 취소되었습니다.');
default:
throw ApiException(
message: '네트워크 오류가 발생했습니다. 인터넷 연결을 확인해 주세요.',
);
}
} catch (e) {
throw ApiException(message: '예상치 못한 오류가 발생했습니다: $e');
}
}
// 이 함수를 통해 모든 API 요청을 처리
Future<List> getUsers() async {
final data = await _handleRequest<List>(() => _dio.get('/users'));
return data.map((json) => User.fromJson(json)).toList();
}
}
Flutter HTTP 통신 Best Practices
마지막으로, Flutter에서 HTTP 통신을 구현할 때 알아두면 좋은 몇 가지 모범 사례를 알아볼게요. 이것들은 제가 실제 프로젝트를 개발하면서 배운 것들이에요.
항상 이런 원칙들을 지키려고 노력하지만, 가끔은 빠른 개발을 위해 타협할 때도 있어요. 하지만 장기적으로는 이런 패턴을 따르는 것이 유지보수에 큰 도움이 된다는 걸 경험으로 알게 되었죠.
- 계층화된 아키텍처 사용하기: API 호출, 데이터 모델링, 비즈니스 로직, UI를 분리하세요. 레포지토리 패턴이나 BLoC, Provider와 같은 상태 관리 솔루션을 사용하면 좋아요.
- 모델 클래스 사용하기: API 응답을 그냥 Map으로 사용하지 말고, 명시적인 Dart 클래스로 변환하세요. JSON 직렬화/역직렬화 코드는 자동화 도구(json_serializable 패키지)를 사용하는 것이 좋아요.
- 싱글톤 API 클라이언트: 앱 전체에서 하나의 API 클라이언트 인스턴스를 공유하는 것이 좋아요. 이렇게 하면 인증, 로깅, 캐싱 등의 공통 설정을 중앙에서 관리할 수 있어요.
- 요청 취소 지원: 사용자가 화면을 벗어나거나 다른 작업을 시작할 때 진행 중인 API 요청을 취소할 수 있도록 하세요. Dio의 CancelToken을 사용하면 쉽게 구현할 수 있어요.
- 캐싱 전략 구현: 자주 변경되지 않는 데이터는 로컬에 캐싱하여 네트워크 사용량을 줄이고 오프라인 지원을 향상시키세요. Hive, sqflite, shared_preferences 등을 활용할 수 있어요.
- 모의 API 응답(Mock) 사용: 개발 초기 단계나 테스트 시에는 실제 서버 없이도 작업할 수 있도록 모의 API 응답을 준비하세요. 이렇게 하면 백엔드 개발이 완료되기 전에도 UI 작업을 진행할 수 있어요.
- 환경별 설정 관리: 개발, 스테이징, 프로덕션 환경에 따라 API 엔드포인트가 달라질 수 있어요. 환경별로 설정을 관리하는 방법을 구현하세요.
- 네트워크 상태 모니터링: connectivity_plus 패키지를 사용하여 네트워크 연결 상태를 모니터링하고, 오프라인 상태일 때는 적절한 피드백을 제공하세요.
- 재시도 메커니즘 구현: 일시적인 네트워크 오류나 서버 오류의 경우, 자동으로 재시도하는 메커니즘을 구현하세요. 단, 무한 재시도는 피하고 적절한 횟수 제한을 설정하세요.
- 로깅 시스템 구축: 디버그 모드에서는 API 요청과 응답을 상세하게 로깅하고, 릴리즈 모드에서는 중요한 오류만 로깅하도록 설정하세요. 개인정보가 포함된 데이터는 로깅하지 않도록 주의하세요.
마지막으로, 실제 프로젝트에서 Dio를 사용한 HTTP 통신 서비스의 전체 구조를 보여드릴게요. 이 예제는 실제 앱에서 사용할 수 있는 완전한 형태의 API 서비스예요.
// api_client.dart
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import '../models/api_exception.dart';
import '../utils/token_manager.dart';
class ApiClient {
static final ApiClient _instance = ApiClient._internal();
factory ApiClient() => _instance;
late Dio _dio;
final TokenManager _tokenManager = TokenManager();
final String baseUrl = 'https://api.example.com';
ApiClient._internal() {
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: Duration(milliseconds: 5000),
receiveTimeout: Duration(milliseconds: 3000),
contentType: 'application/json',
responseType: ResponseType.json,
));
_setupInterceptors();
}
void _setupInterceptors() {
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) async {
// 토큰 추가
final token = await _tokenManager.getAccessToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
// 디버그 모드에서만 로깅
if (kDebugMode) {
print('🌐 요청: ${options.method} ${options.uri}');
print('헤더: ${options.headers}');
if (options.data != null) {
print('데이터: ${options.data}');
}
}
return handler.next(options);
},
onResponse: (response, handler) {
if (kDebugMode) {
print('✅ 응답: [${response.statusCode}] ${response.requestOptions.uri}');
print('데이터: ${response.data}');
}
return handler.next(response);
},
onError: (DioException e, handler) async {
if (kDebugMode) {
print('❌ 오류: [${e.response?.statusCode}] ${e.requestOptions.uri}');
print('메시지: ${e.message}');
if (e.response?.data != null) {
print('응답 데이터: ${e.response?.data}');
}
}
// 401 오류(인증 실패)일 때 토큰 갱신 시도
if (e.response?.statusCode == 401) {
try {
final isRefreshed = await _tokenManager.refreshToken();
if (isRefreshed) {
// 토큰 갱신 성공, 원래 요청 재시도
return handler.resolve(await _retry(e.requestOptions));
}
} catch (refreshError) {
// 토큰 갱신 실패, 로그아웃 처리 필요
_tokenManager.clearTokens();
// 로그아웃 이벤트 발생 (이벤트 버스나 상태 관리 솔루션 사용)
}
}
return handler.next(e);
},
),
);
}
Future<Response> _retry(RequestOptions requestOptions) async {
final token = await _tokenManager.getAccessToken();
final options = Options(
method: requestOptions.method,
headers: {
...requestOptions.headers,
'Authorization': 'Bearer $token',
},
);
return _dio.request(
requestOptions.path,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters,
options: options,
);
}
// 기본 API 메서드들
Future get(String path, {
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
}) async {
try {
final response = await _dio.get(
path,
queryParameters: queryParameters,
cancelToken: cancelToken,
);
return response.data as T;
} catch (e) {
throw _handleError(e);
}
}
Future post(String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
}) async {
try {
final response = await _dio.post(
path,
data: data,
queryParameters: queryParameters,
cancelToken: cancelToken,
);
return response.data as T;
} catch (e) {
throw _handleError(e);
}
}
// PUT, PATCH, DELETE 메서드도 유사하게 구현...
Exception _handleError(dynamic e) {
if (e is DioException) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return ApiException(
message: '서버 연결 시간이 초과되었습니다. 네트워크 상태를 확인해 주세요.',
statusCode: e.response?.statusCode,
);
case DioExceptionType.badResponse:
return _handleResponseError(e);
case DioExceptionType.cancel:
return ApiException(message: '요청이 취소되었습니다.');
default:
return ApiException(
message: '네트워크 오류가 발생했습니다. 인터넷 연결을 확인해 주세요.',
);
}
}
return ApiException(message: '예상치 못한 오류가 발생했습니다: $e');
}
ApiException _handleResponseError(DioException e) {
final statusCode = e.response?.statusCode;
final data = e.response?.data;
// 서버에서 보내는 오류 메시지가 있으면 사용
String? serverMessage;
if (data is Map && data.containsKey('message')) {
serverMessage = data['message'] as String?;
}
switch (statusCode) {
case 400:
return ApiException(
message: serverMessage ?? '잘못된 요청입니다.',
statusCode: statusCode,
data: data,
);
case 401:
return ApiException(
message: '로그인이 필요합니다.',
statusCode: statusCode,
);
case 403:
return ApiException(
message: '접근 권한이 없습니다.',
statusCode: statusCode,
);
case 404:
return ApiException(
message: '요청하신 정보를 찾을 수 없습니다.',
statusCode: statusCode,
);
case 500:
case 502:
case 503:
return ApiException(
message: '서버 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.',
statusCode: statusCode,
);
default:
return ApiException(
message: serverMessage ?? '알 수 없는 오류가 발생했습니다.',
statusCode: statusCode,
statusMessage: e.response?.statusMessage,
);
}
}
}
위 코드는 교육 목적으로 제공된 것이며, 실제 프로젝트에 적용할 때는 보안 취약점을 고려하여 수정해야 합니다. 특히 인증 토큰 관리, 민감한 정보 로깅, SSL 인증서 검증 등에 주의하세요.
이제 여러분은 Flutter에서 HTTP 통신과 RESTful API 연동에 대한 기본 개념부터 실제 구현 방법, 그리고 모범 사례까지 배웠어요. 이 지식을 바탕으로 서버와 데이터를 주고받는 더 견고하고 유지보수하기 쉬운 앱을 만들 수 있을 겁니다.
간단한 앱을 만들고 있는데, 두 패키지 중 어떤 것이 더 적합할지 고민이에요.
http 패키지는 Flutter 팀이 제공하는 기본 패키지로 간단한 HTTP 요청에 적합해요. 작은 프로젝트나 API 호출이 몇 개 없는 앱이라면 충분합니다. 반면 dio는 인터셉터, 요청 취소, 자동 재시도, 폼 데이터 등 더 많은 기능을 제공하는 강력한 패키지입니다. 중대형 프로젝트나 복잡한 API 통신이 필요한 경우에는 dio를 선택하는 것이 좋습니다. 처음에는 http로 시작해도 괜찮지만, 프로젝트가 커지면 dio로 마이그레이션하는 경우가 많아요.
API에서 받은 JSON 데이터를 Dart 클래스로 변환하는 작업이 너무 반복적이고 지루해요. 더 효율적인 방법이 있을까요?
수동으로 JSON 파싱 코드를 작성하는 것은 정말 지루하고 오류가 발생하기 쉽습니다. 대신 json_serializable 패키지를 사용하면 코드 생성을 통해 이 과정을 자동화할 수 있어요. 모델 클래스에 몇 가지 어노테이션을 추가하고 build_runner를 실행하면 fromJson과 toJson 메서드가 자동으로 생성됩니다. 이는 특히 복잡한 중첩 객체가 있는 경우 매우 유용합니다. 또 다른 대안으로는 freezed 패키지가 있는데, 이는 불변(immutable) 객체를 쉽게 만들 수 있게 해주고 json_serializable 기능도 포함하고 있어요.
API에서 JWT 인증을 사용하는데, 토큰이 만료되면 자동으로 갱신하고 싶어요. 어떻게 구현하는 것이 좋을까요?
JWT 토큰 관리는 보안이 중요한 부분이에요. 액세스 토큰과 리프레시 토큰을 secure_storage나 flutter_secure_storage 패키지를 사용해 안전하게 저장하세요. 그리고 Dio 인터셉터를 활용해 401 오류(인증 실패)가 발생하면 리프레시 토큰으로 새 액세스 토큰을 요청하고, 성공하면 원래 요청을 재시도하는 로직을 구현하면 됩니다. 이때 동시에 여러 API 호출이 401 오류를 받을 경우 토큰 갱신이 여러 번 호출되지 않도록 토큰 갱신 요청을 큐에 넣거나 뮤텍스를 사용해 관리하는 것이 좋아요. 또한 리프레시 토큰도 만료된 경우에는 사용자를 로그아웃 시키고 로그인 화면으로 이동시켜야 합니다.
일부 API 호출이 꽤 오래 걸리는데, 사용자가 앱이 멈춘 것처럼 느끼지 않게 하고 싶어요.
느린 API 응답에 대처하는 여러 방법이 있어요. 첫째, 스켈레톤 UI나 적절한 로딩 인디케이터를 사용해 사용자에게 작업이 진행 중임을 알려주세요. 둘째, 캐싱을 구현해 이전에 로드한 데이터를 즉시 표시하고 백그라운드에서 새로운 데이터를 가져오는 "stale-while-revalidate" 패턴을 사용하세요. 셋째, 페이지네이션을 통해 대량의 데이터를 작은 청크로 나누어 로드하세요. 넷째, 중요하지 않은 API 호출은 우선순위를 낮추고 중요한 콘텐츠를 먼저 로드하세요. 다섯째, 앱 상태를 최적화하여 불필요한 API 호출을 줄이세요. 마지막으로, 너무 오래 걸리는 요청에 대해서는 타임아웃을 설정하고 적절한 피드백을 제공하세요.
서버에서 오류 응답이 올 때 사용자에게 어떻게 알려주는 것이 가장 좋은 방법일까요?
오류 메시지는 사용자가 이해하기 쉽고 다음에 무엇을 해야 할지 안내해야 합니다. 기술적인 오류 코드나 "알 수 없는 오류가 발생했습니다"와 같은 모호한 메시지는 피하세요. 대신 "인터넷 연결이 불안정합니다. Wi-Fi 연결을 확인한 후 다시 시도해 주세요"와 같이 구체적이고 행동 지향적인 메시지를 제공하세요. UI에 오류를 표시하는 방법으로는 중요한 오류의 경우 알림 다이얼로그, 덜 중요한 오류는 스낵바나 토스트 메시지, 그리고 특정 컨텐츠 로딩 실패의 경우 해당 영역에 인라인 오류 메시지와 재시도 버튼을 표시하는 것이 좋습니다. 또한 사용자가 지원팀에 문의할 수 있도록 오류 ID나 간단한 디버그 정보를 제공하는 것도 고려해 보세요.
사용자가 이미지를 업로드하고 PDF 파일을 다운로드할 수 있는 기능을 구현하고 싶은데, 어떻게 시작해야 할지 모르겠어요.
파일 업로드는 Dio의 FormData를 사용하면 쉽게 구현할 수 있어요. 먼저 image_picker 패키지로 이미지를 선택하고, 그 다음 FormData.fromMap을 사용해 멀티파트 요청을 만들어 서버에 전송하면 됩니다. 파일 다운로드의 경우, Dio의 download 메서드를 사용하고 파일 저장 경로를 path_provider 패키지로 구하면 됩니다. 다운로드 진행 상황을 모니터링하려면 onReceiveProgress 콜백을 사용하세요. 또한 다운로드된 파일을 열기 위해서는 url_launcher(웹 링크), open_file(로컬 파일), file_picker(파일 선택) 등의 패키지를 활용할 수 있습니다. 대용량 파일의 경우 청크 단위 업로드나 다운로드를 구현하고, 네트워크 연결이 불안정한 환경을 고려해 중단 지점부터 재개할 수 있는 기능도 고려해 보세요.
감사합니다.
'Developer > Flutter' 카테고리의 다른 글
[Flutter 공부] Provider와 상태 관리 (0) | 2025.03.15 |
---|---|
[Flutter 공부] JSON 파싱과 직렬화 (0) | 2025.03.13 |
[Flutter 공부] 간단한 애니메이션 구현 방법 (0) | 2025.03.11 |
[Flutter 공부하기] 커스텀 위젯 만들기: 재사용 가능한 위젯 설계와 구현 (0) | 2025.03.11 |
[Flutter 공부] Form 관리와 유효성 검사 - 사용자 입력 폼 구현과 검증 (0) | 2025.03.10 |