이 글은 누구를 위한 것인가
- LCP(Largest Contentful Paint)를 개선하려는 팀
- Next.js
<Image>컴포넌트를 제대로 활용하고 싶은 개발자 - AVIF/WebP 변환으로 이미지 크기를 줄이고 싶은 팀
들어가며
이미지는 대부분 웹페이지에서 가장 무거운 리소스다. AVIF는 JPEG 대비 50% 작고, Next.js <Image>는 자동으로 포맷 변환, 크기 최적화, lazy loading을 처리한다. 올바르게 설정하면 LCP를 0.5-1초 단축할 수 있다.
이 글은 bluefoxdev.kr의 웹 이미지 최적화 가이드 를 참고하여 작성했습니다.
1. 이미지 포맷 선택 가이드
[포맷 비교 (동일 품질 기준)]
AVIF:
압축률: PNG 대비 80%, JPEG 대비 50%
브라우저: Chrome 85+, Firefox 93+, Safari 16+
인코딩: 느림 (서버 부하 있음)
적합: 사진, 복잡한 이미지
WebP:
압축률: PNG 대비 30%, JPEG 대비 25-35%
브라우저: 모든 현대 브라우저
인코딩: 빠름
적합: 사진, 일러스트 모두
PNG:
압축: 무손실
투명: 지원
적합: 아이콘, 스크린샷, 텍스트 포함 이미지
SVG:
크기: 매우 작음 (벡터)
확대: 무제한 품질
적합: 로고, 아이콘, 다이어그램
[Next.js Image 자동 최적화]
요청 이미지 → 브라우저 지원 포맷으로 변환
AVIF 지원: AVIF 서빙
WebP 지원: WebP 서빙
나머지: 원본 포맷
크기: 요청된 sizes에 맞게 리사이즈
캐싱: 생성된 이미지 캐시 (Vercel Edge Cache)
2. Next.js Image 최적화 구현
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
// 외부 이미지 도메인 허용
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
pathname: '/products/**',
},
{
protocol: 'https',
hostname: '**.cloudinary.com',
},
],
// 지원 포맷 (우선순위 순)
formats: ['image/avif', 'image/webp'],
// 이미지 크기 힌트 (srcset 생성에 사용)
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// 최적화 품질
minimumCacheTTL: 60 * 60 * 24 * 7, // 7일
},
};
export default nextConfig;
// 히어로 이미지 - LCP 최적화
import Image from 'next/image';
export function HeroSection() {
return (
<section className="relative h-screen">
<Image
src="/hero.jpg"
alt="쇼핑몰 메인 배너 - 봄 신상품 컬렉션"
fill // 부모 기준 채우기
sizes="100vw" // 뷰포트 전체 너비
priority // LCP: preload 적용
quality={85} // 품질 (기본 75)
className="object-cover"
placeholder="blur" // 블러 플레이스홀더
blurDataURL="data:image/jpeg;base64,/9j..." // 작은 블러 이미지
/>
<div className="absolute inset-0 flex items-center justify-center">
<h1 className="text-white text-5xl font-bold">봄 신상품</h1>
</div>
</section>
);
}
// 상품 카드 그리드 - 반응형 이미지
export function ProductCard({ product }: { product: Product }) {
return (
<div className="relative">
<Image
src={product.imageUrl}
alt={product.name}
width={400}
height={400}
// sizes: 각 미디어 쿼리에서 이미지가 차지하는 너비 명시
// → 브라우저가 올바른 srcset 선택
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
className="rounded-lg object-cover"
// lazy loading (기본값, 뷰포트 밖 이미지)
loading="lazy"
/>
</div>
);
}
// 썸네일 - 작은 고정 크기
export function Avatar({ user }: { user: User }) {
return (
<Image
src={user.avatarUrl ?? '/default-avatar.png'}
alt={`${user.name} 프로필`}
width={40}
height={40}
// 고정 크기 → sizes 불필요
className="rounded-full"
/>
);
}
// 동적 블러 플레이스홀더 생성
import { getPlaiceholder } from 'plaiceholder';
async function getImageProps(src: string) {
const buffer = await fetch(src).then(r => r.arrayBuffer());
const { base64 } = await getPlaiceholder(Buffer.from(buffer));
return { src, blurDataURL: base64, placeholder: 'blur' as const };
}
// 서버 컴포넌트에서 사용
export async function ProductImage({ src, alt }: { src: string; alt: string }) {
const imageProps = await getImageProps(src);
return (
<Image
{...imageProps}
alt={alt}
width={800}
height={600}
sizes="(max-width: 768px) 100vw, 50vw"
/>
);
}
// 커스텀 이미지 로더 (Cloudinary, Imgix 등)
const cloudinaryLoader = ({
src,
width,
quality,
}: {
src: string;
width: number;
quality?: number;
}) => {
const params = [
'f_auto', // 자동 포맷 (AVIF/WebP)
'c_limit', // 최대 크기 제한
`w_${width}`, // 너비
`q_${quality || 75}`, // 품질
].join(',');
return `https://res.cloudinary.com/mycloud/image/upload/${params}/${src}`;
};
// next.config.ts
// images: { loader: 'custom', loaderFile: './src/utils/cloudinary-loader.ts' }
// 사용
<Image
loader={cloudinaryLoader}
src="products/shoe-001.jpg"
alt="상품 이미지"
width={400}
height={400}
sizes="(max-width: 768px) 100vw, 400px"
/>
// 이미지 최적화 분석 유틸리티
export function analyzeImageUsage() {
// 개발 환경에서 sizes 누락 감지
if (process.env.NODE_ENV !== 'development') return;
const images = document.querySelectorAll('img');
images.forEach(img => {
if (!img.sizes && img.naturalWidth > 500) {
console.warn(
`큰 이미지에 sizes 속성이 없습니다: ${img.src}\n`,
'예시: sizes="(max-width: 768px) 100vw, 50vw"'
);
}
});
}
interface Product { name: string; imageUrl: string }
interface User { name: string; avatarUrl: string | null }
마무리
Next.js <Image>에서 가장 자주 실수하는 것은 sizes 속성 누락이다. sizes 없으면 브라우저가 100vw로 가정해 불필요하게 큰 이미지를 다운로드한다. LCP 이미지에는 반드시 priority를 추가해 preload를 트리거한다. 외부 이미지 소스(Cloudinary, Imgix)를 쓸 때는 커스텀 로더로 자동 AVIF 변환을 활성화하면 별도 Next.js 서버 최적화 없이도 최소 크기를 유지할 수 있다.