테마 전환

Next.js 이미지 최적화 완전 정복: Image 컴포넌트의 올바른 사용법

Easton editorial illustration: performance inspection lens

Lighthouse 테스트 점수는 62점이었습니다.

2주 동안 만든 Next.js 프로젝트의 성능 점수가 겨우 합격선이라니 당황스러웠습니다. Performance를 열어 보니 가장 큰 문제가 바로 드러났습니다. LCP(Largest Contentful Paint)가 4.8초였고, 전부 이미지가 발목을 잡고 있었습니다.

처음에는 어리둥절했습니다. 분명 Next.js를 사용했는데, 프레임워크가 자체적으로 최적화해 주는 것 아니었나요? 코드를 확인해 보니 홈페이지의 대형 Hero 이미지가 아직도 가장 기본적인 <img> 태그를 쓰고 있었습니다. 전자상거래 목록 페이지의 수십 장 상품 이미지도 서버의 원본 이미지를 그대로 불러왔고, 한 장에 3~4MB나 됐습니다.

나중에야 Next.js가 강력하더라도 이미지 최적화를 사용하려면 Image 컴포넌트를 직접 적용해야 한다는 사실을 알았습니다. 제대로 사용하면 이미지 용량을 60~80% 줄이고, LCP를 4초에서 2초 이내로 낮출 수 있습니다.

문제는 저처럼 Image 컴포넌트를 사용하고도 여러 함정에 빠지는 사람이 많다는 점입니다. 원격 이미지에서 “Un-configured Host” 오류가 나거나, 페이지를 불러올 때 이미지 영역이 계속 움직이거나, 수많은 설정 항목을 어떻게 써야 할지 모르는 경우가 많습니다. 이 글에서는 제가 겪은 문제와 해결책을 모두 정리해, 처음부터 Next.js Image 컴포넌트를 올바르게 사용하는 방법을 설명하겠습니다.

Next.js Image 컴포넌트를 사용해야 하는 이유

일반 img 태그의 세 가지 함정

이미지는 <img src="xxx"> 하나면 되는 것 아닌가 생각할 수 있습니다. 저도 처음에는 그랬습니다. 하지만 성능 테스트를 해 보니 문제가 훨씬 컸습니다.

첫 번째 함정: 형식을 최적화하지 않아 대역폭을 낭비합니다

일반 img 태그는 지정한 형식을 그대로 표시합니다. 3MB PNG를 올리면 사용자는 그대로 3MB를 내려받습니다. 요즘 브라우저는 대부분 WebP를 지원하며 JPEG보다 용량을 30% 줄일 수 있고, AVIF를 지원하는 브라우저에서는 다시 40%를 절약할 수 있습니다. 하지만 img 태그는 이를 신경 쓰지 않고 지정된 파일만 그대로 불러옵니다.

두 번째 함정: 화면 크기와 상관없이 같은 이미지를 사용합니다

이 문제는 모바일에서 특히 두드러집니다. 웹사이트에 2000x1500 고해상도 이미지를 올려 두었는데 사용자의 휴대폰 화면 너비가 375px에 불과해도, 전체 이미지를 내려받은 뒤 브라우저가 축소해서 보여 줍니다. 데이터 비용을 낭비하고 로딩도 느려집니다.

세 번째 함정: 레이아웃 이동으로 사용자를 불편하게 합니다

웹페이지를 열고 버튼을 누르려는 순간 이미지가 나타나면서 전체 페이지가 아래로 밀려 엉뚱한 곳을 누른 경험이 있을 겁니다. 이것이 CLS(Cumulative Layout Shift) 문제입니다. Google은 이를 Core Web Vitals의 핵심 지표 중 하나로 삼고 있으며 SEO 순위에도 직접 영향을 줍니다.

Image 컴포넌트의 자동 최적화 기능

Next.js의 Image 컴포넌트는 바로 이런 문제를 해결하기 위한 도구입니다. 단순히 img 태그를 감싼 것이 아니라 완전한 이미지 최적화 솔루션입니다.

자동 형식 선택

Image 컴포넌트는 사용자의 브라우저가 보내는 Accept 요청 헤더를 확인해 지원하는 형식을 자동으로 판단합니다. AVIF를 지원하면 AVIF, WebP를 지원하면 WebP를 제공하고, 둘 다 지원하지 않을 때만 원본 형식을 제공합니다. 이 과정은 완전히 자동이므로 코드를 한 줄도 추가할 필요가 없습니다.

제가 테스트했을 때 500KB JPEG는 WebP로 자동 변환한 뒤 180KB, AVIF로는 120KB가 됐습니다. 웹사이트에 이미지가 수십, 수백 장이라면 절약되는 대역폭이 상당합니다.

반응형 로딩

Image 컴포넌트는 사용자 기기의 화면 크기에 맞는 이미지를 자동 생성하고 불러옵니다. 모바일 사용자는 너비 375px 버전을, 데스크톱 사용자는 1920px 버전을 봅니다. 이 기능을 srcset이라고 합니다. 일반 img 태그로도 구현할 수 있지만 많은 설정을 직접 작성해야 하는 반면, Image 컴포넌트는 모두 자동으로 처리합니다.

지연 로딩

기본적으로 Image 컴포넌트는 viewport 안에 나타난 이미지만 불러옵니다. 페이지 아래쪽 이미지는 사용자가 해당 위치까지 스크롤했을 때 로딩을 시작합니다. 이 기능은 최초 로딩 데이터양을 크게 줄여 페이지를 훨씬 빠르게 열어 줍니다.

실제 데이터에 따르면 Next.js Image 컴포넌트를 올바르게 사용하면 이미지 용량을 60~80% 줄이고, LCP를 2.5초 이내로 유지하며, CLS를 0에 가깝게 만들 수 있습니다. 이 수치는 이론이 아니라 제가 실제 프로젝트에서 측정한 결과입니다.

기본 사용법: 로컬 이미지와 원격 이미지

Image 컴포넌트를 처음 사용할 때 많은 사람이 가장 헷갈려 하는 점은 어떤 이미지는 바로 되고 어떤 이미지는 오류가 나는 이유입니다. 저도 그랬습니다. 핵심은 로컬 이미지와 원격 이미지를 처리하는 방식이 다르다는 데 있습니다.

로컬 이미지: 가장 간단한 경우

로컬 이미지는 프로젝트 안에 있는 이미지 파일이며, 일반적으로 두 가지 방식으로 사용합니다.

방법 1: import로 가져오기(권장)

import heroImage from '/public/images/hero.jpg'
import Image from 'next/image'

export default function Home() {
  return (
    <Image
      src={heroImage}
      alt="Hero image"
    />
  )
}

이 방식이 가장 편합니다. Next.js가 빌드할 때 이미지의 너비와 높이 정보를 자동으로 읽기 때문에 widthheight조차 작성하지 않아도 됩니다. 지금은 로컬 이미지를 처리할 때 거의 항상 이 방식을 사용합니다.

방법 2: 경로 직접 입력

<Image
  src="/images/hero.jpg"
  width={1920}
  height={1080}
  alt="Hero image"
/>

이미지가 public 폴더에 있다면 경로를 직접 입력해도 됩니다. 다만 이 방식에서는 너비와 높이를 직접 지정해야 하며, 빠뜨리면 오류가 발생합니다.

원격 이미지: 가장 흔히 문제가 생기는 곳

원격 이미지는 클라우드 스토리지 등 외부 URL에서 불러오는 이미지입니다. 이 경우에 문제가 가장 자주 발생합니다.

흔한 오류: “Un-configured Host”

<Image
  src="https://images.unsplash.com/photo-123456"
  width={800}
  height={600}
  alt="Sample image"
/>

이 코드를 그대로 쓰면 십중팔구 다음 오류가 나타납니다.

Error: Invalid src prop (https://images.unsplash.com/photo-123456) on `next/image`, 
hostname "images.unsplash.com" is not configured under images in your `next.config.js`

왜 그럴까요? Next.js는 악의적인 사용자가 서버를 이용해 임의 URL의 이미지를 최적화하고 서버 리소스를 무단으로 소비하는 일을 막으려 합니다. 따라서 최적화를 허용할 이미지 도메인을 명시적으로 선언하도록 요구합니다.

올바른 방법: remotePatterns 설정

next.config.js에 다음 설정을 추가합니다(Next.js 14 이상 권장).

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        port: '',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: 's3.amazonaws.com',
        port: '',
        pathname: '/my-bucket/**',
      },
    ],
  },
}

module.exports = nextConfig

각 설정 항목의 의미는 다음과 같습니다.

  • protocol: https 또는 http이며, 일반적으로 https를 사용합니다.
  • hostname: 이미지 도메인으로, 정확히 일치해야 합니다.
  • port: 포트 번호이며, 보통 비워 둡니다.
  • pathname: 경로 매칭 규칙입니다. /**는 모든 경로를 뜻하며 특정 경로로 제한할 수도 있습니다.

설정을 마친 뒤에는 개발 서버를 다시 시작해야 합니다. 설정 파일 변경 사항은 재시작해야 적용됩니다. 저도 처음에는 설정을 바꾸고 재시작하지 않아 설정이 잘못된 줄 알고 30분이나 허비했습니다.

이전 버전의 설정 방식(권장하지 않음)

일부 튜토리얼에서 domains 설정을 사용하는 것을 볼 수 있습니다.

module.exports = {
  images: {
    domains: ['images.unsplash.com', 's3.amazonaws.com'],
  },
}

이 방식은 Next.js 14 이후 폐기되었습니다. 아직 사용할 수는 있지만 remotePatterns로 바꾸는 것을 권장합니다. remotePatterns는 더 안전하고 구체적인 경로 범위를 제한할 수 있기 때문입니다.

너비와 높이를 반드시 지정해야 하는 이유

import 방식을 제외하면 로컬이든 원격이든 widthheight를 직접 지정해야 합니다. CLS 레이아웃 이동을 방지하기 위해서입니다.

브라우저는 이미지를 불러오기 전에 이미지가 차지할 공간을 알아야 미리 자리를 확보할 수 있습니다. 크기를 모르면 이미지 다운로드가 끝난 뒤에야 레이아웃을 조정할 수 있으므로 페이지가 순간적으로 움직입니다.

반응형 이미지라 화면 크기에 따라 너비가 바뀌어야 하는데 어떻게 고정 너비와 높이를 지정하느냐고 생각할 수 있습니다. 뒤에서 fill 속성을 설명할 때 이 문제를 해결하겠습니다.

레이아웃 이동 문제 해결(CLS 최적화)

페이지가 계속 움직이는 것은 정말 불편합니다. 예전에 만든 뉴스 웹사이트에서 사용자들이 가장 많이 불평한 것도 “제목을 누르려는데 이미지가 로딩되면서 광고를 누르게 된다”는 문제였습니다. CLS 지표를 자세히 살펴본 뒤에야 이 문제가 얼마나 중요한지 알게 됐습니다.

CLS란 무엇이며 왜 중요한가

CLS(Cumulative Layout Shift)는 누적 레이아웃 이동을 뜻합니다. 쉽게 말해 페이지 로딩 중 요소의 위치가 갑자기 움직이는 정도입니다.

Google은 CLS를 Core Web Vitals의 세 가지 핵심 지표 중 하나로 삼고 있으며 SEO 순위에도 직접 영향을 줍니다. CLS 점수가 0.1을 넘으면 좋지 않고, 0.1보다 낮아야 양호합니다. 한 페이지에 이미지가 10~20장 있고 각 이미지가 로딩될 때마다 페이지가 움직인다면 CLS가 높아질 수밖에 없습니다.

무엇보다 사용자 경험에 큰 영향을 줍니다. 저도 웹을 탐색하다 계속 움직이는 페이지를 만나면 내용을 읽을 마음이 사라져 바로 닫곤 합니다.

Image 컴포넌트가 CLS를 방지하는 방법

Next.js Image 컴포넌트가 CLS를 막는 핵심 원리는 간단합니다. 공간을 미리 확보하는 것입니다.

Image 컴포넌트에 widthheight를 지정하면 브라우저는 이미지가 다운로드되기 전에 해당 크기의 빈 영역을 페이지에 확보합니다. 이미지 로딩이 끝나면 그 자리에 그대로 채우므로 페이지 레이아웃은 변하지 않습니다.

<Image
  src="/product.jpg"
  width={400}
  height={300}
  alt="Product image"
/>

이 코드에서는 브라우저가 페이지에 400x300 플레이스홀더를 먼저 그리고 이미지를 불러옵니다. CLS 값이 바로 0이 됩니다.

하지만 현재 웹페이지는 대부분 반응형 디자인이어서 이미지 너비가 화면 크기에 따라 바뀌어야 합니다. 고정 너비와 높이만으로는 충분하지 않습니다.

반응형 이미지의 올바른 방법: fill 속성

반응형 이미지를 위해 Next.js는 fill 속성을 제공합니다. 이 속성을 사용하면 이미지가 부모 컨테이너 전체를 채우고, 너비와 높이는 CSS가 제어합니다.

<div style={{ position: 'relative', width: '100%', height: '400px' }}>
  <Image
    src="/hero.jpg"
    fill
    style={{ objectFit: 'cover' }}
    alt="Hero image"
  />
</div>

다음 핵심 사항에 유의하세요.

  1. 부모 컨테이너에 반드시 position: relative를 설정합니다: 이미지가 어느 요소를 기준으로 채울지 알 수 있게 합니다.
  2. 부모 컨테이너에는 명확한 높이가 있어야 합니다: height: auto가 아니라 구체적인 값이나 백분율이어야 합니다.
  3. objectFit으로 이미지 맞춤 방식을 제어합니다: cover는 이미지를 잘라 컨테이너를 채우고, contain은 전체 이미지를 표시하지만 여백이 생길 수 있습니다.

이 방식에서도 브라우저는 부모 컨테이너 높이를 보고 공간을 미리 확보하므로 CLS를 방지할 수 있습니다.

sizes 속성: 불러올 이미지 크기를 브라우저에 알리기

fill 속성을 사용했다면 sizes 속성도 함께 사용해야 합니다. 그렇지 않으면 Next.js는 어느 크기의 이미지를 생성해야 할지 알 수 없습니다.

<div style={{ position: 'relative', width: '100%', height: '400px' }}>
  <Image
    src="/hero.jpg"
    fill
    sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
    style={{ objectFit: 'cover' }}
    alt="Hero image"
  />
</div>

sizes 속성의 의미는 다음과 같습니다.

  • 화면 너비가 768px 이하이면 이미지가 viewport 너비의 100%를 차지합니다.
  • 화면 너비가 768px~1200px이면 이미지가 viewport 너비의 50%를 차지합니다.
  • 화면 너비가 1200px보다 크면 이미지가 viewport 너비의 33%를 차지합니다.

Next.js는 이 설정을 바탕으로 여러 크기의 이미지를 생성하고, 브라우저가 가장 적합한 것을 자동 선택합니다. 모바일 사용자는 데스크톱용 대형 이미지를 내려받지 않아도 되므로 데이터를 절약하고 속도도 빨라집니다.

실제 사례: 상황별 처리 방식

Hero 이미지(첫 화면 전체 너비 대형 이미지)

<div style={{ position: 'relative', width: '100%', height: '60vh' }}>
  <Image
    src="/hero.jpg"
    fill
    priority
    sizes="100vw"
    style={{ objectFit: 'cover' }}
    alt="Hero image"
  />
</div>

여기서는 뒤에서 설명할 priority 속성으로 첫 화면의 Hero 이미지를 우선 로딩합니다. sizes="100vw"는 이미지가 언제나 viewport 전체 너비를 채운다는 뜻입니다.

글 썸네일(고정 크기)

<Image
  src={post.thumbnail}
  width={300}
  height={200}
  alt={post.title}
/>

썸네일 크기가 고정되어 있다면 widthheight를 직접 사용하는 것이 가장 간단합니다.

상품 목록(반응형 그리드)

<div style={{ position: 'relative', width: '100%', paddingBottom: '100%' }}>
  <Image
    src={product.image}
    fill
    sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
    style={{ objectFit: 'cover' }}
    alt={product.name}
  />
</div>

paddingBottom: '100%'로 정사각형 컨테이너(반응형 1:1 비율)를 만들어 상품 이미지를 표시하기에 적합합니다. 이미지 너비는 화면에 따라 달라져 모바일에서는 한 줄에 하나, 태블릿에서는 두 개, 데스크톱에서는 세 개가 표시됩니다.

성능 최적화 핵심 설정

기본 사용법과 CLS 최적화를 익혔다면 이제 성능을 한 단계 더 끌어올릴 핵심 설정을 살펴보겠습니다.

priority: 첫 화면의 핵심 이미지를 우선 로딩하기

기본적으로 Image 컴포넌트의 이미지는 모두 지연 로딩되며, viewport 안으로 스크롤해야 로딩을 시작합니다. 하지만 첫 화면의 Hero 이미지나 Logo 같은 핵심 요소는 바로 나타나야 합니다. 그렇지 않으면 사용자는 빈 영역을 보게 됩니다.

이때 priority 속성을 사용합니다.

<Image
  src="/hero.jpg"
  width={1920}
  height={1080}
  priority
  alt="Hero image"
/>

priority를 추가하면 Next.js는 다음 작업을 수행합니다.

  1. 지연 로딩을 취소하고 즉시 이미지 로딩을 시작합니다.
  2. HTML의 <head>에 preload 태그를 삽입해 브라우저에 이 이미지가 중요하다고 알립니다.
  3. LCP(Largest Contentful Paint) 지표가 눈에 띄게 개선됩니다.

한 번은 성능 최적화 과정에서 홈페이지 Hero 이미지에 priority만 추가했는데 LCP가 3.8초에서 2.1초로 줄었습니다. 효과가 즉시 나타났습니다.

priority는 언제 사용해야 하나요?

  1. 홈페이지 Hero 이미지
  2. 웹사이트 Logo(크기가 큰 경우)
  3. 글 상세 페이지의 대표 이미지
  4. LCP 요소가 될 가능성이 있는 모든 이미지

그렇다면 모든 이미지에 priority를 붙이면 더 빠르지 않을까 생각할 수 있습니다. 하지만 그러면 안 됩니다. priority는 브라우저가 이미지를 즉시 다운로드하게 만듭니다. 페이지의 이미지 20장에 모두 추가하면 브라우저가 20장을 동시에 내려받아 오히려 느려집니다. 가장 중요한 이미지에만 신중하게 사용해야 합니다.

Next.js 16의 변경 사항

여기서 Next.js 16 업데이트도 언급해야 합니다(현재는 아직 RC 버전입니다). priority 속성이 preload 속성으로 바뀝니다. 최신 버전을 사용한다면 코드를 다음과 같이 수정해야 합니다.

<Image
  src="/hero.jpg"
  width={1920}
  height={1080}
  preload
  alt="Hero image"
/>

또는 loading="eager"fetchPriority="high"를 사용하는 더 유연한 방식도 있습니다.

<Image
  src="/hero.jpg"
  width={1920}
  height={1080}
  loading="eager"
  fetchPriority="high"
  alt="Hero image"
/>

loading: 로딩 전략 제어

loading 속성에는 두 가지 값이 있습니다.

  • lazy(기본값): 지연 로딩하며 이미지가 viewport 안에 들어올 때 다운로드를 시작합니다.
  • eager: 이미지가 viewport 안에 있는지와 관계없이 즉시 로딩합니다.

대부분은 기본값인 lazy로 충분합니다. 첫 화면의 핵심 이미지에만 eager가 필요합니다.

// 페이지 하단의 관련 글 이미지는 기본 lazy 사용
<Image src="/related-1.jpg" width={300} height={200} alt="Related post" />

// 첫 화면의 주요 콘텐츠 이미지는 eager로 변경
<Image src="/main-content.jpg" width={800} height={600} loading="eager" alt="Main content" />

quality: 품질과 용량의 균형

quality 속성은 이미지 압축 품질을 제어하며 범위는 1~100, 기본값은 75입니다.

<Image
  src="/product.jpg"
  width={800}
  height={600}
  quality={90}
  alt="Product image"
/>

품질이 높을수록 이미지는 선명하지만 파일도 커집니다. 다음 경험치를 참고할 수 있습니다.

  • 첫 화면의 주요 이미지: quality={90}으로 선명도를 보장합니다.
  • 일반 콘텐츠 이미지: quality={75}(기본값)로 품질과 용량의 균형을 맞춥니다.
  • 썸네일과 배경 이미지: quality={60}으로 대역폭을 절약합니다.

테스트해 보니 quality를 90에서 75로 낮춰도 육안으로는 거의 차이가 없지만 파일 용량은 약 30% 줄었습니다. 75에서 60으로 낮춰도 일반 화면에서는 충분히 괜찮았고, 용량이 다시 20% 줄었습니다.

중요 업데이트: Next.js 16부터 quality가 필수 항목이 됩니다. 악의적인 사용자가 URL 매개변수로 여러 품질의 이미지를 요청해 서버 리소스를 소모하는 일을 막기 위해서입니다. 16으로 업그레이드한 뒤에는 각 Image 컴포넌트에 quality 속성을 추가해야 합니다.

자동 형식 선택: WebP와 AVIF

이 기능은 완전히 자동이므로 아무것도 설정할 필요가 없습니다. Next.js는 브라우저의 Accept 요청 헤더를 확인해 가장 적합한 형식을 자동 선택합니다.

  • 브라우저가 AVIF 지원 → AVIF 출력(가장 작지만 인코딩이 느림)
  • 브라우저가 WebP 지원 → WebP 출력(작고 호환성이 좋음)
  • 둘 다 미지원 → 원본 형식(JPEG/PNG) 출력

AVIF는 WebP보다도 30~40% 작지만 호환성이 약간 떨어집니다. 그래도 주요 브라우저(Chrome 85+, Firefox 93+, Safari 16+)가 모두 AVIF를 지원하므로 거의 걱정할 필요가 없습니다.

실전 설정 예제

이 설정들을 조합하면 상황별 코드는 다음과 같습니다.

홈페이지 Hero 이미지(우선 로딩, 고품질)

<div style={{ position: 'relative', width: '100%', height: '60vh' }}>
  <Image
    src="/hero.jpg"
    fill
    priority
    quality={90}
    sizes="100vw"
    style={{ objectFit: 'cover' }}
    alt="Welcome to our site"
  />
</div>

글 목록 썸네일(지연 로딩, 중간 품질)

{posts.map(post => (
  <Image
    key={post.id}
    src={post.thumbnail}
    width={300}
    height={200}
    quality={75}
    alt={post.title}
  />
))}

페이지 하단 관련 링크 아이콘(지연 로딩, 낮은 품질)

<Image
  src="/footer-icon.png"
  width={40}
  height={40}
  quality={60}
  alt="Footer icon"
/>

흔한 오류와 해결 방법

앞에서 여러 사용법을 살펴봤으니 이제 실제 개발에서 가장 자주 마주치는 함정을 알아보겠습니다. 모두 제가 직접 겪었고, 그중 일부는 해결하는 데 꽤 오래 걸렸습니다.

오류 1: Un-configured Host

가장 자주 발생하는 오류입니다.

오류 메시지:

Error: Invalid src prop (https://example.com/image.jpg) on `next/image`, 
hostname "example.com" is not configured under images in your `next.config.js`

원인:

Image 컴포넌트에서 외부 URL을 사용했지만 next.config.jsremotePatterns에서 해당 도메인을 허용하지 않았습니다.

해결 방법:

next.config.js에 다음 설정을 추가합니다.

module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com',
        port: '',
        pathname: '/**',
      },
    ],
  },
}

주의 사항:

  1. 설정 파일을 수정한 뒤에는 개발 서버를 반드시 다시 시작해야 합니다(npm run dev).
  2. hostname은 정확히 일치해야 하며 와일드카드를 사용할 수 없습니다. *.example.com은 쓸 수 없고 구체적인 하위 도메인을 작성해야 합니다.
  3. 이미지가 여러 도메인에서 온다면 remotePatterns 배열에 객체를 여러 개 추가합니다.

오류 2: 레이아웃 이동 또는 지나치게 높은 CLS

증상:

페이지를 불러올 때 이미지 영역이 갑자기 나타나 다른 콘텐츠를 아래로 밀고, 페이지가 계속 움직입니다.

원인:

두 가지 가능성이 있습니다.

  1. widthheight를 설정하지 않아 브라우저가 확보할 공간의 크기를 모릅니다.
  2. fill 속성을 사용했지만 부모 컨테이너에 높이를 설정하지 않았습니다.

해결 방법:

상황 1: 이미지에 명확한 너비와 높이를 설정합니다.

// ❌ 오류: 너비와 높이가 없음
<Image src="/product.jpg" alt="Product" />

// ✅ 올바름: 너비와 높이 지정
<Image src="/product.jpg" width={400} height={300} alt="Product" />

상황 2: 부모 컨테이너에 높이를 설정합니다.

// ❌ 오류: 부모 컨테이너에 높이가 없음
<div style={{ position: 'relative', width: '100%' }}>
  <Image src="/hero.jpg" fill alt="Hero" />
</div>

// ✅ 올바름: 부모 컨테이너에 명확한 높이가 있음
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
  <Image src="/hero.jpg" fill alt="Hero" />
</div>

오류 3: 이미지가 흐리거나 크기가 지나치게 큼

증상:

모바일에서 접속하면 이미지가 흐리거나 로딩이 매우 느립니다.

원인:

sizes 속성을 설정하지 않아 Next.js가 생성할 이미지 크기를 모르고 기본값 100vw(viewport 전체 너비)를 사용했습니다. 실제 이미지가 화면의 절반만 차지한다면 필요한 것보다 두 배 큰 이미지가 생성됩니다.

해결 방법:

실제 렌더링 크기에 맞춰 sizes를 설정합니다.

// 화면에 따라 이미지가 차지하는 너비가 달라짐
<Image
  src="/product.jpg"
  width={400}
  height={300}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  alt="Product"
/>

이미지가 언제나 고정 너비라면(예: 썸네일) widthheight만 사용하면 되고 sizes는 필요하지 않습니다.

오류 4: 폐기된 API 사용

Next.js 14와 15에서는 일부 API가 변경되었습니다. 오래된 튜토리얼을 참고하면 이미 폐기된 방식을 사용할 수 있습니다.

폐기 1: domains 설정

// ❌ Next.js 14+에서 폐기됨
module.exports = {
  images: {
    domains: ['example.com'],
  },
}

// ✅ remotePatterns 사용
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com',
      },
    ],
  },
}

폐기 2: onLoadingComplete 콜백

// ❌ Next.js 14+에서 폐기됨
<Image
  src="/image.jpg"
  width={400}
  height={300}
  onLoadingComplete={() => console.log('loaded')}
  alt="Image"
/>

// ✅ onLoad 사용
<Image
  src="/image.jpg"
  width={400}
  height={300}
  onLoad={() => console.log('loaded')}
  alt="Image"
/>

Next.js 16의 변경 사항(출시 예정):

// ⚠️ Next.js 16에서 priority가 preload로 변경됨
// 이전 방식(Next.js 15 이하)
<Image src="/hero.jpg" width={1920} height={1080} priority alt="Hero" />

// 새로운 방식(Next.js 16)
<Image src="/hero.jpg" width={1920} height={1080} preload alt="Hero" />
// 또는
<Image src="/hero.jpg" width={1920} height={1080} loading="eager" fetchPriority="high" alt="Hero" />

빠른 문제 해결 체크리스트

문제가 발생하면 다음 순서로 확인하세요.

  1. ✅ 원격 이미지에 remotePatterns를 설정했나요?
  2. ✅ 설정 파일을 수정한 뒤 개발 서버를 다시 시작했나요?
  3. ✅ 이미지에 widthheight를 설정했나요(또는 부모 컨테이너의 높이를 설정했나요)?
  4. fill을 사용할 때 부모 컨테이너에 position: relative와 명확한 높이가 있나요?
  5. ✅ 반응형 이미지에 적절한 sizes를 설정했나요?
  6. ✅ 폐기된 API(domains, onLoadingComplete 등)를 사용하고 있지 않나요?

고급 팁과 모범 사례

기본 사용법과 흔한 오류 처리법을 익혔다면 이제 이미지 경험을 한 단계 더 개선할 몇 가지 고급 팁을 살펴보겠습니다.

placeholder로 사용자 경험 개선

이미지를 불러오는 데는 시간이 걸리며, 네트워크가 느릴 때는 특히 그렇습니다. 먼저 흐린 플레이스홀더 이미지를 보여 주면 사용자 경험이 훨씬 좋아집니다.

blur placeholder(흐린 플레이스홀더 이미지)

import Image from 'next/image'
import heroImage from '/public/hero.jpg'

export default function Hero() {
  return (
    <Image
      src={heroImage}
      placeholder="blur"
      alt="Hero image"
    />
  )
}

import로 가져온 로컬 이미지에는 Next.js가 저품질 base64 플레이스홀더를 자동 생성합니다. placeholder="blur"를 추가하면 이미지가 로딩되는 동안 흐린 버전을 먼저 표시한 뒤 점차 선명하게 바뀝니다. Instagram, Medium 같은 웹사이트에서 흔히 볼 수 있는 효과입니다.

원격 이미지의 blur placeholder

원격 이미지에는 blurDataURL을 직접 제공해야 합니다.

<Image
  src="https://example.com/image.jpg"
  width={800}
  height={600}
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
  alt="Remote image"
/>

이 온라인 도구로 blurDataURL을 생성하거나 서버에서 sharp 라이브러리로 생성할 수 있습니다.

empty placeholder

플레이스홀더 효과가 필요 없다면 placeholder="empty"를 명시할 수 있습니다. 이미지가 로딩되기 전까지 빈 영역으로 표시됩니다. 이것이 기본 동작이므로 작성하지 않아도 같습니다.

CDN과 함께 사용하기

Next.js는 기본적으로 자체 Image Optimization API로 이미지를 최적화합니다. Cloudinary나 Uploadcare 같은 이미지 CDN 서비스를 사용한다면 사용자 정의 loader를 설정할 수 있습니다.

// next.config.js
module.exports = {
  images: {
    loader: 'cloudinary',
    path: 'https://res.cloudinary.com/your-cloud-name/',
  },
}

또는 loader 함수를 직접 작성할 수 있습니다.

// next.config.js
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './my-loader.js',
  },
}

// my-loader.js
export default function myLoader({ src, width, quality }) {
  return `https://cdn.example.com/${src}?w=${width}&q=${quality || 75}`
}

이렇게 하면 모든 이미지 요청이 Next.js 서버가 아니라 CDN을 거칩니다. 트래픽이 많은 웹사이트에 적합합니다.

반응형 이미지의 완전한 구현

반응형 이미지는 크기만 조절하는 것이 아니라 상황별 표시 방식도 고려해야 합니다.

모바일, 태블릿, 데스크톱 차별화 전략

<div className="image-container">
  <Image
    src="/product.jpg"
    width={1200}
    height={800}
    sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
    style={{
      width: '100%',
      height: 'auto',
    }}
    alt="Product image"
  />
</div>

CSS를 함께 사용합니다.

.image-container {
  width: 100%;
}

@media (min-width: 640px) {
  .image-container {
    width: 50%;
  }
}

@media (min-width: 1024px) {
  .image-container {
    width: 33.333%;
  }
}

sizes 속성과 CSS 미디어 쿼리를 일치시키면 브라우저가 가장 적합한 이미지 크기를 선택할 수 있습니다.

모니터링과 디버깅

Chrome DevTools에서 이미지 로딩 확인

Chrome DevTools → Network 탭 → Img 필터를 선택하면 각 이미지의 다음 정보를 볼 수 있습니다.

  • 파일 크기
  • 로딩 시간
  • 응답 헤더(형식 정보 포함)

Image 컴포넌트를 사용한 이미지 URL 뒤에는 ?w=xxx&q=xxx 같은 매개변수가 붙습니다. Next.js가 최적화하고 있다는 뜻입니다.

Lighthouse 성능 테스트

Chrome DevTools → Lighthouse → “Analyze page load”를 클릭하고 다음 지표를 중점적으로 확인합니다.

  • LCP (Largest Contentful Paint): 이상적인 값 < 2.5초
  • CLS (Cumulative Layout Shift): 이상적인 값 < 0.1
  • Image 요소 제안: Lighthouse가 최적화되지 않은 이미지를 알려 줍니다.

이미지 최적화를 마칠 때마다 Lighthouse를 실행해 성능 점수가 얼마나 올랐는지 확인합니다. 일반적으로 60점대에서 90점 이상으로 높일 수 있습니다.

Core Web Vitals 지속 모니터링

프로덕션 환경에서는 Google Search Console이나 Vercel Analytics로 Core Web Vitals 지표를 지속적으로 모니터링하는 것이 좋습니다. 그래야 성능 저하 문제를 제때 발견할 수 있습니다.

결론

지금까지 설명한 내용을 정리하면 Next.js Image 컴포넌트의 핵심은 느린 이미지 로딩, 설정 오류, 레이아웃 이동이라는 세 가지 문제를 해결하는 것입니다.

Image 컴포넌트를 올바르게 사용하면 다음 효과를 얻을 수 있습니다.

  • 이미지 용량 60~80% 감소(자동 WebP/AVIF 변환)
  • LCP를 2.5초 이내로 유지(priority의 적절한 사용)
  • CLS를 0에 가깝게 유지(너비와 높이 또는 fill의 올바른 설정)

가장 중요한 설정은 다음과 같습니다.

  1. 원격 이미지는 remotePatterns를 설정하고, 변경 후 서버를 다시 시작합니다.
  2. widthheight를 설정하거나, 부모 컨테이너 높이와 함께 fill을 사용합니다.
  3. 첫 화면의 핵심 이미지에는 priority를 추가하고 나머지는 기본 지연 로딩을 사용합니다.
  4. 반응형 이미지에는 sizes를 설정해 브라우저가 적절한 크기를 선택하게 합니다.
  5. 이미지 중요도에 따라 quality를 조정합니다. 첫 화면은 90, 일반 이미지는 75, 썸네일은 60입니다.

이제 프로젝트를 확인해 <img> 태그를 <Image>로 바꿔 보세요. Lighthouse로 한 번 측정하면 성능 점수가 얼마나 오르는지 확인할 수 있습니다. 적어도 20점은 오를 것이라고 장담합니다.

문제가 생기면 이 글의 흔한 오류 섹션을 다시 살펴보세요. 대부분 해결책을 찾을 수 있습니다. Next.js의 Image 컴포넌트는 설정 항목이 많지만 핵심 몇 가지만 익히면 대부분의 상황에 대응할 수 있습니다.

Next.js Image 컴포넌트 최적화 전체 과정

원격 이미지 설정부터 성능 최적화와 레이아웃 이동 방지까지의 전체 단계

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: 원격 이미지 도메인 설정

    next.config.js에서 다음과 같이 설정합니다:
    • images.remotePatterns 배열 추가
    • protocol, hostname, pathname 설정
    • 와일드카드 매칭 지원

    예시:
    images: {
    remotePatterns: [
    {
    protocol: 'https',
    hostname: 'example.com',
    pathname: '/images/**'
    }
    ]
    }

    주의: 설정 후 개발 서버를 반드시 다시 시작해야 합니다.
  2. 2

    Step 2: img 태그를 Image 컴포넌트로 교체

    기본 사용법:
    • 가져오기: import Image from 'next/image'
    • width와 height를 반드시 설정하거나 fill 사용
    • alt 텍스트 추가(SEO 필수)

    예시:
    <Image
    src="/hero.jpg"
    width={800}
    height={600}
    alt="설명 텍스트"
    />
  3. 3

    Step 3: 레이아웃 이동(CLS) 처리

    방법 1: 고정 크기 설정
    • width와 height를 반드시 설정
    • aspect-ratio로 비율 유지

    방법 2: fill 모드 사용
    • 부모 컨테이너에 position: relative 설정
    • Image 컴포넌트에 fill 속성 사용
    • 부모 컨테이너의 너비와 높이 설정

    방법 3: placeholder 사용
    • blurDataURL: 흐린 플레이스홀더 이미지
    • placeholder="blur": 플레이스홀더 이미지 표시
  4. 4

    Step 4: 로딩 성능 최적화

    첫 화면 핵심 이미지:
    • priority 속성 추가
    • quality=90 설정
    • 이미지가 viewport 안에 있는지 확인

    그 밖의 이미지:
    • 기본 지연 로딩(별도 설정 불필요)
    • quality=75(품질과 용량의 균형)
    • sizes 속성으로 반응형 구현

    썸네일:
    • quality=60이면 충분
    • 작은 크기 버전 사용
  5. 5

    Step 5: 반응형 이미지 설정

    sizes 속성을 사용합니다:
    • 화면 크기별로 필요한 이미지 크기를 브라우저에 알림
    • 브라우저가 가장 적합한 이미지를 자동 선택

    예시:
    <Image
    src="/hero.jpg"
    width={1200}
    height={630}
    sizes="(max-width: 768px) 100vw, 50vw"
    alt="설명"
    />

    이렇게 하면 모바일에서는 전체 너비 이미지를, 데스크톱에서는 50% 너비 이미지를 불러옵니다.
  6. 6

    Step 6: 테스트 및 검증

    성능 테스트:
    • Lighthouse로 LCP와 CLS 테스트
    • Network 탭에서 이미지 로딩 상태 확인
    • 이미지 형식(WebP/AVIF) 검증

    체크리스트:
    • 모든 원격 이미지의 도메인이 설정되어 있음
    • 모든 이미지에 width와 height가 있음
    • 첫 화면 이미지에 priority가 추가되어 있음
    • CLS 점수가 0에 가까움
    • 이미지 용량이 60% 이상 감소함

FAQ

원격 이미지에서 'Un-configured Host' 오류가 발생하는 이유는 무엇인가요?
Next.js Image 컴포넌트는 보안을 위해 허용할 원격 이미지 도메인을 명시적으로 설정하도록 요구합니다. next.config.js의 images.remotePatterns에 protocol, hostname, 선택 사항인 pathname을 포함한 도메인 설정을 추가하세요. 설정 후에는 개발 서버를 다시 시작해야 적용됩니다.
Image 컴포넌트에는 반드시 width와 height를 설정해야 하나요?
네, 반드시 설정해야 합니다. 레이아웃 이동(CLS)을 방지하기 위해서입니다. 고정 크기를 지정하고 싶지 않다면 부모 컨테이너의 너비와 높이를 지정한 뒤 fill 모드를 사용하거나 aspect-ratio로 비율을 유지할 수 있습니다. 설정하지 않으면 CLS 점수가 나빠집니다.
이미지 로딩 중 레이아웃 이동을 어떻게 방지하나요?
방법은 다음과 같습니다. 1) width와 height 설정, 2) 부모 컨테이너와 함께 fill 모드 사용, 3) placeholder=&quot;blur&quot;로 흐린 플레이스홀더 표시, 4) aspect-ratio로 비율 유지. 핵심은 이미지 컨테이너의 크기를 명확히 지정하는 것입니다.
priority 속성은 언제 사용하나요?
priority는 첫 화면(Above the fold)의 핵심 이미지에 사용합니다. 해당 이미지를 지연 로딩하지 않고 즉시 불러오도록 Next.js에 알리는 속성입니다. 일반적으로 Hero 이미지, Logo 등 첫 화면에 보이는 중요한 이미지에 사용합니다. 남용하면 오히려 성능이 저하됩니다.
Image 컴포넌트가 이미지 형식을 자동으로 변환하나요?
네. Next.js Image 컴포넌트는 브라우저가 지원하면 이미지를 WebP 형식으로 자동 변환하고, AVIF를 지원하는 브라우저에는 AVIF를 우선 제공합니다. 이미지 용량을 크게 줄여(보통 60~80%) 로딩 속도를 높일 수 있습니다.
sizes 속성은 어떤 역할을 하나요?
sizes 속성은 화면 크기에 따라 어느 크기의 이미지를 불러와야 하는지 브라우저에 알려 줍니다. 브라우저는 이 정보와 srcset을 바탕으로 가장 적합한 이미지 크기를 자동 선택합니다. 반응형 이미지 최적화에 매우 중요하며, 모바일에서 지나치게 큰 이미지를 불러오는 일을 막아 줍니다.
이미지 품질은 어떻게 최적화하나요?
quality 속성으로 이미지 품질을 제어합니다. 첫 화면의 핵심 이미지는 90, 일반 이미지는 75, 썸네일은 60을 사용하세요. 품질이 높을수록 파일도 커지므로 균형이 필요합니다. 데스크톱에는 큰 이미지, 모바일에는 작은 이미지처럼 서로 다른 이미지 크기를 사용할 수도 있습니다.

5분 읽기 · 게시일: 2025년 12월 19일 · 수정일: 2026년 9월 8일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog