react native 프로젝트에서 supabase storage에 이미지를 저장을 할 때, 주의해야할 사항이 있다.
React Native는 웹 브라우저와 달리 File, Blob, FormData와 같은 웹 표준 파일 객체를 완벽하게 지원하지 않는다.
특히, Supabase Storage의 공식 JS 클라이언트는 브라우저 환경을 기준으로 만들어져 있어, React Native에서 파일을 바로 업로드하려고 하면 제대로 동작하지 않는 경우가 많다 출처
이 때문에 base64, arrayBuffer를 사용해서 사진을 저장했다.
Base64는 컴퓨터에서 이진 데이터를 텍스트로 안전하게 변환하는 인코딩 방식이다. 즉, 바이너리 데이터를 ASCII 문자 집합 내의 64가지 인쇄 가능한 문자로 변환해, 텍스트 기반의 시스템(이메일, 웹, XML, JSON 등)에서 데이터가 손상되지 않고 전송될 수 있게 해준다.
ArrayBuffer는 자바스크립트에서 고정 크기의 연속된 메모리 공간을 할당하여 바이너리(이진) 데이터를 저장할 수 있게 해주는 객체이다.
쉽게 말해서 Base64를 사용해서 보낼 수 있게 만들고 ArrayBuffer를 사용해서 디코딩 할 수 있게 해준다. 또한 Base64로 인코딩시 이미지는 용량이 33% 정도 커지므로, 실제 업로드 시에는 다시 디코딩해 ArrayBuffer로 변환하는 것이 효율적이다.
나는 아래와 같이 구현하였다.
export const uploadImageAndGetUrl = async (imageUri: string, bucketName: string): Promise<string | null> => {
try {
const userId = await getCurrentUserId();
if (!userId) {
console.error('User not authenticated');
return null;
}
// 1. 파일 확장자 추출
const extension = imageUri.split('.').pop() || 'jpg';
const fileName = `${userId}/${new Date().toISOString()}_${Math.random().toString(36).substring(2, 15)}.${extension}`;
// 2. base64로 이미지 읽기
const base64Data = await RNFS.readFile(imageUri, 'base64');
// 3. base64를 ArrayBuffer로 변환 (base64-arraybuffer 패키지 사용)
const arrayBuffer = decodeBase64(base64Data);
// 4. Supabase Storage에 업로드
const { data, error: uploadError } = await supabase.storage
.from(bucketName)
.upload(fileName, arrayBuffer, {
contentType: `image/${extension}`,
cacheControl: '3600',
upsert: false,
});
// 5. 공개 URL 가져오기
const { data: publicUrlData } = supabase.storage
.from(bucketName)
.getPublicUrl(data.path);
return publicUrlData.publicUrl;
} catch (err) {
console.error('uploadImageAndGetUrl function error:', err);
return null;
}
};
Supabase Storage에서 버킷이 "private"으로 설정하면 insert,update등에 제약을 걸 수 있다. 하지만 사진을 가져오려면 인증이 필요하며 signed URL을 만들어서 진행한다.
signed URL은 일정 시간 동안만 유효한, 인증이 내장된 임시 링크를 만들어 주는 방식이다. 이 링크를 가진 사람은 해당 시간 동안만 이미지를 볼 수 있다.
나는 아래와 같이 구현했다.
// 1. 서명된 URL 생성 함수
export const getSignedImageUrl = async (imagePath: string, bucketName: string): Promise<string | null> => {
try {
const { data, error } = await supabase.storage
.from(bucketName)
.createSignedUrl(imagePath, 3600); // 1시간 유효
if (error) {
console.error('Error creating signed URL:', error);
return null;
}
return data.signedUrl;
} catch (err) {
console.error('getSignedImageUrl function error:', err);
return null;
}
};
// 2. 식물 데이터와 함께 서명된 URL 생성
export const getCurrentUserFoundPlants = async () => {
// ... 데이터베이스에서 식물 정보 가져오기 ...
// 각 이미지에 대해 서명된 URL 생성
const plantsWithSignedUrls = await Promise.all(
data.map(async (plant) => {
try {
// URL에서 버킷 이름과 파일 경로 추출
const urlParts = plant.image_url.split('/');
const bucketName = urlParts[urlParts.indexOf('public') + 1];
const filePath = urlParts.slice(urlParts.indexOf('public') + 2).join('/');
if (bucketName && filePath) {
const signedUrl = await getSignedImageUrl(filePath, bucketName);
return { ...plant, signed_url: signedUrl || undefined };
}
} catch (err) {
console.error('Error processing image URL:', err);
}
return { ...plant, signed_url: undefined };
})
);
return plantsWithSignedUrls;
};
// 3. React Native에서 이미지 표시
<Image
source={{ uri: plant.signed_url || plant.image_url }}
className="w-full h-[200px]"
resizeMode="cover"
defaultSource={require('@assets/default.png')}
/>