Supbase를 이용해 게시판에 이미지 업로드 하기

Donghwan Oh·2025년 5월 5일
post-thumbnail

목표

게시글을 작성할 때 이미지를 추가/제거 할 수 있는 이미지 입력 폼을 제작하려고 합니다.

기능 동작 흐름

이미지 업로드 폼 구현을 위해서는 위와 같은 상태와 함수, 그리고 ImageUploadBox 컴포넌트가 필요합니다.

  • postImages : 실제 supabase 스토리지에 업로드되는 File 객체 배열입니다.

  • previewImages : 이미지 입력 폼 UI에 표시되는 미리보기 이미지 url로, 이미지가 base64 형식으로 인코딩된 문자열 배열입니다.

  • hadleImageUpload : 입력 폼에 추가된 이미지 파일을 postImages에 추가하고, FileReader를 통해 base64로 인코딩 해 previewImages에 추가합니다.

  • hanleImageDelete : 입력 폼에서 이미지가 제거되면 postImagespreviewImages에서 해당 이미지를 제거합니다.

  • handleUploadStorage : supabase 스토리지에 postImages 파일들을 업로드한 후, 각 이미지들의 public url배열을 반환합니다. public url 배열은 게시글 db의 image_url 컬럼에 저장됩니다.

게시글 작성 완료 버튼을 클릭했을 때 추가된 이미지들을 supabase 스토리지에 업로드하기 위해 postImages와 previewImages로 상태를 나눴습니다.

이미지 입력 컴포넌트 input의 onChange에 handleImageUpload를 등록해 postImages, previewImages를 변경하고, 전체 폼의 onSubmit에 handleUploadStorage를 등록해 supabase와 연결하였습니다.

코드

handleImageUpload

const handleImageUpload = (e) => {
  const fileList = Array.from(e.target.files);

  for (const file of fileList) {
    if (file.size > 3 * 1024 * 1024) {
      // 3MB를 바이트로 변환
      alert('업로드 할 수 있는 최대 파일의 크기는 3MB 입니다.');
      return;
    }
  }

  if (postImages.length + fileList.length > 3) {
    alert('이미지는 최대 3개까지 업로드할 수 있습니다.');
    return;
  }

  setPostImages((prev) => [...prev, ...fileList]);

  const fileUrlList = [];

  fileList.forEach((file, index) => {
    const reader = new FileReader();
    // 콜백 함수 등록 (read후 실행)
    reader.onload = () => {
      fileUrlList[index] = reader.result;
      // 모든 파일을 다 읽었을 때만 setPreviewImages 실행 (falsy 값 제거)
      if (fileUrlList.filter(Boolean).length === fileList.length) {
        setPreviewImages((prev) => [...prev, ...fileUrlList]);
      }
    };
    reader.readAsDataURL(file);
  });
};

handleImageDelete

// 이미지 제거
const handleImageDelete = (index) => {
  setPostImages((prev) => prev.filter((_, i) => i !== index));
  setPreviewImages((prev) => prev.filter((_, i) => i !== index));
};

handleUploadStorage

// 수퍼베이스 스토리지에 파일 업로드 후 public url 이미지 배열을 반환
const handleUploadStorage = async (files) => {
  const uploadedImageLists = [];

  for (let i = 0; i < files.length; i++) {
    // 파일 이름 생성
    const fileName = `post_${Date.now()}_${i}.${files[i].type.split('/')[1]}`;

    // 스토리지 업로드
    const { data, error } = await supabase.storage
    .from('post-images')
    .upload(fileName, files[i]);

    if (error) {
      console.log('이미지 업로드에 실패하였습니다.');
      return;
    }

    // storage에 담긴 이미지의 publicUrl 가져오기
    const res = supabase.storage.from('post-images').getPublicUrl(data.path);
    uploadedImageLists.push(res.data.publicUrl);
  }
  return uploadedImageLists;
};

// 폼 제출 핸들러
const onSubmit = async (formData) => {
  let uploadedImageUrlList = [];
  if (postImages.length > 0) {
    uploadedImageUrlList = await handleUploadStorage(postImages);
  }
  mutation.mutate({
    studyId,
    loggedInUserId,
    type: boardType,
    title: formData.title,
    content: formData.content,
    imgUrlList: uploadedImageUrlList,
  });
};
profile
왜?에 대해 공부한 것을 기록합니다.

0개의 댓글