전체 목록
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기준모든 브라우저
WebPJPEG 대비 약 25~35% 용량 감소모든 현대 브라우저
AVIFJPEG 대비 약 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 (재방문 시 빠른 로드)