Axios로 FormData 전송 시 Android NetworkError (IOS는 정상 동작)

Ollin·2025년 9월 22일

React Native

목록 보기
9/10

🧩 문제 발생

Android NetworkError

AxiosError: Network Error - 안드로이드에서 FormData를 사용한 이미지 업로드 시 네트워크 오류 발생

  • FormData를 사용해 이미지를 업로드하려 했으나 안드로이드에서만 네트워크 오류 발생
  • iOS에서는 정상적으로 작동

여러 가지 해결 방법 시도:

  • as unknown as Blob 타입 캐스팅
  • file:// 접두사 처리
  • Content-Type 헤더 수동 설정
  • FormData 구성 방식 변경

🔍 실패한 시도

문제의 원인을 파악하기 위해 테스트를 진행

서버 및 로직 확인: Swagger/Postman과 iOS 시뮬레이터에서는 동일한 로직으로 업로드에 성공
-> 이를 통해 백엔드 API와 기본적인 FormData 구성 로직 자체에는 문제가 없음을 확인

클라이언트 측 시도: 안드로이드 환경에서 아래와 같은 여러 방법을 시도했으나 모두 동일한 Network Error를 반환했습니다.

  • as unknown as Blob 타입 캐스팅
  • Content-Type 헤더 직접 설정 및 제거
  • FormData에 담는 파일 객체의 구조 변경

이 과정들을 통해, 문제는 코드의 명시적인 버그가 아닌 안드로이드 환경과 axios 라이브러리가 FormData를 처리하는 방식의 차이 때문에 발생한다고 결론


💡 문제 해결 과정

  • FormData 구성 방식 변경 시도
formData.append('file', {
  uri: fileUri,
  type: 'image/jpeg',
  name: fileName,
} as unknown as Blob);
  • Content-Type 헤더 수동 설정 시도
headers: {
  'Content-Type': 'multipart/form-data'
}
  • file:// 접두사 처리 시도
const fileUri = Platform.OS === 'android' 
  ? uri.replace('file://', '') 
  : uri;

▶️ 문제 원인

  1. FormData 변환 문제:
    • axios가 FormData를 자동으로 문자열화하려고 시도
    • Android에서 이 변환이 문제를 일으킴
  2. Content-Type 헤더 문제:
    • 수동으로 'multipart/form-data' 설정 시 boundary 정보 누락
    • axios가 자동으로 설정해야 하는 부분을 수동으로 처리하려 함
  3. 파일 객체 구조 문제:
    • 복잡한 객체 구조가 Android에서 처리되지 않음
    • 불필요한 타입 캐스팅으로 인한 문제

✅ 해결 방안

  1. FormData 구성 단순화
const formData = new FormData();

formData.append('file', {
  uri: image.uri,
  type: image.type || 'image/jpeg',      // 정확한 MIME 타입 명시
  name: image.fileName || 'upload.jpg',
});
  1. Axios 설정 최적화 (transformRequest 사용)
    → transformRequest 옵션을 사용하여 axios가 FormData를 변환하지 않고 그대로 보내도록 설정
    → headers의 content-type은 axios가 boundary를 올바르게 추가하도록 설정
// 공통 API 요청 함수 예시
const uploadImageApi = (formData: FormData) => {
  return apiClient.post('/file/upload', formData, {
    // 1. FormData를 그대로 반환하여 axios의 자동 변환을 막음
    transformRequest: (data, headers) => {
      return data;
    },
    // 2. headers에 content-type을 명시하되, boundary는 axios가 자동 생성
    headers: {
      'Content-Type': 'multipart/form-data',
    },
  });
};

최종 코드

재사용 가능한 커스텀 훅
→ React Query(@tanstack/react-query)와 결합하여 재사용 가능한 파일 업로드 훅

// useImageUpload.ts
import { useMutation } from '@tanstack/react-query';
import { apiClient } from '@/shared/api'; // 설정된 axios 인스턴스
import { PHOT_BASE_URL } from '@/shared/api/model';

// API 요청 함수
const uploadImageApi = async (formData: FormData): Promise<string> => {
  const response = await apiClient.post<string>('/file/upload', formData, {
    transformRequest: (data) => data,
    headers: { 'Content-Type': 'multipart/form-data' },
  });
  return response.data;
};

// 파일 업로드를 위한 커스텀 훅
export const useImageUpload = () => {
  const mutation = useMutation({
    mutationFn: uploadImageApi,
  });

  const uploadImage = async (uri: string, metadata?: TImageMetadata): Promise<string> => {
    // 이미 서버 URL인 경우(수정하지 않은 이미지) 그대로 반환
    if (!uri.startsWith('file://') && !uri.startsWith('content://')) {
      return uri;
    }

    const formData = new FormData();
    formData.append('file', {
      uri,
      type: metadata?.type ?? 'image/jpeg',
      name: metadata?.fileName ?? 'image.jpg',
    });

    // React Query의 mutateAsync를 사용해 업로드 실행 및 결과 반환
    const resultFromServer = await mutation.mutateAsync(formData);
    return PHOT_BASE_URL + resultFromServer;
  };

  return { uploadImage, isUploading: mutation.isPending };
};

참고한 글

0개의 댓글