프로필 저장 성능 개선

김현준·2025년 6월 4일

문제

  • 프로필 저장 시 전체 3~4초 소요
  • 그중 이미지 삭제 + 업로드만 2.5초, 병목 지점으로 확인됨

테스트

측정 방식

console.time()을 사용해 실제 프로필 저장에 걸리는 시간을 측정

console.time('...');
console.timeEnd('...');

테스트 항목(핵심 코드)

유저 프로필 업로드 form 제출 시 실행되는 코드

const onSubmit = async () => {
  console.time('프로필 저장 전체 시간');

  console.time('1.이미지 삭제 + 업로드');
  // 기존 이미지 삭제 → 새 이미지 압축(webp) → 업로드 로직
   ...
  console.timeEnd('1.이미지 삭제 + 업로드');

  console.time('2.patchUser 요청');
  await patchUser(...);
  console.timeEnd('2.patchUser 요청');

  console.time('3.캐시 무효화 + 리디렉션');
  queryClient.invalidateQueries(...);
  router.push(...);
  console.timeEnd('3.캐시 무효화 + 리디렉션');

  console.timeEnd('프로필 저장 전체 시간');
};

개선 전 성능 분석

총 프로필 저장 시간: 3,858 ms

  • 이미지 삭제 + 업로드: 2,491 ms
  • patchUser 요청: 1,362 ms
  • 캐시 무효화 + 리디렉션: 4.98 ms

핵심 부분(이미지 삭제 + 업로드)

console.time('기존 이미지 삭제');
await supabase.storage.from('profiles').remove([...]);
console.timeEnd('기존 이미지 삭제'); //196.76ms

console.time('압축 시간');
await compressImageToWebp(...);
console.timeEnd('압축 시간'); //85.86ms

console.time('업로드 시간');
await supabase.storage.from('profiles').upload(...);
console.timeEnd('업로드 시간'); //1,280.17ms
  • 가장 큰 문제는 이미지 업로드였고, SDK의 오버헤드가 원인으로 추정

문제 분석

수파베이스 SDK 방식의 한계

  • 수파베이스 SDK의 업로드 과정에서 인코딩, 권한 확인, 버킷 경로 처리 등 여러 작업이 포함되어 오버헤드 발생
  • 실제 업로드 시간이 가장 크며, 문제의 핵심

기존 방식

await supabase.storage.from('profiles').upload(path, file, { upsert: true });

수파베이스가 대신 처리해주는 방식

  • 파일을 수파베이스 SDK에 넘기면 수파베이스가 중간에서 파일을 대신 S3로 전송

내부 처리 흐름(브라우저 → Supabase 서버 → S3)

  1. 파일 인코딩(Base64 → Blob)
  2. 인증/권한 확인
  3. 경로 설정
  4. 파일 전송

중간에서 수파베이스 서버가 파일을 한 번 더 받고 처리한 다음, 다시 전달하는 구조라서 네트워크 비용 + 서버 처리 시간이 추가

장점

  • 코드가 간단
  • 인증이 자동으로 처리(예: 로그인된 사용자만 업로드 가능하게 하는 등)

단점

  • 파일이 수파베이스 서버를 거쳐야 하므로 중간에 우회가 생김
  • 파일 크기가 크면 업로드 시간이 훨씬 늘어남
  • 파일이 작아도 중간 처리 단계가 많아 느림

추가 고민

수파베이스 자체의 업로드 시간이 오래 걸리는 것으로 보인다.
혹시나 Region이 해외로 되어있는지 확인해봤으나 한국으로 되어있었다.

  • Region이 해외로 되어있나? NO
    • 한국으로 되어 있음
  • 무료플랜 업로드 파일 제한을 초과했나? NO
    • 무료플랜 최대 파일 크기 제한: 50MB
    • 내 사이트에서 업로드 되는 파일 크기: 약 97KB

해결: PreSigned URL 방식 전환

개선 방식 (PreSigned URL + fetch)

const { signedUrl } = await getSignedUploadUrl('profiles', filename);
await fetch(signedUrl, {
  method: 'PUT',
  headers: { 'Content-Type': file.type },
  body: file,
});

사용자가 직접 S3에 업로드하는 방식

  • 수파베이스는 업로드용 서명된 URL (signed URL) 하나만 만들어줌
  • 사용자가 이 URL로 직접 파일을 업로드

즉, 수파베이스는 "여기 URL 있으니 이걸로 바로 업로드 해라" 라는 역할만 하고 실제 업로드는 브라우저 → S3로 직접 연결되어 진행된다.

PreSigned 방식의 흐름(브라우저 → 바로 S3)

  1. 서버에서 Supabase API로 업로드용 서명 URL 발급
  2. 클라이언트는 해당 URL로 fetch 전송 (HTTP PUT)
  3. S3가 바로 저장 (수파베이스 서버는 우회됨)

중간 서버 없이 브라우저가 바로 S3에 PUT 요청하기 때문에 빠르고 효율적

장점

  • 서버를 거치지 않고 직접 S3에 올리니까 시간 단축(훨씬 빠름)
  • 특히 이미지 압축하고 올릴 때 체감이 큼
  • 서버 부하 감소
  • 보안성 증가: 서명 URL에 유효시간 존재
  • 병렬 업로드 쉬움 (Promise.all 등)

단점

  • 직접 fetch()로 요청을 보내야 하므로 코드가 살짝 복잡해짐

개선 후 측정 결과

항목소요 시간
기존 이미지 삭제261 ms
이미지 압축111 ms
서명 URL 생성558 ms
fetch 업로드271 ms
이미지 삭제 + 업로드 합계1,208 ms
patchUser 요청1,111 ms
캐시 무효화 + 리디렉션1.92 ms
총 프로필 저장 시간2,321 ms

개선 전후 비교

  • 이미지 업로드: 약 5배 향상
    • 1,280 ms -> 271 ms
  • 전체 저장 시간: 약 40% 향상
    • 3,858 ms -> 2,321 ms

추가내용

Supabase SDK 방식과 PreSigned URL 방식이 아직 잘 이해되지 않았다면?

Supabase SDK의 이미지 업로드 흐름은 다음과 같다고 했었다.
1. 파일 인코딩(Base64 → Blob)
2. 인증/권한 확인
3. 경로 설정
4. 파일 전송

여기서 3번과 4번을 사용자가 하게 된다고 생각하면 쉽다.

역할 분담 비교표

처리 단계Supabase SDK 방식PreSigned URL 방식
1. 파일 인코딩수파베이스 내부 처리 (숨겨짐)사용자가 파일을 준비해야 함 (Blob, File, 압축 등)
2. 인증/권한 확인Supabase SDK가 자동 수행수파베이스가 URL 발급 시 1회 수행 (서명 포함)
3. 경로 설정 (버킷/파일명)수파베이스가 설정함 (from().upload(...)에서 처리)사용자가 직접 설정해서 수파베이스에 전달 (getSignedUploadUrl(bucket, path))
4. 파일 전송수파베이스 서버가 대신 S3에 업로드사용자가 직접 fetch(signedUrl, { PUT })로 업로드

지하철에 비유해보자.

방식비유경로속도 차이 이유
Supabase SDK 방식지하철 + 환승브라우저 → 수파베이스 → S3수파베이스가 중간에 파일을 수신하고, 인증도 매번 확인해야 해서 느림
PreSigned URL 방식직통 열차브라우저 → S3중간 경유 없음. 인증은 미리 해결됐고, 바로 전송하니 빠름

왜 PreSigned URL은 안전한가?

보안 문제는 signedUrl이 해결해준다.

  • 짧은 시간 동안만 유효
  • 지정한 경로에만 업로드 가능
  • 파일명도 고정 가능
profile
기록하자

0개의 댓글