[Flutter] http 패키지의 MultipartRequest로 파일 업로드하기

2024-10-06 hit count image

Flutter에서 http 패키지의 MultipartRequest를 사용하여 파일을 업로드하는 방법과 이를 테스트하는 방법에 대해서 알아보겠습니다.

개요

Flutter에서 http 패키지를 사용하여 파일을 업로드해야할 때가 있습니다. 이때, http 패키지의 MultipartRequest를 사용하면 파일을 업로드할 수 있습니다. 이번 블로그 포스트에서는 MultipartRequest를 사용하여 파일을 업로드하는 방법과 이를 테스트하는 방법에 대해서 알아보겠습니다.

MultipartRequest로 파일 업로드하기

다음은 실제로 http 패키지의 MultipartRequest를 사용하여 파일을 업로드하는 예제입니다.

class ExampleAPI {
  final String token;
  final http.MultipartRequest httpClient;

  ExampleAPI({
    required this.token,
    @visibleForTesting http.MultipartRequest? mockClient,
  }) : httpClient = mockClient ??
            http.MultipartRequest(
              "POST",
              Uri.parse('${ENV.apiServer}/api/app'),
            );

  Future<InfoData> sendData({
    required DateTime date,
    required int status,
    required int docType,
    required String note,
    required String passportImage,
    String? certificationImage,
    List<String>? receiptImages,
  }) async {
    Map<String, String> data = {};
    data['date'] = dateFormatForSearch(date);
    data['status'] = '$status';
    data['doc_type'] = '$docType';
    data['note'] = note;

    List<MultipartFile> files = [];
    files.add(await http.MultipartFile.fromPath('passport_image', passportImage));
    if (jpnCertImg != null && jpnCertImg != '') {
      files.add(await http.MultipartFile.fromPath('cert_img', certificationImage));
    }
    if (receiptImages?.isNotEmpty == true) {
      for (var image in receiptImages!) {
        if (image == '') continue;
        files.add(await http.MultipartFile.fromPath('receipt_img[]', image));
      }
    }

    httpClient.headers.addAll({'Authorization': 'Bearer $token'});
    httpClient.fields.addAll(data);
    httpClient.files.addAll(files);

    final stream = await httpClient.send();
    return http.Response.fromStream(stream).then((response) {
      final data = jsonDecode(utf8.decode(response.bodyBytes));

      if (data['success'] == true) {
        return InfoData.fromJson(data['data']);
      } else {
        throw Exception('Unknown response');
      }
    });
  }
}

http 패키지의 MultipartRequest를 사용하여 파일을 업로드하는 부분을 좀 더 자세히 살펴보겠습니다.

ExampleAPI({
  required this.token,
  @visibleForTesting http.MultipartRequest? mockClient,
}) : httpClient = mockClient ??
          http.MultipartRequest(
            "POST",
            Uri.parse('${ENV.apiServer}/api/app'),
          );

MultipartRequst로 파일을 전송하려면 우선, http.MultipartRequest의 인스턴스를 생성해야 합니다.

Map<String, String> data = {};
data['date'] = dateFormatForSearch(date);
data['status'] = '$status';
data['doc_type'] = '$docType';
data['note'] = note;

MultipartRequest는 파일 이외에 다른 정보도 함께 보낼 수 있습니다. 그래서 함께 보낼 다른 정보를 준비하였습니다.

List<MultipartFile> files = [];
files.add(await http.MultipartFile.fromPath('passport_image', passportImage));
if (jpnCertImg != null && jpnCertImg != '') {
  files.add(await http.MultipartFile.fromPath('cert_img', certificationImage));
}
if (receiptImages?.isNotEmpty == true) {
  for (var image in receiptImages!) {
    if (image == '') continue;
    files.add(await http.MultipartFile.fromPath('receipt_img[]', image));
  }
}

파일을 설정하는 함수는 업로드하고자 하는 파일의 경로를 전달받습니다. 이렇게 전달받은 파일의 경로를 http.MultipartFile.fromPath를 사용하여 MultipartFile로 변환합니다.

여러 파일을 업로드할 때에는 receipt_img[]와 같이 []를 사용하여 배열로 전달할 수 있습니다.

httpClient.headers.addAll({'Authorization': 'Bearer $token'});
httpClient.fields.addAll(data);
httpClient.files.addAll(files);

이제 파일을 업로드하기 위해 Bearer 토큰을 헤더에 설정하고, 준비한 데이터를 fields에 추가합니다. 준비한 파일은 files에 추가합니다.

final stream = await httpClient.send();
return http.Response.fromStream(stream).then((response) {
  final data = jsonDecode(utf8.decode(response.bodyBytes));

  if (data['success'] == true) {
    return InfoData.fromJson(data['data']);
  } else {
    throw Exception('Unknown response');
  }
});

마지막으로 데이터를 전송하고 응답을 받습니다. 이렇게 받은 응답을 자신에 앱에 맞게 처리하면 됩니다.

테스트 코드

http 패키지의 MultipartRequest를 사용하여 파일을 업로드하는 함수를 테스트하는 방법에 대해서 알아보겠습니다.

우선 전체 코드는 다음과 같습니다.

...
@GenerateMocks([http.MultipartRequest])
void main() {
  final mockHttpMultipartRequest = CustomMockMultipartRequest();
  setUp(() {
    mockHttpMultipartRequest.headers.clear();
    mockHttpMultipartRequest.fields.clear();
    mockHttpMultipartRequest.files.clear();
    when(mockHttpMultipartRequest.send()).thenAnswer(
      (_) async {
        final responseBody = jsonEncode({
          'success': true,
          'data': {
            'amount': 1000,
            'commission': 3000,
          }
        });
        final stream = Stream.value(utf8.encode(responseBody));
        return http.StreamedResponse(stream, 200);
      },
    );
  });

  test('Success', () async {
    final result = await TaxRefundAPI(
      token: 'test_token',
      client: mockHttpMultipartRequest,
    ).sendData(
      permitDate: DateTime.parse('2022-01-01 01:04'),
      status: 91,
      docType: 2,
      note: '',
      passportImg: 'assets/images/passport_sample.jpg',
      certificationImage: 'assets/images/placeholder.png',
      receiptImages: [
        'assets/images/sample_receipt.png',
        'assets/images/sample_receipt.png',
        'assets/images/sample_receipt.png',
      ],
    );

    // Request parameters
    expect(
      mockHttpMultipartRequest.headers,
      {'Authorization': 'Bearer test_token'},
    );
    expect(
      mockHttpMultipartRequest.fields,
      {
        'date': '2022-01-01',
        'status': '91',
        'doc_type': '2',
        'note': '',
      },
    );
    expect(mockHttpMultipartRequest.files.length, 5);
    expect(mockHttpMultipartRequest.files[0].field, 'passport_image');
    expect(mockHttpMultipartRequest.files[0].filename, 'passport_sample.jpg');
    expect(mockHttpMultipartRequest.files[1].field, 'cert_img');
    expect(mockHttpMultipartRequest.files[1].filename, 'placeholder.png');
    expect(mockHttpMultipartRequest.files[2].field, 'receipt_img[]');
    expect(
      mockHttpMultipartRequest.files[2].filename,
      'sample_receipt.png',
    );
    expect(mockHttpMultipartRequest.files[3].field, 'receipt_img[]');
    expect(
      mockHttpMultipartRequest.files[3].filename,
      'sample_receipt.png',
    );
    expect(mockHttpMultipartRequest.files[4].field, 'receipt_img[]');
    expect(
      mockHttpMultipartRequest.files[4].filename,
      'sample_receipt.png',
    );

    // Response
    expect(result, isA<InfoData>());
    expect(result.toMap(), {
      'amount': 1000,
      'commission': 3000,
    });
  });

  test('Throw error when response is failed', () async {
    when(mockHttpMultipartRequest.send()).thenAnswer(
      (_) async {
        final responseBody = jsonEncode({
          'success': false,
        });
        final stream = Stream.value(utf8.encode(responseBody));
        return http.StreamedResponse(stream, 200);
      },
    );

    try {
      await TaxRefundAPI(
        token: 'test_token',
        client: mockHttpMultipartRequest,
      ).sendData(
        permitDate: DateTime.parse('2022-01-01 01:04'),
        status: 91,
        docType: 2,
        note: '',
        passportImg: 'assets/images/passport_sample.jpg',
        certificationImage: 'assets/images/placeholder.png',
        receiptImages: [
          'assets/images/sample_receipt.png',
          'assets/images/sample_receipt.png',
          'assets/images/sample_receipt.png',
        ],
      );
    } catch (e) {
      expect(e.toString(), 'Exception: Unknown response');
    }
  });
}

class CustomMockMultipartRequest extends MockMultipartRequest {
  @override
  final Map<String, String> headers = {};

  @override
  final Map<String, String> fields = {};

  @override
  final List<MultipartFile> files = [];
}

좀 더 자세히 살펴보도록 하겠습니다.

...
@GenerateMocks([http.MultipartRequest])
void main() {
  final mockHttpMultipartRequest = CustomMockMultipartRequest();
  setUp(() {
    mockHttpMultipartRequest.headers.clear();
    mockHttpMultipartRequest.fields.clear();
    mockHttpMultipartRequest.files.clear();
    when(mockHttpMultipartRequest.send()).thenAnswer(
      (_) async {
        final responseBody = jsonEncode({
          'success': true,
          'data': {
            'amount': 1000,
            'commission': 3000,
          }
        });
        final stream = Stream.value(utf8.encode(responseBody));
        return http.StreamedResponse(stream, 200);
      },
    );
  });
  ...
}

class CustomMockMultipartRequest extends MockMultipartRequest {
  @override
  final Map<String, String> headers = {};

  @override
  final Map<String, String> fields = {};

  @override
  final List<MultipartFile> files = [];
}

DI(Dependency Injection)를 사용하고 있기 때문에, 테스트를 위한 Mock 객체를 생성하고, setUp 함수를 사용하여 Mock 객체를 초기화했습니다.

...
@GenerateMocks([http.MultipartRequest])
void main() {
  final mockHttpMultipartRequest = CustomMockMultipartRequest();
  ...
  test('Success', () async {
    final result = await TaxRefundAPI(
      token: 'test_token',
      client: mockHttpMultipartRequest,
    ).sendData(
      permitDate: DateTime.parse('2022-01-01 01:04'),
      status: 91,
      docType: 2,
      note: '',
      passportImg: 'assets/images/passport_sample.jpg',
      certificationImage: 'assets/images/placeholder.png',
      receiptImages: [
        'assets/images/sample_receipt.png',
        'assets/images/sample_receipt.png',
        'assets/images/sample_receipt.png',
      ],
    );

    // Request parameters
    expect(
      mockHttpMultipartRequest.headers,
      {'Authorization': 'Bearer test_token'},
    );
    expect(
      mockHttpMultipartRequest.fields,
      {
        'date': '2022-01-01',
        'status': '91',
        'doc_type': '2',
        'note': '',
      },
    );
    expect(mockHttpMultipartRequest.files.length, 5);
    expect(mockHttpMultipartRequest.files[0].field, 'passport_image');
    expect(mockHttpMultipartRequest.files[0].filename, 'passport_sample.jpg');
    expect(mockHttpMultipartRequest.files[1].field, 'cert_img');
    expect(mockHttpMultipartRequest.files[1].filename, 'placeholder.png');
    expect(mockHttpMultipartRequest.files[2].field, 'receipt_img[]');
    expect(
      mockHttpMultipartRequest.files[2].filename,
      'sample_receipt.png',
    );
    expect(mockHttpMultipartRequest.files[3].field, 'receipt_img[]');
    expect(
      mockHttpMultipartRequest.files[3].filename,
      'sample_receipt.png',
    );
    expect(mockHttpMultipartRequest.files[4].field, 'receipt_img[]');
    expect(
      mockHttpMultipartRequest.files[4].filename,
      'sample_receipt.png',
    );

    // Response
    expect(result, isA<InfoData>());
    expect(result.toMap(), {
      'amount': 1000,
      'commission': 3000,
    });
  });
  ...
}
...

앞서 생선한 Mock 객체를 사용하여 보낸 요청이 성공적으로 처리된 경우를 테스트하였습니다.

...
@GenerateMocks([http.MultipartRequest])
void main() {
  final mockHttpMultipartRequest = CustomMockMultipartRequest();
  ...
  test('Throw error when response is failed', () async {
    when(mockHttpMultipartRequest.send()).thenAnswer(
      (_) async {
        final responseBody = jsonEncode({
          'success': false,
        });
        final stream = Stream.value(utf8.encode(responseBody));
        return http.StreamedResponse(stream, 200);
      },
    );

    try {
      await TaxRefundAPI(
        token: 'test_token',
        client: mockHttpMultipartRequest,
      ).sendData(
        permitDate: DateTime.parse('2022-01-01 01:04'),
        status: 91,
        docType: 2,
        note: '',
        passportImg: 'assets/images/passport_sample.jpg',
        certificationImage: 'assets/images/placeholder.png',
        receiptImages: [
          'assets/images/sample_receipt.png',
          'assets/images/sample_receipt.png',
          'assets/images/sample_receipt.png',
        ],
      );
    } catch (e) {
      expect(e.toString(), 'Exception: Unknown response');
    }
  });
}
...

마지막으로, 응답이 실패한 경우를 테스트하였습니다.

완료

이것으로 Flutter에서 http 패키지의 MultipartRequest를 사용하여 파일을 업로드하는 방법과 이를 테스트하는 방법에 대해서 알아보았습니다.

Flutter에서 파일 업로드 기능을 구현하신다면, 이 블로그를 참고하여 구현하고, 테스트 코드를 작성해 보시기 바랍니다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

블로그를 운영하면서 좋은 기회가 생겨 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.

스무디 한 잔 마시며 끝내는 React Native, 비제이퍼블릭
스무디 한 잔 마시며 끝내는 리액트 + TDD, 비제이퍼블릭
[심통]현장에서 바로 써먹는 리액트 with 타입스크립트 : 리액트와 스토리북으로 배우는 컴포넌트 주도 개발, 심통
Posts