본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
12장 : 성능 최적화

이미지 최적화

next/image의 크기·우선순위·반응형 속성과 외부 호스트 설정으로 레이아웃 이동과 이미지 전송량을 줄입니다.

웹 애플리케이션의 성능 최적화는 사용자 경험과 SEO에 모두 영향을 주는 중요한 작업입니다.

특히 이미지 최적화는 로딩 속도 저하의 가장 흔한 원인 중 하나라 반드시 신경 써야 합니다.

Next.js는 내장 컴포넌트 next/image를 통해 고성능 이미지 처리를 쉽게 구현할 수 있게 해줍니다.

이 절에서는 이미지 최적화의 중요성부터 next/image 컴포넌트의 사용법, 그리고 이미지 최적화를 위한 다양한 기법들을 상세히 알아보겠습니다.


왜 이미지를 최적화해야 하나요?

이미지는 웹 페이지에서 시각적인 매력을 더하고 정보를 전달하는 데 필수적이지만, 동시에 페이지 로딩 속도를 늦추는 주범이 되기도 합니다.

  • 느린 로딩 속도: 용량이 큰 이미지는 네트워크를 통해 전송되는 데 시간이 오래 걸려 페이지 로딩을 지연시킵니다. 이는 사용자 이탈률 증가로 이어질 수 있습니다.
  • 데이터 사용량 증가: 모바일 환경에서는 사용자의 데이터 요금 부담을 가중시킬 수 있습니다.
  • Core Web Vitals 저하: Google의 Core Web Vitals 지표(Largest Contentful Paint, Cumulative Layout Shift 등)에 부정적인 영향을 미쳐 SEO 점수를 낮출 수 있습니다.
  • 불필요한 리소스 낭비: 사용자 기기의 성능에 맞지 않는 고해상도 이미지를 전송하는 것은 불필요한 리소스 낭비입니다.

이미지 최적화는 이러한 문제들을 해결하여 웹 성능을 크게 개선하는 데 기여합니다.


next/image 컴포넌트 사용하기

Next.js는 이미지 최적화를 위해 특별히 설계된 <Image> 컴포넌트를 next/image에서 제공합니다.

이 컴포넌트는 단순히 <img> 태그를 대체하는 것을 넘어, 빌드 시점과 요청 시점에 이미지 최적화를 자동으로 수행합니다.

주요 최적화 기능
  • 반응형 이미지: 기기 크기에 따라 최적화된 크기의 이미지를 자동으로 제공합니다.
  • 지연 로딩(Lazy Loading): 뷰포트(Viewport)에 들어올 때까지 이미지 로딩을 지연시켜 초기 로딩 속도를 향상시킵니다.
  • 이미지 형식 최적화: 기본 설정은 브라우저가 지원하면 WebP로 변환합니다. AVIF까지 제공하려면 images.formatsimage/avif를 추가합니다.
  • 이미지 품질 최적화: 지정된 품질 설정에 따라 이미지 압축을 수행합니다.
  • Cumulative Layout Shift (CLS) 방지: 이미지의 widthheight를 미리 정의하여 이미지 로딩 시 레이아웃이 밀리는 현상을 방지합니다.
  • 이미지 호스팅 최적화: Next.js 서버 또는 CDN을 통해 이미지를 효율적으로 제공합니다.

next/image 설치

Next.js 13 이상을 사용하는 App Router 프로젝트라면 별도의 설치 없이 바로 사용할 수 있습니다.

기본 사용법

<Image> 컴포넌트에는 src와 의미에 맞는 alt가 필요합니다.

public 문자열 경로나 원격 URL을 사용하면 widthheight로 비율을 알려야 하지만, 정적 import가 크기를 제공하거나 fill로 부모 영역을 채우는 경우에는 두 속성을 생략합니다.

src/app/page.tsx 또는 다른 컴포넌트 파일
import Image from 'next/image';
import styles from './page.module.css'; // CSS 모듈 사용 예시

export default function HomePage() {
  return (
    <div className={styles.container}>
      <h1>Next.js 이미지 최적화</h1>

      {/* 로컬 이미지 사용 */}
      <div className={styles.imageWrapper}>
        <h2>로컬 이미지</h2>
        <Image
          src="/logo.png"
          alt="Next.js 로고"
          width={500}
          height={300}
          placeholder="blur"
          blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
          style={{ maxWidth: '100%', height: 'auto' }}
        />
        <p>이 이미지는 Next.js 로고입니다.</p>
      </div>

      {/* 외부 이미지 사용 */}
      <div className={styles.imageWrapper}>
        <h2>외부 이미지</h2>
        <Image
          src="https://images.unsplash.com/photo-1617424699564-90f7a77e9b40?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
          alt="아름다운 자연 풍경"
          width={800}
          height={600}
          preload
          style={{ maxWidth: '100%', height: 'auto' }}
        />
        <p>이 이미지는 Unsplash에서 가져온 풍경 이미지입니다.</p>
      </div>

      {/* 레이아웃 채우기 (fill) 예시 */}
      <div className={styles.fillImageWrapper}>
        <h2>레이아웃 채우기 (fill)</h2>
        <div style={{ position: 'relative', width: '100%', height: '300px' }}>
          <Image
            src="https://images.unsplash.com/photo-1610214644596-f94d3f572a8c?q=80&w=2835&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
            alt="도시 야경"
            fill
            sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
            style={{ objectFit: 'cover' }}
          />
        </div>
        <p>이 이미지는 부모 요소의 공간을 채웁니다.</p>
      </div>
    </div>
  );
}

// src/app/page.module.css
/*
.container {
  padding: 2rem;
  max-width: 900px;
  margin: 0 auto;
  font-family: Arial, sans-serif;
}

.imageWrapper {
  margin-bottom: 3rem;
  border: 1px solid #ddd;
  padding: 1rem;
  text-align: center;
  background-color: #f9f9f9;
}

.imageWrapper h2 {
  margin-top: 0;
  color: #333;
}

.imageWrapper p {
  font-size: 0.9em;
  color: #666;
  margin-top: 1rem;
}

.fillImageWrapper {
  margin-bottom: 3rem;
  border: 1px solid #007bff;
  padding: 1rem;
  text-align: center;
  background-color: #eaf6ff;
}

.fillImageWrapper h2 {
  margin-top: 0;
  color: #007bff;
}

.fillImageWrapper p {
  font-size: 0.9em;
  color: #666;
  margin-top: 1rem;
}
*/

<Image> 컴포넌트의 주요 속성

  • src: 이미지 파일의 경로 (로컬 또는 외부 URL).
  • alt: 이미지에 대한 대체 텍스트. 접근성 및 SEO에 필수적입니다.
  • width, height: 이미지의 고유한 너비와 높이(픽셀). 정적 임포트나 fill을 사용하지 않는 경우 필수이며, 브라우저가 비율을 미리 확보해 CLS를 막도록 돕습니다.
  • preload: 실제 LCP 후보가 명확할 때 <Image preload />로 이미지 발견 시점을 앞당깁니다. Next.js 16에서는 기존 priority 대신 이 속성을 사용합니다. 일반적인 상단 이미지는 loading="eager"fetchPriority="high"가 더 적합할 수 있습니다.
  • fill: (불리언) 부모 요소의 크기에 맞춰 이미지를 채웁니다. 이 경우 widthheight 속성은 필요 없으며, 부모 요소에 position: relative 등의 스타일이 적용되어야 합니다.
  • sizes: fill을 사용하거나 CSS로 반응형 크기를 정할 때 실제 표시 폭을 브라우저에 알려 적절한 srcset 후보를 선택하게 합니다. (예: sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw")
  • quality: 이미지 압축 품질을 1에서 100 사이의 숫자로 지정합니다. Next.js 16은 next.configimages.qualities 허용 목록을 적용하며 기본 허용값은 75입니다. 다른 값을 사용하려면 허용 목록에도 추가합니다.
  • placeholder: 이미지가 로딩되는 동안 표시될 자리표시자.
    • blur: 작은 blurDataURL을 사용하여 흐릿한 이미지 효과를 보여줍니다. 지원 형식의 이미지를 정적으로 import하면 자동으로 적용됩니다.
    • empty: 투명한 이미지 자리표시자를 보여줍니다.
  • blurDataURL: placeholder="blur"와 함께 사용할 작은 Base64 인코딩 이미지 데이터 URL. import hero from "./hero.jpg"처럼 지원 형식의 이미지를 정적으로 import하면 Next.js가 자동 생성합니다. src="/logo.png" 같은 public 문자열 경로나 외부 이미지는 직접 제공해야 합니다.
  • loading: lazy (기본값, 지연 로딩) 또는 eager (즉시 로딩)를 지정합니다. preload와 동시에 지정하지 않습니다.
  • unoptimized: (불리언) Next.js의 이미지 최적화 기능을 사용하지 않고 순수 <img> 태그처럼 동작하게 합니다. 특수한 경우에만 사용합니다.

아래 보드는 실제 화면에 이미지를 배치할 때 width/height, fill, sizes, preload를 어떤 순서로 결정할지 정리한 것입니다.


아래 판단표는 next/image 속성을 의미, 자리, 전송량, 출처 순서로 결정하는 기준을 정리합니다.


외부 이미지 도메인 설정

외부 이미지 URL을 사용하는 경우, Next.js가 해당 이미지를 최적화할 수 있도록 next.config.ts에 프로토콜·호스트·경로를 구체적으로 허용합니다.

next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: 'www.example.com',
        pathname: '/images/**',
      },
    ],
  },
};

export default nextConfig;

next.config.ts 파일을 수정한 후에는 개발 서버를 다시 시작해야 변경 사항이 적용됩니다.


이미지 최적화를 위한 추가 팁

아래 다이어그램은 next/image를 사용할 때 이미지 출처, 크기, 우선순위, 접근성 속성을 함께 점검하는 기준입니다.

아래 다이어그램은 next/image의 크기 속성, preload, 외부 경로 설정이 이미지 최적화에 연결되는 지점을 보여줍니다.

  • 적절한 이미지 크기: 이미지를 업로드하기 전에 사용될 최대 크기에 맞춰 적절히 리사이즈하는 것이 좋습니다. next/image가 반응형 이미지를 처리하지만, 너무 큰 원본 이미지를 업로드하는 것은 여전히 비효율적일 수 있습니다.
  • 파일 형식 선택
    • JPEG: 사진과 같이 색상이 풍부한 이미지에 적합하며, 손실 압축을 통해 파일 크기를 줄일 수 있습니다.
    • PNG: 투명도가 필요한 이미지나 로고, 아이콘 등 색상 수가 적고 선명도가 중요한 이미지에 적합합니다. (비손실 압축)
    • WebP / AVIF: 최신 이미지 형식으로 JPEG나 PNG보다 효율적인 압축을 제공할 수 있습니다. next/image의 기본 출력 형식은 WebP이며, AVIF를 함께 사용하려면 next.configimages.formats['image/avif', 'image/webp']처럼 설정합니다.
    • SVG: 로고, 아이콘 등 벡터 기반의 그래픽에 적합합니다. 해상도에 구애받지 않고 확대/축소해도 깨지지 않으며, 파일 크기가 매우 작습니다. <Image> 컴포넌트가 아닌 일반 <img> 태그로 사용하거나 React 컴포넌트로 직접 임포트할 수 있습니다.
  • Placeholder 사용: placeholder="blur" 또는 placeholder="empty"를 사용하여 이미지가 로딩되는 동안 사용자 경험을 개선합니다. LCP 이미지가 명확하다면 preload와 함께 사용할 수 있지만, 작은 Base64 placeholder가 실제 전송 비용을 늘리지 않는지도 확인합니다.
  • 캐싱 전략: CDN(Content Delivery Network)을 사용하여 사용자에게 물리적으로 가장 가까운 서버에서 이미지를 제공하여 로딩 속도를 단축합니다. Next.js는 기본적으로 Vercel에 배포할 때 내장된 이미지 최적화 서비스를 활용합니다.
  • Lazy Loading 활용: 기본적으로 next/image는 지연 로딩을 사용하므로, 뷰포트 바깥의 이미지는 스크롤될 때까지 로딩되지 않습니다. 이는 초기 페이지 로딩 속도를 크게 개선합니다.

이미지 최적화는 단순히 이미지를 웹 페이지에 넣는 것을 넘어, 사용자 경험과 웹 성능을 극대화하기 위한 필수적인 과정입니다.

Next.js의 next/image 컴포넌트를 올바르게 활용하면 개발자가 복잡한 최적화 기법을 직접 구현할 필요 없이 뛰어난 이미지 성능을 얻을 수 있습니다.

아래 다이어그램은 이미지 최적화를 렌더링 비용, 전송 비용, 측정 지표 순서로 좁혀 봅니다.