Next.js Image 최적화 정리 Next.js 15+ 기준
1. Next.js Image 최적화의 동작 원리
빌드 타임이 아닌 런타임 최적화
Next.js Image가 빌드 타임에 .next 폴더에 webp 이미지를 미리 생성해둔다고 생각하는 경우가 있습니다.
그게 바로 저입니다... 실제로는 런타임에 on-demand로 최적화가 이루어 진다고 하네요.
요청 흐름:
1. 브라우저 → /_next/image?url=...&w=800&q=75 요청
2. Next.js 서버 → 원본 이미지 fetch
3. Next.js 서버 → 리사이즈 + webp/avif 변환
4. .next/cache/images/ 에 캐시 저장
5. 이후 동일 요청 → 캐시에서 즉시 응답
동작 방식
- 동적 src도 최적화 가능: 런타임 처리이므로 동적 이미지 URL도 혜택을 받음
- 정적 페이지 필수 아님: SSR, ISR, CSR 모두에서 동작
- 캐시 기반: 첫 요청만 느리고, 이후는 캐시 히트로 빠름
2. 제가 오해한 부분
오해: 이미 webp면 최적화를 건너뛴다
아님.
Next.js는 원본 포맷과 관계없이 무조건 최적화 거침.
이미 최적화된 webp 이미지도 재처리됩니다.
오해: new Image()로 프리로드하면 Next Image도 빨라진다
아님.
URL이 다르기 때문에 브라우저 캐시가 공유되지 않습니다.
프리로드 URL: https://example.com/photo.jpg
Next Image URL: /_next/image?url=https://example.com/photo.jpg&w=800&q=75
3. 상황별 최적화 전략
케이스 1: /public에 이미 최적화된 이미지
증상: webp로 직접 최적화한 이미지인데 Next Image가 오히려 느림
원인: 불필요한 재처리 오버헤드
해결: unoptimized={true} 사용
// 이미 최적화된 로컬 이미지
<Image
src="/images/optimized-hero.webp"
alt="Hero"
width={1200}
height={600}
unoptimized
/>
케이스 2: 외부 이미지 (CDN, 사용자 업로드 등)
증상: 첫 로드가 느리고, 두 번째부터 빠름
원인: 정상 동작. 첫 요청 시 서버에서 최적화 작업 수행
해결: 서버 캐시 워밍업 (아래 섹션 참고)
케이스 3: LCP (Largest Contentful Paint) 이미지
해결: priority prop 필수
<Image
src={heroImage}
alt="Hero"
priority // preload 힌트 + fetchpriority="high"
/>
상황별 알아서 분기하는 래퍼 컴포넌트
상황별 자동 분기 처리:
import Image, { ImageProps } from 'next/image';
interface SmartImageProps extends Omit<ImageProps, 'unoptimized'> {
src: string;
}
export function SmartImage({ src, width, ...props }: SmartImageProps) {
const isAlreadyOptimized =
src.endsWith('.webp') ||
src.endsWith('.avif') ||
src.includes('imagecdn.com') ||
src.includes('cloudinary.com');
const isSmallImage = typeof width === 'number' && width < 100;
const isSvg = src.endsWith('.svg');
const skipOptimization = isAlreadyOptimized || isSmallImage || isSvg;
return (
<Image
src={src}
width={width}
unoptimized={skipOptimization}
{...props}
/>
);
}
4. 서버 캐시 워밍업으로 첫 방문자도 빠르게
문제 상황
외부 이미지의 경우 첫 방문자는 항상 최적화 대기 시간을 겪습니다.
브라우저 캐시는 같은 유저의 재방문에만 효과가 있고,
서버 캐시가 비어있으면 모든 신규 방문자가 느린 첫 로드를 경험합니다.
해결: 배포 후 캐시 워밍업
워밍업 스크립트
// scripts/warmup-images.ts
const BASE_URL = process.env.SITE_URL || 'http://localhost:3000';
// 워밍업할 이미지 목록
const IMAGES_TO_WARM = [
'https://external-cdn.com/hero-image.jpg',
'https://external-cdn.com/product-1.jpg',
'https://external-cdn.com/product-2.jpg',
// 동적으로 가져올 수도 있음
];
// Next.js 기본 deviceSizes + imageSizes
const WIDTHS = [640, 750, 828, 1080, 1200, 1920];
const QUALITY = 75;
async function warmupImage(src: string, width: number): Promise<void> {
const url = `${BASE_URL}/_next/image?url=${encodeURIComponent(src)}&w=${width}&q=${QUALITY}`;
try {
const response = await fetch(url);
if (response.ok) {
console.log(`✓ Warmed: ${src} @ ${width}w`);
} else {
console.error(`✗ Failed: ${src} @ ${width}w - ${response.status}`);
}
} catch (error) {
console.error(`✗ Error: ${src} @ ${width}w -`, error);
}
}
async function warmup(): Promise<void> {
console.log('🔥 Starting image cache warmup...\n');
const tasks: Promise<void>[] = [];
for (const src of IMAGES_TO_WARM) {
for (const width of WIDTHS) {
tasks.push(warmupImage(src, width));
}
}
// 동시 요청 제한 (서버 부하 방지)
const CONCURRENCY = 5;
for (let i = 0; i < tasks.length; i += CONCURRENCY) {
await Promise.all(tasks.slice(i, i + CONCURRENCY));
}
console.log('\n✅ Warmup complete!');
}
warmup();
실행 방법
# 로컬 테스트
pnpm build && pnpm start &
sleep 5 # 서버 시작 대기
npx tsx scripts/warmup-images.ts
# 또는 package.json에 추가
{
"scripts": {
"warmup": "tsx scripts/warmup-images.ts",
"start:warmed": "next start & sleep 5 && npm run warmup"
}
}
5. 배포 환경별 설정
Vercel
Vercel은 이미지 최적화가 Edge에서 자동 처리되고 글로벌 CDN 캐시가 포함됨.
별도 설정 없이 최적의 성능을 얻을 수 있음.
필요한 경우 수동으로 배포시 워밍업 로직을 실행하게 할 수는 있음...
GitHub Actions
# .github/workflows/warmup.yml
name: Image Cache Warmup
on:
deployment_status:
jobs:
warmup:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm install
- name: Wait for deployment to stabilize
run: sleep 30
- name: Run warmup script
env:
SITE_URL: ${{ github.event.deployment_status.target_url }}
run: npx tsx scripts/warmup-images.ts
Docker (Cloud Run, ECS, etc.)
컨테이너 환경에서는 추가 고려사항이 있습니다.
1. 캐시 영속성 문제
컨테이너 재시작 시 .next/cache/images/ 캐시가 사라집니다.
해결: 볼륨 마운트 또는 외부 캐시
# docker-compose.yml
services:
web:
image: your-nextjs-app
volumes:
- image-cache:/app/.next/cache/images
volumes:
image-cache:
2. 스케일아웃 시 캐시 분산
여러 인스턴스가 각자 캐시를 갖게 됩니다.
해결: CDN을 앞에 배치
사용자 → CloudFront/Cloud CDN → Container
(/_next/image/* 캐시)
3. Dockerfile에 워밍업 포함
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
COPY scripts/warmup-images.ts ./scripts/
# 헬스체크 + 워밍업 엔트리포인트
COPY docker-entrypoint.sh ./
RUN chmod +x docker-entrypoint.sh
EXPOSE 3000
ENTRYPOINT ["./docker-entrypoint.sh"]
#!/bin/bash
# docker-entrypoint.sh
# Next.js 서버 시작 (백그라운드)
node server.js &
# 서버 준비 대기
until curl -s http://localhost:3000/api/health > /dev/null; do
sleep 1
done
# 캐시 워밍업
npx tsx scripts/warmup-images.ts
# 포그라운드로 전환
wait
외부 이미지 최적화 서비스 활용
Next.js 내장 최적화 대신 전문 서비스를 사용하면 인프라 복잡도를 줄일 수 있다는데,, 아직 해보질 못했네
// next.config.ts
import type { NextConfig } from 'next';
const config: NextConfig = {
images: {
loader: 'custom',
loaderFile: './lib/image-loader.ts',
},
};
export default config;
// lib/image-loader.ts
interface ImageLoaderParams {
src: string;
width: number;
quality?: number;
}
export default function cloudinaryLoader({
src,
width,
quality = 75
}: ImageLoaderParams): string {
// Cloudinary 예시
const params = [
'f_auto',
'c_limit',
`w_${width}`,
`q_${quality}`,
];
return `https://res.cloudinary.com/your-cloud/image/fetch/${params.join(',')}/${encodeURIComponent(src)}`;
}
정리: 상황별 체크리스트
| 상황 | 권장 설정 |
|---|---|
/public 폴더의 이미 최적화된 이미지 |
unoptimized={true} |
| 외부 이미지, 성능 중요 | 서버 캐시 워밍업 적용 |
| LCP 이미지 (Hero, 메인 배너 등) | priority prop 추가 |
| 아이콘, 작은 이미지 (< 100px) | unoptimized={true} |
| SVG 이미지 | unoptimized={true} |
| Vercel 배포 | GitHub Actions 워밍업 |
| Docker 배포 | CDN + 볼륨 마운트 + 엔트리포인트 워밍업 |
참고 자료
'TIL' 카테고리의 다른 글
| [251124 TIL] 간접 의존성 관련 업데이트 (0) | 2025.11.24 |
|---|---|
| [251123 TIL] Turbopack 간접 의존성 경고 상황 (0) | 2025.11.23 |
| [251123 TIL] OpenTelemetry? (with Next.js) (0) | 2025.11.23 |
| [251123 TIL] Next.js instrumentation.ts 정리 (0) | 2025.11.23 |
| [251102 TIL] PostgREST vs GraphQL(Hasura)기술 스택 비교 (0) | 2025.11.02 |