
바이브 코더를 위한 Next.js 최적화 (1/3): 이미지 최적화
Next.js Image 컴포넌트가 해결하는 네 가지 문제와 Vercel 비용 구조, 자체 아키텍처 설계까지 정리했어요.
Introduction
AI로 프로젝트를 만들었는데 Lighthouse 점수가 40점대라면, 원인은 높은 확률로 이미지예요.
5MB짜리 JPEG를 그대로 올리면 모바일에서는 로딩에 수 초가 걸리고, 포맷이 낡으면 같은 품질에서도 용량이 2~3배 차이 나죠. 지연 로딩(Lazy Loading)이 없으면 화면 밖 이미지까지 한꺼번에 받아오고, 크기 정보가 빠지면 레이아웃이 갑자기 밀려나는 CLS(Cumulative Layout Shift, 누적 레이아웃 이동)가 발생해요.
정리하면 이미지에는 네 가지 문제가 있어요.
- 용량: 원본 그대로 서빙하면 불필요하게 큰 파일을 전송해요.
- 포맷: PNG/JPEG보다 WebP/AVIF가 같은 품질에서 훨씬 가벼워요.
- 지연 로딩(Lazy Loading): 뷰포트 밖 이미지를 미리 로드하면 초기 로딩이 느려져요.
- CLS: 이미지 크기를 브라우저가 모르면 로드 후 레이아웃이 흔들려요.
Next.js의 <Image> 컴포넌트는 이 네 가지를 한 번에 처리해줘요.
이 글의 목표
바이브 코딩으로 만든 프로젝트에서 이미지 최적화가 왜 필요한지 이해하고, Next.js Image 컴포넌트의 설정과 비용 구조까지 파악하는 것이 목표예요.
이 글은 3부작 시리즈의 첫 번째 편이에요.
1편: 이미지 최적화 (현재 글)
img vs Image, next.config.js, 비용 구조, 아키텍처
2편: 렌더링 전략
CSR, SSR, SSG, ISR, PPR 비교와 선택 기준
3편: 코드 분할과 캐싱
dynamic import, Script 최적화, unstable_cache
<img> vs <Image> 비교
HTML의 <img> 태그는 이미지를 있는 그대로 보여줘요. 리사이징, 포맷 변환, Lazy Loading 같은 최적화는 개발자가 직접 처리해야 하죠.
Next.js의 <Image> 컴포넌트는 이 작업을 빌드 타임과 서빙 시점에 자동으로 처리해줘요.
export default function BadPage() {
return (
<div>
<h1>일반 이미지 태그</h1>
<img
src="/assets/large-photo.jpg"
alt="큰 이미지"
style={{ width: '100%', height: 'auto' }}
/>
</div>
);
}로컬 이미지를 import로 가져오면 width, height, placeholder="blur"까지 자동으로 처리돼요. 외부 이미지는 빌드 타임에 Next.js가 파일 정보를 알 수 없기 때문에 width와 height를 직접 지정해야 해요.
Image 컴포넌트가 해결하는 4가지
- 디바이스 크기에 맞는 자동 리사이징: 5MB 원본도 뷰포트에 맞춰 30KB 수준으로 줄여줘요.
- WebP/AVIF 자동 변환: 브라우저가 지원하는 최적의 포맷으로 서빙해요. AVIF는 JPEG 대비 50% 이상, WebP는 25-35% 용량을 줄여줘요.
- 뷰포트 진입 시 Lazy Loading: 화면에 보일 때만 이미지를 로드해요.
- width/height 기반 CLS 방지: 이미지 공간을 미리 확보해서 레이아웃이 밀리지 않아요.
next.config.js 설정
외부 도메인의 이미지를 <Image> 컴포넌트에서 사용하려면 remotePatterns를 설정해야 해요. Next.js는 보안을 위해 기본적으로 외부 이미지를 차단하기 때문이에요.
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 's3.amazonaws.com',
port: '',
pathname: '/my-bucket/**',
},
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
],
},
};
module.exports = nextConfig;protocol, hostname, pathname을 조합해서 허용할 외부 이미지의 범위를 지정해요. 와일드카드(**)를 사용하면 하위 경로 전체를 허용할 수 있어요.
AI가 생성한 외부 이미지에 주의하세요
AI가 Unsplash, Pexels 같은 외부 이미지를 코드에 넣어줄 때가 있어요. remotePatterns에 해당 도메인이 등록돼 있지 않으면 이미지가 차단됩니다. 빌드 에러 없이 이미지만 안 뜨는 경우가 많으니, 외부 이미지를 사용할 때는 설정을 먼저 확인하세요.
비용 구조
Vercel에 배포한다면 이미지 최적화 비용을 알아둘 필요가 있어요. <Image> 컴포넌트가 서빙하는 이미지는 Vercel의 이미지 최적화 API(Image Optimization API)를 거치는데, 이 API 호출에 비용이 발생해요.
| 항목 | Hobby (무료) | Pro |
|---|---|---|
| Image Transformations | 월 5,000건 | 1,000건당 $0.05 |
| Image Cache Reads | 월 300,000건 | 1M건당 $0.40 |
| Image Cache Writes | 월 100,000건 | 1M건당 $4.00 |
Image Transformation(이미지 변환)은 원본을 변환하는 작업이고, Cache Write(캐시 쓰기)는 변환 결과를 캐시에 저장하는 작업이에요. Cache Read(캐시 읽기)는 이미 캐시된 이미지를 서빙할 때 발생하죠. Cache Write가 Cache Read보다 10배 비싸기 때문에, 같은 이미지를 반복 변환하지 않는 게 중요해요.
캐시 TTL을 넉넉하게 설정하세요
이미지 URL이 고유하다면(UUID 기반 파일명 등) minimumCacheTTL 값을 길게 잡는 것을 권장해요. 캐시가 만료되면 동일한 이미지도 다시 변환하면서 Cache Write 비용이 발생하기 때문이에요.
const nextConfig = {
images: {
minimumCacheTTL: 2592000, // 30일 (초 단위)
},
};이미지 최적화 아키텍처
Vercel에 배포하면 이미지 최적화가 내장돼 있지만, 다른 플랫폼(AWS, GCP 등)에 배포한다면 직접 구축해야 해요. 전형적인 아키텍처는 CloudFront + S3 + Lambda 조합이에요.

전체 흐름을 단계별로 정리하면 이렇게 돼요.
- 사용자가 이미지를 요청해요. URL 쿼리 파라미터(Query Parameter)에 원하는 포맷(
webp,avif)과 너비(w=640)를 지정하죠. - CloudFront에서 캐시를 확인해요. 캐시에 있으면 바로 반환하고, 여기서 대부분의 요청이 처리돼요.
- 캐시 미스가 발생하면 S3에서 변환된 이미지를 확인해요. 이전에 변환해둔 결과가 있을 수 있기 때문이에요.
- S3에도 없으면 Lambda 함수가 원본 이미지를 가져와서 리사이징하고 포맷을 변환해요. 변환 결과를 S3에 저장한 뒤 CloudFront로 서빙하죠.
핵심은 "한 번 변환하고 계속 캐시에서 서빙한다"는 점이에요. Vercel의 Image Optimization API도 내부적으로 비슷한 구조로 동작해요. 차이가 있다면 Vercel은 이 모든 과정을 설정 없이 <Image> 컴포넌트만으로 제공한다는 것이죠.
자체 인프라를 운영하면 비용 통제가 유연해지는 대신 운영 부담이 생겨요. 트래픽 규모와 팀 역량에 따라 판단하면 돼요.
AI로 이미지 최적화 점검하기
바이브 코딩으로 프로젝트를 만들었다면, AI에게 이미지 최적화 상태를 점검해달라고 요청할 수 있어요. 아래 프롬프트를 그대로 사용하면 돼요.
내 Next.js 프로젝트의 이미지 최적화 상태를 점검해줘.
점검 항목:
1. next.config.js의 remotePatterns 설정이 보안상 안전한가?
2. <Image /> 컴포넌트에 width, height가 CLS를 방지하도록 명시돼 있는가?
3. LCP 요소에 priority 속성이 적절히 부여돼 있는가?
4. placeholder 처리가 돼 있는가?
각 항목별로 현재 상태와 개선 코드를 제시해줘.LCP(Largest Contentful Paint, 최대 콘텐츠풀 페인트) 요소는 보통 페이지에서 가장 큰 이미지예요. 히어로 배너나 메인 이미지가 해당되는 경우가 많은데, 이런 이미지에는 priority 속성을 추가해서 Lazy Loading을 비활성화하고 즉시 로드하도록 해야 해요.
<Image
src={heroImage}
alt="히어로 배너"
priority
/>priority를 지정하면 <head>에 <link rel="preload">가 자동으로 추가돼요. 사용자가 처음 보는 화면의 핵심 이미지가 빠르게 표시되도록 도와주죠.
TL;DR
- HTML
<img>대신 Next.js<Image>컴포넌트를 쓰면 리사이징, 포맷 변환, Lazy Loading, CLS 방지가 자동으로 처리돼요. - 외부 이미지를 사용할 때는
next.config.js의remotePatterns에 도메인을 등록해야 해요. - Vercel 배포 시 Cache Write 비용이 가장 비싸니,
minimumCacheTTL을 넉넉하게 설정해서 불필요한 재변환을 줄이세요. - LCP에 해당하는 이미지에는
priority속성을 추가해서 즉시 로드되도록 하세요.
참고 자료
- 원본 영상: 바이브 코더를 위한 웹사이트 최적화 — 코드팩토리 YouTube
- Next.js Image 공식 문서 — Next.js Documentation
- Vercel Image Optimization 가격 정책 — 최신 가격과 사용량 한도
- Next.js Image 실측 성능 분석 — DebugBear, 포맷별 압축률과 LCP 벤치마크