Next.jsEasy#42
Next.js에서 이미지 최적화를 위해 next/image를 사용하는 이유는?
#Next.js#성능#이미지#최적화
힌트
자동 포맷 변환, 지연 로딩, 크기 최적화를 생각해보세요.
정답 및 해설
Next.js에서 이미지 최적화를 위해 next/image를 사용하는 이유는?
Next.js의 next/image 패키지가 제공하는 Image 컴포넌트는 웹 성능 최적화에 필수적인 이미지 처리를 자동으로 수행합니다. 단순한 <img> 태그 대신 Image 컴포넌트를 사용하면 별도의 설정 없이도 다양한 최적화 기법이 적용됩니다. Google의 Core Web Vitals 지표 개선에도 직접적인 영향을 미쳐 SEO와 사용자 경험을 동시에 향상시킵니다.
next/image의 핵심 최적화 기능
1. 현대적 이미지 포맷 자동 변환
브라우저가 지원하는 경우, PNG/JPEG 이미지를 WebP 또는 AVIF 포맷으로 자동 변환합니다.
| 포맷 | 압축률 | 브라우저 지원 |
|---|---|---|
| JPEG | 기준 | 모든 브라우저 |
| WebP | JPEG 대비 약 25~35% 용량 감소 | 모든 현대 브라우저 |
| AVIF | JPEG 대비 약 50% 용량 감소 | Chrome, Firefox (최신) |
// 단순히 Image 컴포넌트를 사용하면 자동으로 WebP/AVIF 변환됨
import Image from 'next/image'
function ProductCard() {
return (
<Image
src="/images/product.png" // PNG 업로드
alt="상품 이미지"
width={400}
height={300}
// 브라우저가 지원하면 자동으로 WebP or AVIF로 제공됨
/>
)
}
2. Responsive 이미지 (srcset 자동 생성)
디바이스의 화면 크기에 맞는 적절한 해상도의 이미지를 자동으로 제공합니다.
// fill 속성 사용 - 부모 컨테이너 크기에 맞춤
function HeroBanner() {
return (
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image
src="/images/hero.jpg"
alt="히어로 배너"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
style={{ objectFit: 'cover' }}
/>
</div>
)
}
// 생성되는 HTML (자동으로 srcset 생성됨):
// <img
// srcset="/_next/image?url=...&w=640&q=75 640w,
// /_next/image?url=...&w=750&q=75 750w,
// /_next/image?url=...&w=1080&q=75 1080w,
// /_next/image?url=...&w=1920&q=75 1920w"
// sizes="(max-width: 768px) 100vw, ..."
// />
// sizes prop으로 렌더링 크기를 브라우저에 힌트 제공
function ProductGrid() {
return (
<div className="grid grid-cols-3">
{products.map((product) => (
<Image
key={product.id}
src={product.image}
alt={product.name}
width={400}
height={400}
// 화면 크기별로 이미지가 차지하는 비율 명시
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
))}
</div>
)
}
3. Lazy Loading (지연 로딩)
뷰포트에 진입할 때만 이미지를 로드하여 초기 페이지 로드 속도를 향상시킵니다.
// 기본적으로 lazy loading 적용됨
function ArticleList() {
return (
<div>
{articles.map((article) => (
<div key={article.id}>
<h2>{article.title}</h2>
{/* 스크롤하여 뷰포트에 진입할 때 로드됨 */}
<Image
src={article.thumbnail}
alt={article.title}
width={800}
height={450}
// loading="lazy" 가 기본값
/>
</div>
))}
</div>
)
}
// Above-the-fold 이미지는 priority 속성 사용
function HeroSection() {
return (
<Image
src="/hero.jpg"
alt="메인 히어로"
width={1200}
height={600}
priority // 즉시 로드 (LCP 개선에 중요)
/>
)
}
4. CLS(레이아웃 시프트) 방지
이미지가 로드되기 전에 공간을 미리 확보하여 레이아웃 시프트를 방지합니다.
// width, height 명시 → 브라우저가 미리 공간 확보 (CLS 방지)
function BlogPost() {
return (
<Image
src="/blog-thumbnail.jpg"
alt="블로그 썸네일"
width={800} // 브라우저가 로드 전에 800x450 공간 확보
height={450}
// 내부적으로 aspect-ratio: 800/450 CSS 적용됨
/>
)
}
// placeholder로 로딩 중 블러 효과 추가
import profilePic from '../public/profile.jpg' // 로컬 이미지
function Profile() {
return (
<Image
src={profilePic}
alt="프로필 사진"
placeholder="blur" // 로딩 중 블러 이미지 표시 (CLS 방지 + UX 개선)
// 로컬 이미지는 자동으로 blurDataURL 생성됨
/>
)
}
// 원격 이미지는 blurDataURL 직접 제공
function RemoteImage() {
return (
<Image
src="https://example.com/image.jpg"
alt="원격 이미지"
width={400}
height={300}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQ..." // 저해상도 base64
/>
)
}
5. 이미지 캐싱 및 CDN 최적화
// next.config.js - 이미지 최적화 설정
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
// 허용할 외부 도메인 설정
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
{
protocol: 'https',
hostname: '**.amazonaws.com',
},
],
// 커스텀 이미지 크기 설정 (srcset 생성에 사용)
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// 최소 캐시 TTL (초)
minimumCacheTTL: 60,
// 지원 포맷
formats: ['image/avif', 'image/webp'],
},
}
일반 img 태그와의 비교
// ❌ 일반 img 태그 사용
function OldWay() {
return (
<img
src="/large-image.png" // 원본 그대로 전송 (수 MB 가능)
alt="이미지"
// 포맷 변환 없음
// lazy loading 기본 미적용 (브라우저마다 다름)
// CLS 방지 없음 (width/height 없으면)
// 캐싱 최적화 없음
/>
)
}
// ✅ next/image 사용
import Image from 'next/image'
function NewWay() {
return (
<Image
src="/large-image.png" // 자동으로 최적화됨
alt="이미지"
width={800}
height={600}
// WebP/AVIF 자동 변환
// 뷰포트 진입 시 lazy loading
// CLS 방지를 위한 공간 예약
// /_next/image API로 캐싱 최적화
/>
)
}
실전 사용 패턴
아바타 이미지
function UserAvatar({ user }: { user: User }) {
return (
<div className="relative w-10 h-10 rounded-full overflow-hidden">
<Image
src={user.avatarUrl || '/default-avatar.png'}
alt={`${user.name} 프로필`}
fill
sizes="40px"
className="object-cover"
/>
</div>
)
}
오픈 그래프 이미지
// app/og/route.tsx - 동적 OG 이미지 생성 (next/og)
import { ImageResponse } from 'next/og'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const title = searchParams.get('title')
return new ImageResponse(
(
<div style={{ display: 'flex', background: '#fff', padding: 40 }}>
<h1>{title}</h1>
</div>
),
{ width: 1200, height: 630 }
)
}
갤러리 컴포넌트
'use client'
import Image from 'next/image'
import { useState } from 'react'
function ImageGallery({ images }: { images: string[] }) {
const [selected, setSelected] = useState(0)
return (
<div>
{/* 메인 이미지 - priority로 빠르게 로드 */}
<div className="relative h-96">
<Image
src={images[selected]}
alt="선택된 이미지"
fill
sizes="(max-width: 768px) 100vw, 800px"
className="object-contain"
priority
/>
</div>
{/* 썸네일 - lazy loading */}
<div className="flex gap-2 mt-4">
{images.map((img, i) => (
<button key={i} onClick={() => setSelected(i)}>
<Image
src={img}
alt={`썸네일 ${i + 1}`}
width={80}
height={80}
className="object-cover"
/>
</button>
))}
</div>
</div>
)
}
성능 개선 효과 수치
| 최적화 항목 | 일반 img 태그 | next/image |
|---|---|---|
| 파일 크기 | 원본 크기 | WebP: ~30% 감소, AVIF: ~50% 감소 |
| 초기 로드 | 모든 이미지 로드 | 뷰포트 이미지만 로드 |
| CLS 점수 | 이미지 크기 미명시 시 높음 | 공간 예약으로 최소화 |
| 캐싱 | 브라우저 캐시만 | CDN + 서버 캐싱 |
| 반응형 | 수동 구현 필요 | srcset 자동 생성 |
정리 표
| 기능 | 설명 | 관련 Core Web Vital |
|---|---|---|
| 현대적 포맷 변환 | PNG/JPEG → WebP/AVIF 자동 변환 | LCP (로드 속도 향상) |
| Responsive 이미지 | 디바이스에 맞는 크기의 이미지 제공 | LCP (불필요한 데이터 감소) |
| Lazy Loading | 뷰포트 진입 시 로드 | LCP (초기 로드 최소화) |
| CLS 방지 | width/height로 공간 미리 확보 | CLS (레이아웃 시프트 방지) |
| placeholder blur | 로딩 중 블러 이미지 표시 | CLS + UX 개선 |
| priority | 중요 이미지 즉시 로드 | LCP (히어로 이미지 최적화) |
| CDN 캐싱 | /_next/image API 통한 캐싱 | LCP (재방문 시 빠른 로드) |