👩🏻‍💻 프로젝트/📌 독서 관리 어플

[React Native] 갤러리 이미지 업로드 구현하기

kssossok 2026. 3. 3. 08:57

갤러리 이미지 불러오기 결과

 

 

1. 웹과 React Native의 가장 큰 차이

웹에서는 브라우저가 `<input type="file" />`을 통해 파일을 선택하면, 파일 이름과 MIME 타입, 실제 데이터까지 모든 게 포함되어 있는 File 객체를 만들어준다.

이 File 객체는 브라우저가 만들어주는 파일을 표현하는 자바스크립트 객체로, 파일 그 자체를 JS에서 다룰 수 있도록 감싸놓은 객체라 보면 된다.

조금 더 정확히는, Blob을 상속한 객체로, 아래와 같은 정보를 포함하고 있다.

  • 파일의 실제 바이너리 데이터
  • 파일 이름
  • 파일 타입
  • 파일 크기
  • 마지막 수정 시간

그래서 우리는 단순히 그 File을 FormData에 넣어서 전송하기만 하면 되는 것이다.

 

blob이란

Binary Large Object의 약자로, 이미지나 오디오, 비디오 등 대용량의 이진(Binary) 데이터를 하나의 불변 덩어리 객체로 취급하는 데이터 형상을 의미한다.

 

 

그러나 React Native는 앱 위에서 동작하는 JS 런타임이기 때문에 브라우저처럼 File 객체가 존재하지는 않는다.

그래서 React Native에서 이미지를 선택한다면, 우리는 대략 아래와 같은 구조를 받게 된다.

  • uri (기기 내부의 파일 경로)
  • fileName
  • mimeType
  • width, height 등 메타 정보

즉, 우리는 파일 그 자체(바이너리 데이터)가 아닌, 파일이 저장된 위치 정보(uri)를 받는 것이다.

그리고 이러한 차이점 때문에 업로드 코드가 웹과는 조금 다르게 만들어진다.

 

 


 

 

 

2. 우리 팀의 명세서

 

(우리 팀의 프로필 사진 업로드 api 명세서는 위와 같고, 나는 이를 기반으로 구현했음을 참고해서 글을 보면 좋을 것 같다.)

 

위 사진에서 "imageFile"이라는 필드명이 매우 중요한데, 각자 명세서에 맞게 필드명을 잘 맞춰서 활용하면 된다.

왜냐하면 multipart/form-data 요청은 여러 개의 파트로 구성되며, 각 파트는 name이라는 키를 갖게 되는데, 서버는 이 name을 기준으로 어떤 데이터를 어떤 변수에 매핑할지 결정하기 때문이다.

즉, 우리 팀의 경우 "imageFile"이라는 이름을 가진 파트를 찾기 때문에 반드시 프론트에서 그 이름으로 파일을 전송해주어야 하는 것이다.

 

 


 

 

3. FormData를 사용하는 이유

이미지는 바이너리(Binary) 데이터이다.
그러나 JSON은 기본적으로 문자열 기반의 데이터 포맷이기 때문에, 이미지 파일처럼 대용량의 이진 데이터를 그대로 담을 수 없다.

 

물론 이론적으로는 파일을 Base64로 인코딩해서 JSON에 포함시킬 수도 있지만, 이 방식은 파일 크기가 증가하고, 메모리 사용량도 커지며, 네트워크 효율도 떨어지기 때문에 일반적인 파일 업로드에서는 사용하지 않는다.

 

 

따라서 HTTP에서는 파일 업로드를 위한 전용 포맷인 multipart/form-data를 제공해준다.

이 포맷은 하나의 HTTP 요청 안에 여러 개의 파트를 구성할 수 있도록 설계되어 있는데, 각 파트는 독립적인 헤더와 본문을 가지며, 텍스트 데이터와 파일 데이터를 동시에 포함할 수 있다.

 

예를 들어 하나의 요청 안에 다음과 같은 것들을 모두 넣을 수 있는 것이다.

  • nickname (텍스트)
  • imageFile (파일)
  • public (boolean 값)

 

 

그리고 여기서 등장하는 게 바로 FormData 객체이다.

constage formData = new FormData();

 

이 코드 한 줄은 이제 multipart 요청 본문을 구성하겠다는 의미를 담고 있다.

 

FormData는 내부적으로 key-value 구조를 가지는데, 각 key는 multipart의 name이 되고, 각 value는 해당 파트의 본문 데이터가 된다.

따라서 우리는 multipath 요청 안에 텍스트 파트 하나를 추가하려면, 아래와 같이 추가해주어야 하는 것이다.

formData.append("nickname", "ssosso");

 

만약 파일 파트를 추가하려면, 아래와 같은 정보들을 넣어주면 된다.

formData.append("imageFile", {
  uri: asset.uri,
  name,
  type: mime,
});

 

 

어쨌든 결론은, FormData는 multipart/form-data 요청의 본문을 구성하기 위한 빌더 역할을 한다는 것이다.

 

 


 

 

4. React Native에서 파일을 multipart로 만드는 방식

그럼 이제 오늘 목표인 갤러리에서 이미지를 선택하는 걸 구현하기 위해서, 파일을 어떻게 multipart로 만들면 되는지 조금 더 자세히 알아보자.

formData.append("imageFile", {
  uri: asset.uri,
  name,
  type: mime,
});

 

여기서 "imageFile"은 각자 명세서에 맞는 필드명으로 꼭 !! 바꾸어서 사용해주어야 한다.

 

위 코드에서 각 값의 의미는 다음과 같다.

  • uri: 실제 파일이 저장된 기기 내부 경로
  • name: 서버로 전송될 파일 이름
  • type: MIME 타입 (image/jpeg 등)

axios는 이 정보를 기반으로 네이티브 파일을 읽어서 multipart 파트로 변환하게 된다.

 

즉, 이 코드는 "이 경로에 있는 파일을 읽어서 imageFile이라는 이름으로 전송해줘 !" 라고 axios에게 지시하는 코드라 보면 된다.

 

 


 

 

5. 실제 구현 코드

5.1. 업로드 API

// 프로필 사진 업로드
export async function uploadProfileImage(asset) {
  const formData = new FormData();

  const mime = asset?.mimeType || "image/jpeg";
  const name = asset?.fileName || "profile.jpg";

  formData.append("imageFile", {
    uri: asset.uri,
    name,
    type: mime,
  });

  const res = await client.post("/api/user/profile/image", formData, {
    headers: {
      Accept: "application/json",
    },
  });

  return res.data;
}

 

 

5.2. 이미지 선택

위 업로드 함수를 실제 화면에서는 아래와 같이 적용해주었다.

// 갤러이에서 이미지 선택
  const pickImage = async () => {
    // 갤러리 권한 요청
    const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
    if (status !== "granted") return null;

    const imageType = ImagePicker.MediaType?.Images;

    // 갤러리 열기
    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: imageType,
      allowsEditing: true,
      aspect: [1, 1], // 가로 세로 비율 지정
      quality: 0.8,   // 압축 품질 지정
    });

    if (result.canceled) return null;
    return result.assets[0];
  };

 

이 코드를 통해 사용자가 이미지를 선택하면 우리는 asset을 얻게 된다.

 

 

5.3. 업로드 처리

  // 프로필 이미지 변경 처리
  const onPressChangePhoto = async () => {
    try {
      if (uploading) return; // 중복 방지
      setUploading(true);

      const asset = await pickImage();
      if (!asset) return;

      await uploadProfileImage(asset);

      // 업로드 후 서버 데이터로 동기화
      try {
        const refreshed = await fetchUserProfile();

        setProfileImageUrl(refreshed.profileImageUrl);
        setNickname(refreshed.nickname ?? nickname);

        setIsPublic(refreshed.public);
      } catch (err) {
        console.error("프로필 재조회 실패:", err);
      }

      setSheetVisible(false);
    } catch (e) {
      console.error("프로필 업로드 실패:", e);
    } finally {
      setUploading(false);
    }
  };

 

 


 

 

6. 전체 흐름 총정리

  1. 사용자가 이미지를 선택한다.
  2. React Native가 uri를 반환한다.
  3. FormData에 { uri, name, type }으로 파일 파트를 구성해준다.
  4. axios가 multipart 요청을 만든다.
  5. 서버는 imageFile을 읽어서 S3에 업로드한다.
  6. 서버가 저장된 URL을 응답해준다.
  7. 프론트에서 다시 프로필을 조회해서 최신 상태로 동기화한다.
  8. UI를 갱신한다.
반응형