Vercel Image Optimization 요금 폭탄 원인과 확실한 해결책을 제시합니다. Next.js unoptimized 전역 설정법부터 AWS S3 저장소 고아 이미지 청소 자동화 구현까지 인프라 비용 절감 노하우를 확인해보세요.
브레이크 없는 Vercel 이미지 과금, 원인부터 짚기
Next.js의 <Image> 컴포넌트는 성능 최적화의 일등 공신이지만, 별다른 설정 없이 Vercel 인프라에 배포하면 상상 이상의 Image Optimization 및 Fast Data Transfer 청구서를 마주하게 됩니다.
많은 개발자가 비용을 줄이기 위해 개별 컴포넌트에 unoptimized 속성을 붙이거나 이미지 용량을 줄이려고 시도하지만, 인프라의 과금 동작 메커니즘을 정확히 모르면 비용은 엉뚱한 곳으로 이동할 뿐입니다. 본 포스트에서는 이미지 과금에 대한 오해를 바로잡고, 최근 프로젝트 인프라를 개선하며 정립한 전역 비용 방어 및 S3 스토리지 정리 노하우를 공유합니다.
sizes 속성은 정말 과금 트리거일까?
가장 먼저 흔하게 하는 오해는 "next/image에 sizes를 직접 지정하면 변형 비용이 발생하는가?"입니다.
결론부터 말씀드리면, sizes 속성 자체가 과금을 유발하는 트리거는 아닙니다. 과금의 핵심 조건은 이미지가 최적화 파이프라인(/_next/image)을 거치는가입니다. 다만 sizes는 파이프라인을 타는 이미지에서 생성되는 너비(변형)의 개수를 조절하여 비용에 직접적인 영향을 줍니다.
-
sizes="100vw"(넓게 설정 시): 대형 화면까지 대응하기 위해 매우 다양한 너비의srcset목록이 빌드됩니다. (예: w=640, 750, 828, 1080, 1200 등) 브라우저가 기기 해상도에 맞춰 이 요청들을 날릴 때마다 각각 별개의 고유 변형 건수로 잡혀 비용이 증가합니다. -
sizes="(max-width: 768px) 50vw, 33vw"(좁게 설정 시): 뷰포트에 필요한 작은 너비 위주로srcset이 생성되므로, 불필요한 고유 변형 생성을 방지하여 오히려 비용을 낮춰줍니다.
unoptimized를 켰는데도 비용이 줄지 않는 3가지 유력한 원인
"분명 코드에 unoptimized를 적용했는데 왜 비용이 계속 크게 잡힐까?"라는 의문이 든다면 다음 사각지대를 전수조사해야 합니다.
원인 A. 일부 컴포넌트의 적용 누락 (실제 누락 사례)
가장 빈번한 원인입니다. 소스 코드를 전수 조사해 보면 특정 컴포넌트나 태그에서 unoptimized 속성이 누락된 채 방치된 곳들이 발견됩니다. 특히 외부 API에서 수많은 고유 원본 이미지를 받아오는 목록 페이지나 썸네일 뷰에서 최적화 파이프라인이 살아있는 경우, 수십 개의 원본 이미지와 각 sizes별 변형이 곱해져 비용이 폭증하게 됩니다.
원인 B. 비용의 이동
이미지가 크면 메모리 사용량이 늘어 비싸진다는 흔한 오해가 있습니다. 하지만 Vercel에는 이미지용 메모리 과금 항목이 없습니다. 과금 구조는 명확하게 두 축으로 나뉩니다.
| 과금 축 | 무엇으로 과금되나 | 원본 용량과의 관계 |
| Image Optimization | 변형 개수 | 파일 용량과 무관, 고유 조합 개수가 핵심 |
| Fast Data Transfer | 실제 전송된 바이트(GB) | 원본이 클수록 직접적으로 증가 |
unoptimized를 적용하면 리사이즈와 압축 과정이 생략되므로 Image Optimization 비용은 사라집니다. 하지만 수 MB짜리 대형 원본 이미지가 모바일 기기에 그대로 전송되면서 Fast Data Transfer 비용이 폭증할 수 있습니다.
단, 이미지가 이미 AWS CloudFront(CDN) 환경에서 서빙되고 있다면, unoptimized 시 대역폭 비용 역시 Vercel이 아닌 AWS(상대적으로 저렴) 쪽에서 나가므로 Vercel 과금을 완전히 우회할 수 있습니다.
원인 C. 타임스탬프 기반 URL로 인한 캐시 무효화
업로드된 이미지의 파일명이 1783315681453-journal.png와 같이 업로드 시점의 타임스탬프를 포함하는 경우입니다. 이미지 내용이 같더라도 URL 주소가 바뀌면 캐시 시스템은 이를 완전히 새로운 이미지로 인식합니다. 이로 인해 기존 캐시(HIT)를 활용하지 못하고 매번 새 변형을 생성(MISS)하게 되어 비용이 누적됩니다.
해결책 1, next.config.ts 하나로 제어하는 전역 unoptimized 레시피
개별 컴포넌트 파일마다 <Image unoptimized/>를 일일이 적어주는 방식은 휴먼 에러(누락)를 유발하기 딱 좋습니다. 이 문제는 next.config.ts 파일 내에 images.unoptimized = true 설정을 더해 한 줄로 완벽하게 통제하는 것이 안전합니다.
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
// [핵심] 전역 이미지 최적화 OFF
// 모든 <Image> 컴포넌트가 Vercel의 최적화 파이프라인을 완전히 우회합니다.
unoptimized: true,
remotePatterns: [
{ protocol: 'https', hostname: 'cdn.uricosme.com' },
],
},
};
export default nextConfig;
전역으로 스위치를 끈 뒤 기존에 흩어져 있던 개별 컴포넌트의 불필요한 unoptimized 프로퍼티들을 모두 걷어내면 코드 가독성과 비용 예측 가능성을 모두 확보할 수 있습니다.
이때 발생할 수 있는 원인 B(대역폭 폭증)를 막기 위해, 클라이언트 사이드 업로드 시점(src/lib/compressImage.ts)에 이미지 압축 가이드라인(≤1MB / ≤1920px) 및 WebP 포맷 변환(fileType: "image/webp") 과정을 선행하여 원본 자체의 용량을 줄이는 정공법을 병행해야 합니다.
해결책 2, 스토리지 낭비 방어, Product와 Journal 도메인 S3 고아 이미지 정리
타임스탬프 기반 업로드 방식의 진짜 문제는 과금이 아니라, "이미지 교체 및 삭제 시 옛날 원본 파일이 S3 저장소에 그대로 남아 누적되는 스토리지 낭비"였습니다. 이번 인프라 개선을 진행하며 도메인별 편차와 사각지대를 완전히 메웠습니다.
-
Product(제품): 기존 로직에서 제품 삭제 또는 수정 시
deleteImageFromS3가 정상적으로 호출되며 S3 이미지 제거가 잘 적용되어 있었습니다. ✅ -
Journal(저널): 내부 삭제 및 추적 로직이 완전히 누락되어 있어, 게시글을 수정(커버 교체, 본문 이미지 교체)하거나 게시글 자체를 삭제할 때 S3에서 기존 이미지가 전혀 삭제되지 않고 계속 누적되고 있었습니다. ❌
이를 해결하기 위해 공용 헬퍼 함수(src/lib/journal-image-cleanup.ts)를 구축하고, 편집 및 삭제 액션 성공 시점에 결합했습니다.
// src/lib/journal-image-cleanup.ts 설계 요약
// 1. collectOwnedJournalImageUrls(): 본문 내 태그를 파싱하여 우리 소유의 이미지 URL만 정확히 추출
// 2. deleteRemovedJournalImages(): 수정 전/후 목록을 비교하여 더 이상 참조되지 않는 이미지(Old 차집합 New)만 S3에서 선별 삭제
// 3. deleteAllJournalImages(): 저널 글 전체 삭제 시 모든 미디어 일괄 삭제
이 청소 파이프라인에는 외부 아웃바운드 링크는 건드리지 않는 소유권 가드, 다국어 본문 간 동일 이미지 잔존 여부를 판단하는 로케일 공유 보존, S3 API 실패가 비즈니스 로직을 블로킹하지 않도록 격리하는 실패 격리(Promise.allSettled) 장치를 심어 안정성을 높였습니다.
추후 이미지 최적화를 다시 안전하게 켜는 '비용 방어벽' 레시피
서비스 규모가 커지거나 비즈니스 요구사항 변경으로 인해 Next.js 이미지 최적화를 다시 활성화해야 한다면, 무작정 스위치를 켜는 것이 아니라 비용 과금 한계선을 명시한 상태로 활성화해야 합니다.
next.config.ts의 images 블록을 아래 레시피로 교체하여 재활성화를 준비하시기 바랍니다.
images: {
// unoptimized: true, // ← 재활성화 시 이 줄을 주석 처리하거나 제거합니다.
// 1. 변형(Transformation) 개수 축소를 위한 너비 세트 제한
deviceSizes: [640, 750, 828, 1080, 1200], // 기본값보다 초대형 규격 제거
imageSizes: [64, 128, 256], // 썸네일 및 아이콘용 소형 규격 지정
// 2. 포맷 제한 (avif까지 활성화 시 변형 콤보가 2배가 되어 과금도 2배가 됩니다)
formats: ["image/webp"],
// 3. 캐시 수명 최대화 (31일 동안 재연산 요청을 원천 차단)
minimumCacheTTL: 2678400,
remotePatterns: [ /* 기존 설정 그대로 유지 */ ],
},
이와 동시에 컴포넌트 단에서 목록/썸네일용 <Image>의 sizes 속성을 뷰포트에 맞게 타이트하게 지정((min-width:768px) 33vw, 50vw)해주면 불필요한 고유 변형 개수가 늘어나는 현상을 완벽히 방어할 수 있습니다.
-
sizes속성은 과금 트리거가 아니며, 최적화가 켜진 상태에서 좁게 지정할수록 비용을 낮추는 컨트롤러 역할을 합니다. -
Product 도메인은 S3 삭제가 잘 작동하고 있었으나, 사각지대였던 Journal 도메인의 수정/삭제 시 S3 이미지 누적 현상을 청소 헬퍼 로직으로 해결했습니다.
-
이미 업로드 단계에서 고효율 WebP 변환 및 규격 압축이 진행되고 있다면 전역
unoptimized: true구조가 비용 방어에 가장 유리하며, 재활성화는 인프라 Usage 지표를 면밀히 검토한 뒤 결정해도 늦지 않습니다.
Q1. unoptimized: true로 설정하면 Next.js <Image> 컴포넌트의 장점이 사라지나요?
A1. 리사이즈와 포맷 변환 같은 서버 사이드 연산 기능은 꺼지지만, <Image> 컴포넌트가 제공하는 핵심 기능인 Lazy Loading(지연 로딩), 레이아웃 시프트 방지(CLS 방지), 브라우저 호환성 최적화 등의 이점은 그대로 유지됩니다.
Q2. 타임스탬프 파일명을 고정 파일명으로 바꾸면 비용이 절감되나요?
A2. 아닙니다. 파일명을 고정하면 이미지 내용이 바뀌었을 때 브라우저나 CDN이 과거 캐시를 계속 보여주는 심각한 캐시 오염 문제가 발생합니다. 비용 절감을 위해서는 파일명이 아닌 next.config.ts 설정 및 업로드 시점의 원본 압축에 집중해야 합니다.
Q3. S3 고아 이미지 정리 시 정상 이미지가 지워질 위험은 없나요?
A3. 본문 내 전체 이미지 주소를 파싱한 뒤, 수정 후 최종 본문 데이터와 비교하여 완벽하게 참조가 끊어진 이미지(Old - New)만 차집합으로 골라내어 삭제하므로 정상 이미지가 삭제될 위험은 없습니다.