이번에 Supabase를 사용하여 이미지를 스토리지 버킷에 저장하고 저장된 이미지 URL을 Users 테이블의 컬럼 값으로 업데이트하여 유저가 본인이 원하는 프로필 이미지로 변경할 수 있는 기능을 구현을 하였다. 생각보다 어려웠던 작업이었다...🥹 작업했던 방식과 트러블 슈팅에 관련하여 글을 작성해보려 한다.
Supabase의 스토리지 버킷을 사용하여 이미지를 업로드하고, 업로드된 이미지의 URL을 Users 테이블에 저장하는 방법을 구현해야 한다. 이를 통해 사용자는 자신이 원하는 프로필 이미지를 설정할 수 있다.
Table Editor에서 테이블을 생성한다. 이때 나에게 맞는 RLS설정을 반드시해준다.
storage 에서 New buket을 만든후 Policies를 선택하여 내가 작업할 내용에 맞게 설정해준다. 나는 SELECT, INSERT, UPDATE, DELETE가 다 필요 했기떄문에 모두 설정하였다.
처음에 업로드 중임을 나타내기 위해 상태 관리를 하고, 이미지를 누적해서 테이블에 저장할 필요가 없기 때문에
upsert
기능을 사용하여 새로운 이미지만 저장하기 위해 고유 파일 이름을 생성한다. 그리고 이미지 업로드 단계별로 오류를 처리해주고, 이미지 URL을 가져와서 그 URL을 Users 테이블에 업데이트한다. 업데이트가 성공하면setProfileImage
,setProfileAlt
를 변경한다.
const uploadProfileImage = async (file: File | Blob, altText: string) => {
setUploading(true); // 이미지 업로드중 상태 변경
try {
// 고유한 파일 이름 생성 (userId를 base64로 인코딩하여 만듦)
const uniqueFileName = `profile_${base64Encode(userId)}.png`;
// 이미지를 Supabase 스토리지에 업로드
const { error: uploadError } = await supabase.storage
.from("images")
.upload(`profileImages/${uniqueFileName}`, file, { upsert: true });
// 업로드 중 오류가 발생하면 콘솔에 오류 메시지 출력하고 함수 종료
if (uploadError) {
console.error("파일 업로드 에러:", uploadError);
setUploading(false);
return;
}
// 업로드된 이미지 URL 가져오기
const { data: profileImageUrlData } = await supabase.storage
.from("images")
.getPublicUrl(`profileImages/${```
uniqueFileName
```}`);
// URL에서 publicUrl 속성 추출
const profileImageUrl = profileImageUrlData.publicUrl;
// URL이 존재하면 Users 테이블의 프로필 이미지 URL 업데이트
if (profileImageUrl) {
const { error: updateError } = await supabase
.from("Users")
.update({ profile_image_url: profileImageUrl })
.eq("user_id", userId);
if (updateError) {
// 업데이트가 실패하면 콘솔에 오류 메시지 출력
console.error("프로필 이미지 URL 업데이트 에러:", updateError);
} else {
// 업데이트가 성공하면 상태를 새 URL과 alt 텍스트로 변경
setProfileImage(profileImageUrl);
setProfileAlt(altText);
}
} else {
// URL을 가져오기를 실패하면 콘솔에 오류 메시지 출력
console.error("유효한 프로필 이미지 URL을 얻지 못했습니다.");
}
} catch (error) {
// 전체 과정 중 오류가 발생하면 콘솔에 오류 메시지 출력
console.error("파일 업로드 중 오류 발생:", error);
} finally {
// 업로드 상태를 완료로 변경
setUploading(false);
}
};
이미지 관련하여 필요한 함수를 생성한다. 자세한 내용은 트러블 슈팅에 작성하였다.
const getProfileImageUrl = (url: string) => {
return `${url}?${new Date().getTime()}`;
};
const base64Encode = (str: string) => {
return Buffer.from(str).toString("base64");
};
사용자가 파일을 선택하면 uploadProfileImage 함수를 호출한다.
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
// 사용자가 선택한 파일을 가져옴
const file = event.target.files?.[0];
// 파일이 존재하는지 확인
if (file) {
// uploadProfileImage 함수를 호출하여 파일을 업로드 하여, 프로필 이미지 업데이트
await uploadProfileImage(file, "프로필 이미지");
}
};
사용자가 파일을 선택하면 uploadProfileImage 함수를 호출합니다. 이때, 데이터를 효율적으로 처리하고 전송하기 위해 Blob을 사용하여 프로필 사진 업로드 시간을 줄였다.
const handleIconClick = async (iconUrl: string, altText: string) => {
// 아이콘 URL로부터 파일 데이터를 가져옴
const response = await fetch(iconUrl);
// 파일 데이터를 Blob 형식으로 변환
const blob = await response.blob();
// Blob 데이터를 사용하여 프로필 이미지를 업로드하고 업데이트
await uploadProfileImage(blob, altText);
};
📄 Blob이란? Blob(Binary Large Object)은 JavaScript에서 바이너리 데이터를 나타내는 객체입니다. 주로 파일과 같은 대용량 바이너리 데이터를 다루기 위해 사용됩니다. 예를 들어, 이미지, 비디오, 오디오 파일 등을 다룰 때 Blob 객체를 자주 사용한다.
이미지 로드에 실패하면 기본 이미지를 설정한다.
const handleImageError = () => {
// 실패시 기본 이미지로 설정
setProfileImage(defaultImage);
// alt 텍스트 설정
setProfileAlt("프로필 이미지");
};
파일 입력 요소를 클릭하여 파일 선택 창을 연다.
const handleFileUploadClick = () => {
// fileInput 요소를 가져옴
const fileInput = document.getElementById("fileInput") as HTMLInputElement;
// fileInput 요소가 존재하는지 확인
if (fileInput) {
// fileInput 요소를 클릭하여 파일 선택 창을 연다.
fileInput.click();
}
}
Supabase의 Row Level Security(RLS) 정책을 설정하지 않아 작업이 안됬다. 코드에서 잘못됬나 계속 시도했지만, 해결이 안되서 구글링을 해보니 RLS 설정을 해야한다는 글을 보았다. RLS는 데이터베이스의 보안을 강화하기 위해 필요한 설정이지만, 초기 설정 시 놓치기 쉬운 부분이였던거 같다 개발 중에는 RLS 설정을 비활성화하여 작업하니 바로 해결됬다!
고유의 이미지명이 필요하여 Users테이블의 user_id 값을 가져왔는데
그러다보니 이미지명에 노출되면 안되는 값이 노출되는거 같아서 이미지명을 한번 더 난독화 시켜주기위해 문자열 Base 64로 인코딩 하였다.
const base64Encode = (str: string) => {
return Buffer.from(str).toString("base64");
};
동일한 이미지명을 사용하다보니 캐시 문제가 발생하여 현재 시간을 이미지명에 추가하여 캐시 문제를 해결하였다.
const getProfileImageUrl = (url: string) => {
return `${url}?${new Date().getTime()}`;
};