HTTP 통신 — http, dio 패키지와 REST API 연동

앱에서 서버와 통신하는 것은 기본 중의 기본입니다. Flutter에서 REST API를 호출하는 두 가지 대표 패키지인 http와 dio를 비교하며 정리해보겠습니다.


http 패키지 (기본)

YAML
dependencies:
  http: ^1.2.0
DART
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는 인터셉터, 취소 토큰, 파일 업로드 등 고급 기능을 제공합니다.

YAML
dependencies:
  dio: ^5.4.0
DART
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 비교

기능httpdio
기본 CRUDOO
인터셉터XO
요청 취소XO
파일 업로드제한적쉬움
타임아웃제한적상세 설정
의존성 크기작음

간단한 API 호출만 필요하면 http, 인터셉터나 파일 업로드 등 고급 기능이 필요하면 dio를 사용하세요.


인터셉터 활용 (dio)

DART
// 인증 토큰 자동 첨부
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));

파일 업로드

DART
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 데이터 표시

DART
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,
                ),
              );
            },
          );
        },
      ),
    );
  }
}

데이터 모델

DART
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 호출을 추상화하는 것이 좋습니다
댓글 로딩 중...