072DATA

다중으로 이미지 업로드 구현하기 ( supabase Storage ) 본문

FrontEnd/Next.js

다중으로 이미지 업로드 구현하기 ( supabase Storage )

0720 2024. 10. 29. 01:06

개발을 하다 보면 이미지 업로드 기능이 필요한 경우가 많은데,

특히 여러 장의 이미지를 한 번에 처리해야 하는 경우가 있습니다.

오늘은 제가 구현했던 다중 이미지 업로드 기능을 기록해보려고 합니다.

 

구현 목표

  • 최대 5장까지 이미지 업로드 가능
  • 이미지 형식 및 크기 제한 (JPG, PNG, GIF, WEBP / 5MB 이하)
  • 이미지 미리보기 기능
  • 개별 이미지 삭제 기능

 

1. 이미지 업로드 컴포넌트 만들기

먼저 이미지 업로드를 위한 기본적인 UI를 구성했고

이 코드에서 ImageUpload 컴포넌트는 다섯 개의 이미지 미리보기 자리를 제공하는 간단한 레이아웃을 만들어유

const ImageUpload = () => {
  // 파일 input 참조를 위한 ref
  const fileInputRef = useRef<HTMLInputElement | null>(null);
  
  // 5개의 미리보기 영역을 위한 배열
  const previewPlaceholders = Array(5).fill(null);

  return (
    <div className="grid grid-cols-5 gap-4">
      {previewPlaceholders.map((_, index) => (
        <div key={index} className="aspect-square bg-gray-300 rounded-lg">
          <span>이미지 추가</span>
        </div>
      ))}
    </div>
  );
};

.

 

  • fileInputRef: 파일 입력 요소에 접근할 수 있게 useRef를 사용하여 생성하고
    사용자가 파일을 선택할 때 클릭 이벤트를 직접 제어할 수 있도록 해유
  • 미리보기 자리 배열: Array(5).fill(null)로 다섯 개의 미리보기 영역을 담아줄겁니닷

 

 

2. 이미지 상태 관리

이미지 관리를 위해 두 가지 상태가 필요했어유

  1. 실제 파일 객체 배열 (업로드용)
  2. 미리보기 URL 배열 (화면 표시용)

또 코드에서는 handleImageChange 함수를 통해 사용자가 업로드한

이미지를 미리볼 수 있게 하고 파일을 배열로 관리했어유

const [previews, setPreviews] = useState<string[]>([]);
const [imageFiles, setImageFiles] = useState<File[]>([]);

const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files?.[0];
  if (!file) return;

  // URL.createObjectURL을 사용해 미리보기 URL 생성
  const previewUrl = URL.createObjectURL(file);
  setPreviews(prev => [...prev, previewUrl]);
  setImageFiles(prev => [...prev, file]);

  // 같은 파일을 다시 선택할 수 있도록 초기화
  e.target.value = '';
};

 

 

  • 상태 변수 선언
    • previews: 이미지 미리보기 URL을 저장하는 배열로, <img src={previewUrl} />처럼 렌더링에 사용할 수 있슴두
    • imageFiles: 실제 업로드할 File 객체들을 저장하는 배열로 서버 전송 시 사용됩니두
  • handleImageChange 함수
    • 파일 선택: 사용자가 파일을 선택하면 e.target.files 배열에서 첫 번째 파일만 가져옵니데이
    • 미리보기 URL 생성: URL.createObjectURL(file)로 선택된 파일의 미리보기 URL을 생성하여 previews 배열에 추가혀유
    • 파일 저장: 선택된 파일은 imageFiles 배열에 추가되어 나중에 서버로 전송할 수 있게 준비하구유
    • 파일 선택 초기화: e.target.value = ''로 입력 필드의 값을 초기화하여 같은 파일을 반복해서 선택할 수 있도록혀유

 

3. 이미지 유효성 검사

 

파일 형식과 크기를 체크하는 validation 함수를 만들었는데유

validateImage 함수는 이미지 파일을 업로드할 때 특정 조건을 검사하여 유효성을 확인하는 기능을 해서

이를 통해 허용되지 않은 형식이나 크기가 큰 이미지를 방지합니더

const validateImage = (file: File) => {
  const validTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
  const maxSize = 5 * 1024 * 1024; // 5MB

  if (!validTypes.includes(file.type)) {
    alert("JPG, PNG, GIF, WEBP 형식의 이미지만 업로드 가능합니다.");
    return false;
  }

  if (file.size > maxSize) {
    alert("이미지 크기는 5MB 이하여야 합니다.");
    return false;
  }

  return true;
};

 

 

 

  • 파일 형식 검사:
    • validTypes 배열에 정의된 형식(image/jpeg, image/png, image/gif, image/webp)만 허용해유
    • 파일 형식이 validTypes에 포함되지 않으면 경고 메시지를 보여주고 false를 반환해유
  • 파일 크기 검사:
    • maxSize는 5MB로 설정하고 파일 크기가 이 값을 초과할 경우 경고 메시지를 표시하고 false를 반환합니더!
  • 유효한 파일 반환:
    • 형식과 크기가 모두 조건에 부합하면 true를 반환하여 파일이 유효하다는 것을 알려줘유

 

4. Supabase Storage에 업로드

 

이미지 업로드는 Promise.all을 사용해 병렬로 처리했구여

여러 개의 이미지를 Supabase 스토리지에 업로드하고

각 이미지에 대한 공개 URL을 가져오는 과정을 수행해줘유

const imageUrls = await Promise.all(
  params.images.map(async (file) => {
    // 파일명에 userId와 타임스탬프를 포함시켜 유니크하게 관리
    const fileName = `${params.userId}/${Date.now()}`;
    
    const { data, error } = await supabase.storage
      .from("challenges")
      .upload(fileName, file);

    if (error) throw error;

    // 업로드된 파일의 public URL 가져오기
    const { data: { publicUrl } } = supabase.storage
      .from("challenges")
      .getPublicUrl(fileName);

    return publicUrl;
  })
);

 

 

 

  • 이미지 업로드 반복 처리:
    • params.images 배열에 있는 모든 파일을 Promise.all과 map을 통해 병렬로 처리하여 업로드합니닷
      쉽게 말해서 params.images에 있는 이미지 파일들을 하나씩 map을 사용해 처리할 때
      Promise.all로 감싸서 여러 파일을 동시에 업로드할 수 있게 한 거예요
    • 각 파일에 대해 유니크한 파일명을 생성하기 위해 fileName을 ${params.userId}/${Date.now()} 형식으로 설정하여, userId와 Date.now() 타임스탬프를 조합하여 중복을 방지하여유
  • 파일 업로드:
    • supabase.storage.from("challenges").upload(fileName, file);을 통해 Supabase의 challenges 버킷에 파일을 업로드하구요
    • 업로드가 실패할 경우, error가 발생하여 해당 에러를 throw하고 작업이 중단됩니데이
  • 공개 URL 생성:
    • 업로드된 파일의 공개 URL을 가져오기 위해 supabase.storage.from("challenges").getPublicUrl(fileName);을 호출합니데이!!!!
    • 이 URL은 클라이언트 측에서 이미지를 표시하거나 사용할 수 있는 실제 이미지 링크예유
  • URL 반환:
    • 각 파일에 대한 publicUrl을 배열로 반환하여 최종적으로 imageUrls에 모든 이미지의 URL이 저장돼유

 

5. 이미지 삭제 기능

const handleDeleteImage = (index: number) => {
  // 메모리 누수 방지를 위해 URL 객체 해제
  URL.revokeObjectURL(previews[index]);
  
  // 해당 인덱스의 이미지 제거
  setPreviews(prev => prev.filter((_, i) => i !== index));
  setImageFiles(prev => prev.filter((_, i) => i !== index));
};

 

 

 

URL.createObjectURL(file)을 호출하면 해당 파일의 URL을 메모리에 생성하고, 브라우저에서 이 URL을 통해 파일을 임시로 사용할 수 있게 되는데유 하지만 이 URL을 계속 사용하지 않으면 메모리만 차지하게 되고 이것이 쌓이면 메모리 누수가 발생할 수 있다고 하네유

URL 해제

URL.revokeObjectURL(previews[index])를 호출하면 해당 URL을 메모리에서 해제하여

브라우저 메모리 사용량을 줄이고 불필요한 URL 데이터를 제거할 수 있기 때문에

이미지 미리보기에서 이미지를 삭제할 때마다 URL.revokeObjectURL을 호출하면

메모리를 효율적으로 관리할 수 있는 겁니다요!

 

 

이렇게 다중으로 이미지를 업로드하는 과정을 기록해 보았는데여

앞으로 프로젝트를 더 진행하면서 개선해야할 사항이 있어유

개선할 수 있는 사항들

  1. 드래그 앤 드롭 지원
  2. 이미지 압축 기능 추가
  3. 이미지 순서 변경 기능
  4. 업로드 진행률 표시
  5. 실패한 업로드에 대한 재시도 기능

마치며

다중 이미지 업로드는 생각보다 고려할 점이 많은 기능이어서 시간이 꽤 들었는데여
이 경험을 통해 더 나은 파일 업로드 UX에 대해 고민해볼 수 있었습니다.

다음에는 드래그 앤 드롭이나 이미지 압축 같은 추가 기능을 구현해보고 싶네요! 😊

가능하다면 개선 사항을 모두 구현하고 싶어유..