Supabase Storage 이미지 업데이트 지연 문제 해결기 (CDN 캐시와의 싸움)

강지수·2025년 5월 3일
0

최종 프로젝트 Uuno

목록 보기
5/6

1. 문제 상황: 이미지를 분명히 바꿨는데 왜 예전 모습 그대로인가?

디지털 명함 에디터 개발 중, 사용자가 Konva 캔버스에서 디자인한 명함 앞/뒷면을 이미지로 저장하는 기능을 구현했다. Konva.Stage 객체의 toDataURL() 메서드를 사용해 캔버스 내용을 PNG 이미지 데이터로 변환하고, 이를 Blob으로 만들어 Supabase Storage에 업로드하는 방식이다.

TypeScript

// Konva 스테이지를 PNG 데이터 URL로 변환
const dataUrl = stage.toDataURL({ mimeType: 'image/png', pixelRatio: 2 });
// Data URL을 Blob으로 변환
const blob = dataURLToBlob(dataUrl);
// 파일 경로 및 이름 지정 (예: userId/slug/front_slug_img.png)
const fileName = `${label}_${lastSlug}_img.png`;
const filePath = `${userId}/${lastSlug}/${fileName}`;
// Supabase Storage에 업로드
const publicUrl = await uploadToSupabaseStorage('cards', filePath, blob);

또한, 사용자가 명함을 수정하고 다시 저장할 때 기존 이미지를 덮어쓰거나, 때로는 기존 폴더의 모든 파일을 삭제(deleteAllFilesInFolder 함수 사용)한 후 새로 업로드하는 로직을 사용했다.

그런데 문제가 발생했다. 명함 디자인을 수정하고 다시 저장하여 Supabase Storage에 새로운 이미지 파일을 업로드했음에도 불구하고, 웹사이트에서는 여전히 수정 전의 예전 이미지가 보이는 현상이 나타난 것이다. next/image 컴포넌트나 일반 <img> 태그로 해당 이미지 URL을 불러왔을 때, 변경사항이 즉시 반영되지 않았다.

2. 원인 분석: 범인은 바로 'CDN 캐시'이다

처음에는 업로드 로직이나 파일 경로 문제인가 싶어 코드를 여러 번 검토했지만, 문제는 다른 곳에 있었다. 바로 Supabase Storage의 CDN(Content Delivery Network) 캐시 정책 때문이다.

Supabase Storage는 성능 향상을 위해 기본적으로 CloudFront와 같은 CDN을 사용하여 파일을 사용자에게 전달한다. CDN은 지리적으로 가까운 서버에 파일의 복사본(캐시)을 저장해두고 사용자의 요청에 빠르게 응답하는 기술이다.

문제는 여기에 있다. 내가 Supabase 버킷의 원본 파일을 삭제하고 같은 이름, 같은 경로로 새 파일을 업로드해도, CDN 서버는 일정 시간(TTL: Time To Live) 동안 이전에 캐시해 둔 예전 버전의 파일을 계속 사용자에게 보여주는 것이다. Supabase 공식 문서에서도 이 동작을 설명하고 있다.

Supabase Storage caches public files using a CDN... When a file is updated or replaced, the CDN may continue to serve the cached version until it expires. - Supabase Docs

안타깝게도 Supabase는 현재 사용자가 직접 특정 파일의 CDN 캐시를 강제로 무효화(Invalidation)하는 기능을 제공하지 않는다. 따라서 단순히 파일을 덮어쓰는 것만으로는 즉각적인 이미지 변경을 보장할 수 없다는 결론에 도달했다.

3. 해결 전략: CDN 캐시를 우회하라!

CDN 캐시를 직접 제어할 수 없다면, CDN이 새 파일로 인식하도록 만드는 수밖에 없다. 두 가지 주요 해결 방법이 존재한다.

방법 1: 파일 이름에 고유 식별자 추가 (Timestamp or UUID)

가장 확실한 방법 중 하나는 파일을 업로드할 때마다 고유한 이름을 생성하는 것이다. 파일 이름에 현재 시간의 타임스탬프(Date.now())나 UUID 같은 고유 식별자를 포함하면, 매번 새로운 파일로 인식되어 CDN 캐시 문제를 원천적으로 피할 수 있다.

// uploadStageImage 함수 내 파일 이름 생성 부분 수정
const timestamp = Date.now();
const fileName = `${label}_${lastSlug}_${timestamp}_img.png`; // 고유 식별자 추가
const filePath = `${userId}/${lastSlug}/${fileName}`;
// ... 이후 업로드 로직 동일 ...

장점: 캐시 문제를 확실하게 회피한다. 파일 버전 관리에도 유리할 수 있다.
단점: 이전 버전의 파일이 Storage에 계속 쌓일 수 있어 별도의 정리 로직이 필요할 수 있다. DB에 저장하는 이미지 URL도 매번 갱신해야 한다.
방법 2: Public URL에 캐시 무력화(Cache Busting) 쿼리 스트링 추가 (권장)

파일 이름과 경로는 동일하게 유지하되, 이미지 URL을 사용할 때마다 뒤에 고유한 쿼리 스트링을 붙여주는 방식이다. 브라우저나 CDN은 URL 전체를 기준으로 캐시를 판단하므로, 쿼리 스트링이 다르면 다른 URL로 인식하여 새로 파일을 요청하게 된다.


// uploadToSupabaseStorage 함수 마지막 return 부분 수정

// ... getPublicUrl 로직 이후 ...
if (!publicUrlData?.publicUrl) {
  throw new Error('public url 가져오기 실패');
}

// 캐시 우회를 위해 타임스탬프 쿼리 스트링 추가
return `${publicUrlData.publicUrl}?v=${Date.now()}`;

이 방식을 사용하면 uploadStageImage 함수는 수정할 필요 없이, uploadToSupabaseStorage 함수가 반환하는 URL에 항상 최신 타임스탬프가 붙게 된다.

장점: 파일명과 경로를 깔끔하게 유지할 수 있다. DB에 저장된 기본 URL 구조가 바뀌지 않는다. 구현이 비교적 간단하다.
단점: URL을 사용하는 모든 곳에서 이 패턴을 적용해야 할 수 있다. (하지만 보통 DB에 저장하고 불러올 때 적용하면 된다.)
우리 팀은 논의 끝에 방법 2 (쿼리 스트링 추가) 를 채택했다. 파일 관리의 복잡성을 줄이면서도 효과적으로 캐시 문제를 해결할 수 있다고 판단했기 때문이다.

4. 배운 점 및 결론

이번 경험을 통해 클라우드 스토리지와 CDN의 동작 방식을 더 깊이 이해하게 되었다. 단순히 파일을 업로드하고 URL을 가져오는 것을 넘어, 캐시 정책이 실제 애플리케이션 동작에 어떤 영향을 미치는지 깨닫는 중요한 계기가 되었다.

Supabase Storage를 사용하면서 이미지나 파일 업데이트가 즉시 반영되지 않는 문제를 겪는다면, CDN 캐시를 의심해보고 '쿼리 스트링을 이용한 캐시 무력화'나 '고유 파일명 사용' 전략을 적용해보는 것을 적극 추천한다. 이는 비단 Supabase뿐만 아니라 다른 클라우드 스토리지 및 CDN 서비스를 사용할 때도 유용하게 적용될 수 있는 중요한 개념이다.

profile
프론트엔드 잘하고 싶다

0개의 댓글