

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

이미지 업로드 폼 구현을 위해서는 위와 같은 상태와 함수, 그리고 ImageUploadBox 컴포넌트가 필요합니다.
postImages : 실제 supabase 스토리지에 업로드되는 File 객체 배열입니다.
previewImages : 이미지 입력 폼 UI에 표시되는 미리보기 이미지 url로, 이미지가 base64 형식으로 인코딩된 문자열 배열입니다.
hadleImageUpload : 입력 폼에 추가된 이미지 파일을 postImages에 추가하고, FileReader를 통해 base64로 인코딩 해 previewImages에 추가합니다.
hanleImageDelete : 입력 폼에서 이미지가 제거되면 postImages와 previewImages에서 해당 이미지를 제거합니다.
handleUploadStorage : supabase 스토리지에 postImages 파일들을 업로드한 후, 각 이미지들의 public url배열을 반환합니다. public url 배열은 게시글 db의 image_url 컬럼에 저장됩니다.
게시글 작성 완료 버튼을 클릭했을 때 추가된 이미지들을 supabase 스토리지에 업로드하기 위해 postImages와 previewImages로 상태를 나눴습니다.
이미지 입력 컴포넌트 input의 onChange에 handleImageUpload를 등록해 postImages, previewImages를 변경하고, 전체 폼의 onSubmit에 handleUploadStorage를 등록해 supabase와 연결하였습니다.
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);
});
};
// 이미지 제거
const handleImageDelete = (index) => {
setPostImages((prev) => prev.filter((_, i) => i !== index));
setPreviewImages((prev) => prev.filter((_, i) => i !== index));
};
// 수퍼베이스 스토리지에 파일 업로드 후 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,
});
};