![[인프런 워밍업 클럽 3기] 풀스택 스터디 2주차 미션 회고 발자국](https://cdn.inflearn.com/public/files/blogs/a4a91959-90cc-4aa1-9bb8-57495436a368/thumbnail.png)
[인프런 워밍업 클럽 3기] 풀스택 스터디 2주차 미션 회고 발자국
학습 내용 요약
인프런 워밍업 클럽 3기 풀스택 스터디 2주차에는 Supabase Storage를 활용하는 방법을 다루었습니다. 인프런에서 발자국을 작성할 때 강의를 보지 않고도 강의 내용을 파악할 수 있을 만큼 자세한 내용을 작성하는 건 지양해 달라고 가이드한 만큼 강의에 대한 필기는 최소화 하고 회고 위주로 적어보겠습니다.
Supabase Strorage 기능 간단 정리
Supabase의 Storage 기능은 파일 저장을 위한 서비스로, AWS S3와 같은 오브젝트 스토리지 기능을 제공함. Next.js, React, Flutter, Node.js 등 다양한 환경에서 사용할 수 있으며, PostgreSQL 기반의 권한 관리(RLS)를 지원하는 것이 특징임.
1. 파일 저장 및 관리
이미지, 동영상, 문서 등 다양한 파일 형식을 저장 가능
파일 업로드, 다운로드, 삭제, 이동 등의 기능 제공
버킷(Bucket) 단위로 파일을 관리
2. 권한 및 보안 (RLS)
Row Level Security (RLS): PostgreSQL과 동일한 방식으로 접근 권한을 설정 가능
공개(Public) 및 비공개(Private) 버킷 지원
JWT 기반 인증을 사용하여 사용자별 접근 제한 가능
3. 파일 접근 방식
퍼블릭 파일: 누구나 URL을 통해 접근 가능
프라이빗 파일: 인증된 사용자만 접근 가능 (Signed URL 활용)
서명된 URL (Signed URLs): 일정 시간 동안만 유효한 URL 생성 가능
4. 폴더 및 파일 구조
디렉토리(폴더) 개념 지원
폴더 내에서 파일 정리 및 관리 가능
5. API 및 SDK 지원
Supabase Client SDK를 통해 간편한 파일 업로드 및 관리 가능
RESTful API 제공 (HTTP Client를 사용하여 직접 호출 가능)
6. 이미지 변환 및 최적화
Supabase Storage Image Transformation 지원 (이미지 크기 조절, 포맷 변경 등)
CDN을 통해 빠른 이미지 제공 가능
Dropbox 클론 미션 회고
풀스택 스터디 2주차 미션은 강의에서 진행하는 Next.js와 Supabase Storage를 활용한 Dropbox 클론 앱에 사진 별 마지막 수정 시간을 표시하는 것이었지만, 저는 기타 편의기능을 더 추가해 보았습니다. 진행했던 미션은 해당 링크에서 보실 수 있습니다.
미션 수행 내용
아래는 제가 수행한 미션에 대한 내용을 정리해보았습니다.
기능 명세
이미지 파일 업로드 기능
드래그 앤 드롭 기능
다중 업로드 기능
업로드한 이미지 파일 조회 기능
키워드 검색 기능
이미지 파일 다운로드 기능
업로드한 이미지 파일 수정 기능
이미지 파일명 변경 기능
업로드한 이미지 파일 삭제 기능
강의에서는 기본적으로 파일 입출력에 관련된 기능만 다루었지만, 저는 실제 사용성을 고려하여 파일 업로드 전 미리보기 기능, 이미지 다운로드 기능, 파일 이름 변경 기능 등을 추가해 보았습니다. 또한 강의에서는 react-dropzone
라이브러리를 사용해서 드래그 앤 드롭 기능을 구현하였지만, 저는 프로젝트의 기본적인 스타일 프레임워크로 만타인을 사용하고 있었기 때문에, 만타인에서 따로 지원하는 @mantine/dropzone
라이브러리를 사용하여 구현하였습니다. 미션에 사용한 기술들은 아래에 따로 정리해 두었습니다. 사용할 때마다 느끼는 거지만 만타인은 정말 편한 것 같아요.
사용 기술
프레임워크:
Next.js v15
,React v19
데이터베이스:
Supabase
서버 상태관리:
Tanstack Query v5
클라이언트 상태관리:
Zustand v5
스타일 프레임워크:
TailWindCSS v3
,Mantine v7
,Tabler Icons
모노레포:
Turbo Repo
패키지 매니저:
pnpm
트러블 슈팅
아래는 미션을 진행하면서 만났던 문제들을 해결하는 과정에 대한 내용입니다.
파일명에 한글이 포함될 경우 Supabase Storage에 업로드하지 못하는 문제
이미지 파일 이름에 한글이 포함될 경우 업로드가 되지 않는 문제가 있었습니다. 이슈를 찾아보니 Supabase Storage의 정책적인 문제였고, AWS의 S3 서비스도 동일한 문제를 가지고 있었기에 아래 조치들을 취하였습니다.
조치 1.
처음 취했던 조치는 아래와 같이 nanoid
라이브러리를 활용하여 중복되지 않는 이름을 생성 후 기존의 파일 이름을 대체하는 방식을 사용했었습니다. 하지만 해당 방식을 사용하면 기존의 파일 이름이 사용자가 식별하지 못하는 값으로 대체되는 문제와, 중복되는 파일을 확인할 수 있는 방법이 없어지는 문제가 있어 최종적으로는 사용하지 않았습니다.
'use server'
import { nanoId } from 'nanoid'
export const uploadImages = async ({
files,
}: UploadImagesParams): Promise<{ data: { id: string; path: string } | null }[]> => {
const client = await createServerSupabaseClient()
return await Promise.all(
files.map((file) => {
const extension = extractExtension(file.name)
const path = `/${nanoId() + '.' + extension}`
return client.storage
.from(process.env.SUPABASE_BUCKET_NAME!)
.upload(path, file, { upsert: true })
}),
)
}
조치 2.
두 번째로 취한 조치는 조금 번거롭긴 하지만 파일과 1 대 1 로 대응되는 데이터베이스 테이블을 만들어서 관리하는 방식을 사용하였습니다. Supabase에서 지원하는 uuid
를 활용하여 테이블의 Primary Key를 설정해 주었고, 이미지 업로드 시 먼저 테이블에 기존 파일 이름을 기반한 데이터 insert 후 생성된 uuid
를 사용하여 파일명을 재설정하는 방식으로 우회하였습니다. Supabase에서 지원하는 uuid
를 사용했기에 nanoid
같은 별도의 식별자 생성 라이브러리를 관리하지 않을 수 있었습니다.
export const uploadImages = async ({
files,
}: UploadImagesParams): Promise<{ data: { id: string; path: string } | null }[]> => {
const client = await createServerSupabaseClient()
const databaseQueries = files.flatMap((file) => {
return client
.from('minibox')
.upsert({ name: file.name })
.select()
.then((result) => result.data?.[0] ?? null)
})
const targetFiles = await Promise.all(databaseQueries)
const storageQueries = targetFiles.map((data) => {
if (!data) {
return { data: null }
}
const extension = extractExtension(data.name)
const path = `/${data.id + '.' + extension}`
const file = files.find((file) => file.name === data.name)!
return client.storage
.from(process.env.SUPABASE_BUCKET_NAME!)
.upload(path, file, { upsert: false })
})
return await Promise.all(storageQueries)
}
업로드한 파일명이 한글일 경우 올바르게 검색 되지 않는 문제 (feat. MacOS)
MacOS 환경에서 업로드한 파일을 별도의 후처리 없이 그대로 데이터베이스에 업로드 했더니 한글이 포함된 파일명에 대해서 아래와 같이 문자열 포함 여부를 판단하는 ilike
쿼리가 제대로 동작하지 않는 문제가 있었습니다.
export const getImages = async ({ query = '' }: GetImagesParams): Promise<DroppedImageFile[]> => {
const client = await createServerSupabaseClient()
const imagesDataAll = await client.from('minibox').select('*').ilike('name', `%${query}}%`)
}
원인을 분석해보니 아래와 같이 파일 이름에 한글이 포함되어 있을 시 자음과 모음이 모두 분리된 상태로 저장되어 있어 특정 키워드 포함 여부를 올바르게 판단하지 못해 발생한 문제였습니다.
// input
'temp-훈이머리귤.jpeg'.split('')
조치
기존에 사용하던 ilike
쿼리를 제거하고, 자바스크립트에서 지원하는 String.prototype.normalize
메서드를 사용하여 기존 데이터에 대한 정규형 정준 결합(Normalization Form Canonical Composition) 절차를 거친 후 필터링을 거치는 방법으로 해결하였습니다.
export const getImages = async ({ query = '' }: GetImagesParams): Promise<DroppedImageFile[]> => {
const client = await createServerSupabaseClient()
const imagesDataAll = await client.from('minibox').select('*')
const targetData = imagesDataAll.data
.filter(({ name }) => name.normalize('NFC').includes(query))
.map(({ id, name }) => `${id + '.' + extractExtension(name)}`)
}
후기
저는 이제껏 프론트엔드 개발을 접해보면서 한 번도 이미지 파일에 관련된 기능을 작업해보지 않았었습니다. 물론 서버 액션을 사용해서 조금 더 간략한 인터페이스를 사용했기에 실제 FormData 인터페이스를 사용하여 통신 로직을 작성하는 경험은 해보지 못했다는 한계가 있지만, 이번 미션을 통해 자바스크립트로 이미지 파일을 핸들링하는 방법과, Storage 서비스를 연동하여 저장까지 모두 접해볼 수 있어서 개인적으로는 만족스러웠던 한 주였던 것 같습니다.
긴글 읽어주셔서 감사합니다. ☺
댓글을 작성해보세요.