HTTP 패키지와 Dio 패키지의 차이점은 무엇인가요?

질문

Flutter에서 네트워크 요청을 처리할 때 사용되는 HTTP 패키지와 Dio 패키지의 주요 차이점과 각각의 장단점은 무엇인가요?

답변

Flutter에서 네트워크 통신을 위해 가장 많이 사용되는 두 패키지인 httpdio는 각각 고유한 특징과 장단점을 가지고 있습니다. 이 두 패키지의 차이점을 이해하면 프로젝트에 더 적합한 솔루션을 선택하는 데 도움이 됩니다.

1. 기본 개요

HTTP 패키지

dependencies:
  http: ^1.1.0
  • Flutter 팀에서 공식적으로 관리하는 패키지
  • 간단한 HTTP 요청을 위한 기본적인 API 제공
  • 가볍고 직관적인 API로 쉽게 시작 가능

Dio 패키지

dependencies:
  dio: ^5.3.2
  • 더 풍부한 기능을 갖춘 강력한 HTTP 클라이언트
  • 인터셉터, 글로벌 설정, 요청 취소, FormData, 파일 다운로드 등 고급 기능 제공
  • 좀 더 복잡한 API이지만 더 많은 기능 제공

2. 기본 사용법 비교

HTTP 패키지 기본 사용

import 'package:http/http.dart' as http;
import 'dart:convert';

Future<void> fetchData() async {
  final response = await http.get(Uri.parse('https://api.example.com/data'));

  if (response.statusCode == 200) {
    final data = json.decode(response.body);
    print('데이터: $data');
  } else {
    print('요청 실패: ${response.statusCode}');
  }
}

Future<void> postData() async {
  final response = await http.post(
    Uri.parse('https://api.example.com/create'),
    headers: {'Content-Type': 'application/json'},
    body: json.encode({'name': '홍길동', 'email': 'hong@example.com'}),
  );

  if (response.statusCode == 201) {
    print('생성 성공: ${response.body}');
  } else {
    print('요청 실패: ${response.statusCode}');
  }
}

Dio 패키지 기본 사용

import 'package:dio/dio.dart';

Future<void> fetchData() async {
  final dio = Dio();
  try {
    final response = await dio.get('https://api.example.com/data');
    print('데이터: ${response.data}');
  } catch (e) {
    if (e is DioException) {
      print('요청 실패: ${e.response?.statusCode}');
      print('에러 정보: ${e.message}');
    } else {
      print('에러: $e');
    }
  }
}

Future<void> postData() async {
  final dio = Dio();
  try {
    final response = await dio.post(
      'https://api.example.com/create',
      data: {'name': '홍길동', 'email': 'hong@example.com'},
      options: Options(
        headers: {'Content-Type': 'application/json'},
      ),
    );
    print('생성 성공: ${response.data}');
  } catch (e) {
    print('에러: $e');
  }
}

3. 주요 차이점

3.1 기능 및 API 디자인

기능 HTTP 패키지 Dio 패키지
API 디자인 간단하고 직관적, 최소한의 기능 풍부한 기능, 더 복잡한 API
JSON 파싱 수동으로 json.decode(response.body) 호출 필요 자동으로 JSON 응답을 Dart 객체로 변환
요청 본문 문자열만 지원 다양한 데이터 타입 지원 (Map, FormData 등)
타임아웃 설정 별도 구현 필요 내장 지원
에러 처리 기본적인 상태 코드 확인 필요 전용 에러 클래스와 인터셉터로 더 강력한 에러 처리

3.2 고급 기능 비교

인터셉터

HTTP 패키지: 공식적인 인터셉터 지원 없음. 직접 구현 필요

Dio 패키지: 내장 인터셉터 지원

final dio = Dio();

// 요청 인터셉터
dio.interceptors.add(
  InterceptorsWrapper(
    onRequest: (options, handler) {
      // 모든 요청에 인증 토큰 추가
      options.headers['Authorization'] = 'Bearer YOUR_TOKEN';
      return handler.next(options);
    },
    onResponse: (response, handler) {
      // 응답 처리
      return handler.next(response);
    },
    onError: (DioException e, handler) {
      // 에러 처리 (예: 401 에러 시 토큰 갱신)
      if (e.response?.statusCode == 401) {
        // 토큰 갱신 로직
        return handler.resolve(await retryRequest(e.requestOptions));
      }
      return handler.next(e);
    },
  ),
);
요청 취소

HTTP 패키지: 기본 제공 기능 없음

Dio 패키지: CancelToken을 통한 요청 취소 지원

final CancelToken cancelToken = CancelToken();

void fetchWithCancellation() async {
  try {
    final response = await dio.get(
      'https://api.example.com/data',
      cancelToken: cancelToken,
    );
    print('결과: ${response.data}');
  } catch (e) {
    if (CancelToken.isCancel(e)) {
      print('요청이 취소되었습니다: $e');
    } else {
      print('에러: $e');
    }
  }
}

// 다른 곳에서 요청 취소
void cancelRequest() {
  cancelToken.cancel('사용자에 의해 취소됨');
}
진행 상태 모니터링

HTTP 패키지: 지원하지 않음

Dio 패키지: 다운로드/업로드 진행 상태 모니터링 지원

void downloadFile() async {
  try {
    await dio.download(
      'https://example.com/file.pdf',
      '/path/to/download/file.pdf',
      onReceiveProgress: (received, total) {
        if (total != -1) {
          // 진행률 계산
          final progress = (received / total * 100).toStringAsFixed(0);
          print('다운로드 진행률: $progress%');
        }
      },
    );
    print('다운로드 완료');
  } catch (e) {
    print('다운로드 에러: $e');
  }
}
멀티파트/파일 업로드

HTTP 패키지: 기본 지원하지만 사용이 번거로움

Future<void> uploadFileWithHttp() async {
  final request = http.MultipartRequest(
    'POST',
    Uri.parse('https://api.example.com/upload'),
  );

  request.files.add(await http.MultipartFile.fromPath(
    'file',
    '/path/to/file.jpg',
  ));

  request.fields['description'] = '프로필 이미지';

  final response = await request.send();
  final responseBody = await response.stream.bytesToString();

  if (response.statusCode == 200) {
    print('업로드 성공: $responseBody');
  } else {
    print('업로드 실패: ${response.statusCode}');
  }
}

Dio 패키지: FormData를 통한 간편한 파일 업로드

Future<void> uploadFileWithDio() async {
  final formData = FormData.fromMap({
    'file': await MultipartFile.fromFile(
      '/path/to/file.jpg',
      filename: 'profile.jpg',
    ),
    'description': '프로필 이미지',
  });

  try {
    final response = await dio.post(
      'https://api.example.com/upload',
      data: formData,
      onSendProgress: (sent, total) {
        final progress = (sent / total * 100).toStringAsFixed(0);
        print('업로드 진행률: $progress%');
      },
    );
    print('업로드 성공: ${response.data}');
  } catch (e) {
    print('업로드 에러: $e');
  }
}
타임아웃 설정

HTTP 패키지: 수동 구현 필요

Future<void> fetchWithTimeout() async {
  try {
    final client = http.Client();
    final request = http.Request('GET', Uri.parse('https://api.example.com/data'));

    final response = await client.send(request).timeout(
      Duration(seconds: 10),
      onTimeout: () {
        client.close();
        throw TimeoutException('요청 시간 초과');
      },
    );

    final responseBody = await response.stream.bytesToString();

    if (response.statusCode == 200) {
      print('응답: $responseBody');
    } else {
      print('실패: ${response.statusCode}');
    }

    client.close();
  } catch (e) {
    print('에러: $e');
  }
}

Dio 패키지: 내장 타임아웃 설정

Future<void> fetchWithTimeout() async {
  final dio = Dio(
    BaseOptions(
      connectTimeout: Duration(seconds: 5),
      receiveTimeout: Duration(seconds: 10),
      sendTimeout: Duration(seconds: 10),
    ),
  );

  try {
    final response = await dio.get('https://api.example.com/data');
    print('응답: ${response.data}');
  } catch (e) {
    if (e is DioException && e.type == DioExceptionType.connectionTimeout) {
      print('연결 시간 초과');
    } else if (e is DioException && e.type == DioExceptionType.receiveTimeout) {
      print('수신 시간 초과');
    } else {
      print('에러: $e');
    }
  }
}

4. 성능 및 패키지 크기 비교

항목 HTTP 패키지 Dio 패키지
패키지 크기 작음 (~20KB) 중간 (~100KB)
의존성 최소 여러 내부 의존성
메모리 사용량 적음 상대적으로 더 많음
기능당 코드량 많음 (직접 구현 필요) 적음 (내장 기능 많음)

5. 사용 사례별 선택 가이드

HTTP 패키지가 적합한 경우

  • 간단한 API 호출만 필요한 소규모 프로젝트
  • 앱 크기를 최소화해야 하는 경우
  • 최소한의 의존성을 원하는 경우
  • 간단한 GET/POST 요청만 필요한 경우
  • 커스텀 HTTP 클라이언트를 직접 구축하려는 경우

Dio 패키지가 적합한 경우

  • 대규모 프로젝트나 복잡한 API 통합
  • 인터셉터, 요청 취소, 파일 업로드 등 고급 기능이 필요한 경우
  • 글로벌 설정을 한곳에서 관리하려는 경우
  • 다운로드/업로드 진행 상황을 표시해야 하는 경우
  • 에러 처리와 재시도 전략을 구현해야 하는 경우
  • REST API 클라이언트를 빠르게 구축하려는 경우

6. 실제 프로젝트 구현 비교

HTTP 패키지로 구현한 API 서비스

class ApiService {
  final String baseUrl = 'https://api.example.com';
  final http.Client client = http.Client();

  Future<Map<String, dynamic>> get(String endpoint) async {
    try {
      final response = await client.get(
        Uri.parse('$baseUrl/$endpoint'),
        headers: await _getHeaders(),
      ).timeout(Duration(seconds: 10));

      return _handleResponse(response);
    } on TimeoutException {
      throw ApiException('요청 시간이 초과되었습니다.');
    } on SocketException {
      throw ApiException('네트워크 연결에 문제가 있습니다.');
    } catch (e) {
      throw ApiException('알 수 없는 오류: $e');
    }
  }

  Future<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> data) async {
    try {
      final response = await client.post(
        Uri.parse('$baseUrl/$endpoint'),
        headers: await _getHeaders(),
        body: json.encode(data),
      ).timeout(Duration(seconds: 10));

      return _handleResponse(response);
    } on TimeoutException {
      throw ApiException('요청 시간이 초과되었습니다.');
    } on SocketException {
      throw ApiException('네트워크 연결에 문제가 있습니다.');
    } catch (e) {
      throw ApiException('알 수 없는 오류: $e');
    }
  }

  Future<Map<String, String>> _getHeaders() async {
    // 인증 토큰 가져오기
    final token = await TokenStorage.getToken();
    return {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      if (token != null) 'Authorization': 'Bearer $token',
    };
  }

  Map<String, dynamic> _handleResponse(http.Response response) {
    if (response.statusCode >= 200 && response.statusCode < 300) {
      if (response.body.isEmpty) return {};
      return json.decode(response.body);
    } else if (response.statusCode == 401) {
      throw UnauthorizedException('인증에 실패했습니다.');
    } else if (response.statusCode == 404) {
      throw NotFoundException('요청한 리소스를 찾을 수 없습니다.');
    } else {
      throw ApiException(
        '서버 오류 (${response.statusCode}): ${response.body}',
        statusCode: response.statusCode,
      );
    }
  }

  void dispose() {
    client.close();
  }
}

class ApiException implements Exception {
  final String message;
  final int? statusCode;

  ApiException(this.message, {this.statusCode});

  @override
  String toString() => message;
}

class UnauthorizedException extends ApiException {
  UnauthorizedException(String message) : super(message, statusCode: 401);
}

class NotFoundException extends ApiException {
  NotFoundException(String message) : super(message, statusCode: 404);
}

Dio 패키지로 구현한 API 서비스

class ApiService {
  late Dio _dio;
  final String baseUrl = 'https://api.example.com';

  ApiService() {
    _dio = Dio(
      BaseOptions(
        baseUrl: baseUrl,
        connectTimeout: Duration(seconds: 5),
        receiveTimeout: Duration(seconds: 10),
        contentType: 'application/json',
        responseType: ResponseType.json,
      ),
    );

    // 인터셉터 설정
    _dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) async {
          // 인증 토큰 추가
          final token = await TokenStorage.getToken();
          if (token != null) {
            options.headers['Authorization'] = 'Bearer $token';
          }
          return handler.next(options);
        },
        onError: (DioException e, handler) async {
          // 401 에러 처리 (토큰 갱신 등)
          if (e.response?.statusCode == 401) {
            try {
              await refreshToken();
              // 원래 요청 재시도
              return handler.resolve(await _dio.fetch(e.requestOptions));
            } catch (refreshError) {
              // 토큰 갱신 실패 처리
              return handler.reject(e);
            }
          }
          return handler.next(e);
        },
      ),
    );

    // 로깅 인터셉터 (디버그 모드에서만)
    if (kDebugMode) {
      _dio.interceptors.add(LogInterceptor(
        request: true,
        requestHeader: true,
        requestBody: true,
        responseHeader: true,
        responseBody: true,
        error: true,
      ));
    }
  }

  Future<T> get<T>(String endpoint) async {
    try {
      final response = await _dio.get<T>(endpoint);
      return response.data as T;
    } on DioException catch (e) {
      throw _handleDioException(e);
    } catch (e) {
      throw ApiException('알 수 없는 오류: $e');
    }
  }

  Future<T> post<T>(String endpoint, dynamic data) async {
    try {
      final response = await _dio.post<T>(
        endpoint,
        data: data,
      );
      return response.data as T;
    } on DioException catch (e) {
      throw _handleDioException(e);
    } catch (e) {
      throw ApiException('알 수 없는 오류: $e');
    }
  }

  ApiException _handleDioException(DioException e) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        return ApiException('요청 시간이 초과되었습니다.', statusCode: e.response?.statusCode);

      case DioExceptionType.badCertificate:
        return ApiException('보안 인증서에 문제가 있습니다.', statusCode: e.response?.statusCode);

      case DioExceptionType.badResponse:
        if (e.response?.statusCode == 401) {
          return UnauthorizedException('인증에 실패했습니다.');
        } else if (e.response?.statusCode == 404) {
          return NotFoundException('요청한 리소스를 찾을 수 없습니다.');
        }
        return ApiException(
          '서버 오류 (${e.response?.statusCode}): ${e.response?.data}',
          statusCode: e.response?.statusCode,
        );

      case DioExceptionType.cancel:
        return ApiException('요청이 취소되었습니다.');

      case DioExceptionType.connectionError:
        return ApiException('네트워크 연결에 문제가 있습니다.');

      default:
        return ApiException('요청 처리 중 오류가 발생했습니다: ${e.message}');
    }
  }

  Future<void> refreshToken() async {
    // 토큰 갱신 로직
  }

  void dispose() {
    _dio.close();
  }
}

7. 사용 추세와 커뮤니티 지원

HTTP 패키지

  • Flutter 팀의 공식 지원
  • 안정적이고 지속적인 유지 관리
  • 단순함을 선호하는 개발자들에게 인기
  • pub.dev에서 높은 인기도 (2023년 기준 주간 다운로드 100만 이상)

Dio 패키지

  • 활발한 커뮤니티 개발
  • 기능 추가 요청에 대한 빠른 대응
  • 복잡한 API 통합이 필요한 대형 프로젝트에서 선호
  • pub.dev에서 높은 인기도 (2023년 기준 주간 다운로드 50만 이상)

결론

HTTP 패키지와 Dio 패키지는 각각의 장점이 있으며, 프로젝트의 요구사항에 따라 선택해야 합니다.

HTTP 패키지는 간단한 HTTP 요청이 필요한 소규모 프로젝트에 적합합니다. 기본적인 기능만 제공하지만, 가볍고 의존성이 적으며 Flutter 팀에서 공식적으로 지원합니다.

Dio 패키지는 인터셉터, 요청 취소, 진행 상태 모니터링, 파일 업로드, 자동 JSON 변환 등 다양한 고급 기능을 제공합니다. 복잡한 API 통합이 필요한 대규모 프로젝트에 더 적합합니다.

결국 선택은 프로젝트의 복잡성, 기능 요구사항, 팀의 경험에 따라 달라질 수 있습니다. 간단한 요청만 필요하다면 HTTP 패키지로 시작하고, 나중에 필요에 따라 Dio로 마이그레이션할 수도 있습니다.

results matching ""

    No results matching ""