juncci 님의 블로그

Next.js Image 최적화 동작 과정 정리 본문

[Next.js]

Next.js Image 최적화 동작 과정 정리

juncci 2026. 10. 8. 15:22

웹 페이지에서 이미지는 전체 네트워크 리소스 중 큰 비중을 차지한다. 특히 상품 이미지나 배너처럼 고해상도 원본 이미지를 그대로 전달하면 사용자가 실제로 보는 크기보다 많은 데이터를 다운로드하게 되고 이미지가 화면에 표시되는 시간에도 영향을 줄 수 있다.

 

Next.js는 이러한 문제를 줄이기 위해 next/image의 <Image> 컴포넌트를 제공한다.

처음에는 <Image>를 사용하면 이미지를 자동으로 압축해주는 정도로 생각하기 쉽다. 하지만 실제 동작을 살펴보면 이미지 압축 하나로 끝나지 않는다.

 

Next.js는 브라우저가 적절한 이미지를 선택할 수 있도록 여러 이미지 후보를 제공하고 선택된 요청에 맞는 이미지를 생성한다. 생성된 결과는 캐시를 통해 재사용할 수 있으며 이미지의 위치와 중요도에 따라 로딩 시점도 조절할 수 있다.

 

전체 과정을 단순화하면 다음과 같다.

 

이번 글에서는 이 흐름을 기준으로 Next.js Image Optimization이 어떤 역할을 수행하는지 정리한다.

원본 이미지 전달 과정

3840 × 2160 크기의 4MB 이미지가 있다고 가정해보자.

Original Image

3840 × 2160
4MB

 

일반적인 <img>를 사용하면 다음과 같이 이미지를 제공할 수 있다.

<img
  src="/images/banner.jpg"
  alt="배너"
/>

 

모바일에서는 CSS를 이용해 이미지의 크기를 줄일 수 있다.

img {
  width: 360px;
}

 

화면에서는 이미지가 360px로 표시된다.

하지만 CSS를 통해 이미지의 렌더링 크기를 줄이는 것과 실제 다운로드되는 이미지 파일의 크기를 줄이는 것은 다른 문제다.

3840px Original Image
          │
          │ 4MB
          ▼
       Browser
          │
          ▼
   CSS width 360px
          │
          ▼
        Render

 

사용자는 360px 정도의 이미지만 보고 있지만 브라우저는 여전히 원본 이미지를 요청한다.

결과적으로 실제 화면에서 필요한 것보다 훨씬 많은 이미지 데이터를 다운로드하게 된다.

이미지 최적화에서는 화면에서 이미지를 작게 표시하는 것뿐만 아니라 실제 렌더링 크기에 가까운 이미지 파일을 전달하는 것이 중요하다.

이미지 후보 생성

Next.js의 <Image> 컴포넌트는 브라우저가 하나의 원본 이미지만 요청하도록 하는 대신 여러 이미지 후보를 제공할 수 있다.

 

다음과 같은 이미지가 있다고 가정해보자.

import Image from 'next/image';

<Image
  src="/images/product.jpg"
  width={1200}
  height={800}
  alt="상품 이미지"
/>

 

Next.js는 최종 HTML에 srcset을 생성해 브라우저가 선택할 수 있는 이미지 후보를 제공한다.

개념적으로 다음과 같은 형태다.

<img
  src="/_next/image?url=...&w=1200&q=75"
  srcset="
    /_next/image?url=...&w=1200&q=75 1x,
    /_next/image?url=...&w=2400&q=75 2x
  "
/>

 

여기서 중요한 점은 Next.js가 사용자의 디바이스를 확인한 뒤 특정 이미지를 직접 선택해서 보내는 것이 아니라는 것이다.

Next.js는 여러 이미지 후보를 제공하고 실제로 어떤 후보를 다운로드할지는 브라우저가 결정한다.

 

 

예를 들어 동일하게 CSS 기준 400px 크기의 이미지를 표시하더라도 DPR이 1인 디스플레이와 DPR이 2인 디스플레이에서 필요한 이미지 픽셀 수는 달라질 수 있다.

 

브라우저는 자신의 환경을 알고 있기 때문에 Next.js는 여러 후보를 제공하고 최종 선택은 브라우저에 맡기는 구조를 사용한다.

sizes를 이용한 렌더링 크기 전달

반응형 레이아웃에서는 srcset만으로 충분하지 않을 수 있다.

다음과 같이 fill을 사용하는 이미지가 있다고 가정해보자.

<Image
  src="/images/product.jpg"
  fill
  alt="상품 이미지"
/>

 

이 이미지는 실제 레이아웃에 따라 서로 다른 크기로 표시될 수 있다.

Mobile
100vw

Tablet
50vw

Desktop
33vw

 

개발자는 CSS를 통해 이 구조를 알고 있지만 브라우저가 적절한 이미지 후보를 선택할 수 있도록 이미지가 어느 정도 크기로 표시될 것인지에 대한 정보를 전달할 필요가 있다.

이때 sizes를 사용할 수 있다.

<Image
  src="/images/product.jpg"
  fill
  sizes="
    (max-width: 768px) 100vw,
    (max-width: 1200px) 50vw,
    33vw
  "
  alt="상품 이미지"
/>

 

브라우저는 현재 viewport와 sizes를 비교해 이미지가 화면에서 어느 정도의 너비로 표시될지 계산한다.

 

fill을 사용하면서 sizes를 지정하지 않으면 브라우저가 이미지가 viewport 전체 너비를 차지한다고 판단할 수 있다.

이 경우 실제 화면에서는 작은 이미지인데도 필요 이상으로 큰 이미지가 선택될 수 있다.

 

따라서 반응형 이미지에서는 <Image>를 사용하는 것만으로 끝나는 것이 아니라 실제 레이아웃에 맞는 sizes를 함께 제공하는 것이 중요하다.

이미지 규격 관리

사용자의 화면 크기는 매우 다양하다.

모든 크기에 정확하게 대응하는 이미지를 각각 생성한다면 이미지 variant가 지나치게 많아질 수 있다.

 

Next.js는 deviceSizes와 imageSizes를 이용해 이미지 width 후보를 구성한다.

기본 deviceSizes는 다음과 같다.

[640, 750, 828, 1080, 1200, 1920, 2048, 3840]

 

기본 imageSizes는 다음과 같다.

[16, 32, 48, 64, 96, 128, 256, 384]

 

두 설정을 기반으로 Next.js가 사용할 수 있는 이미지 너비 후보가 구성되고 필요한 경우 이를 바탕으로 srcset이 생성된다.

deviceSizes
      +
imageSizes
      │
      ▼
Available Image Widths
      │
      ▼
srcset
      │
      ▼
Browser

 

프로젝트의 이미지 레이아웃이 명확하다면 설정을 조정할 수도 있다.

module.exports = {
  images: {
    deviceSizes: [640, 768, 1024, 1280, 1920],
    imageSizes: [32, 64, 128, 256, 384],
  },
};

 

이미지 후보가 많으면 실제 렌더링 크기에 가까운 이미지를 제공할 가능성이 높아진다.

반대로 후보가 지나치게 많으면 생성 가능한 이미지 variant와 캐시 엔트리도 증가한다.

따라서 이미지 규격은 많을수록 좋은 것이 아니라 실제 서비스의 레이아웃과 사용자 환경에 맞는 범위를 구성하는 것이 중요하다.

브라우저의 이미지 요청

브라우저가 srcset, sizes, viewport, DPR을 이용해 적절한 이미지를 선택하면 해당 이미지를 요청한다.

Next.js 기본 Image Loader를 사용하는 경우 다음과 같은 요청을 확인할 수 있다.

/_next/image
?url=/images/product.jpg
&w=640
&q=75

 

각 값은 이미지 최적화에 필요한 정보를 나타낸다.

url 원본 이미지 위치
w 요청 이미지 너비
q 이미지 품질

 

이 요청을 받은 Next.js Image Optimizer는 원본 이미지를 기반으로 요청에 맞는 이미지를 제공한다.

Browser

640px Image Request
        │
        ▼
/_next/image
        │
        ▼
Image Optimizer
        │
        ▼
Original Image
        │
        ├─ Resize
        ├─ Quality
        └─ Format
        │
        ▼
Optimized Image

이미지 크기 변환

Image Optimizer가 수행하는 중요한 역할 중 하나는 이미지 크기를 줄이는 것이다.

예를 들어 원본 이미지가 다음과 같다고 가정해보자.

3840 × 2160
4MB

 

브라우저가 640px 이미지를 요청했다면 원본 이미지를 그대로 전달할 필요가 없다.

3840px Original
       │
       ▼
Image Optimizer
       │
       ▼
640px Optimized Image
       │
       ▼
Browser

 

이를 통해 브라우저가 실제로 필요한 크기에 가까운 이미지를 다운로드하도록 할 수 있다.

결과적으로 원본 이미지를 그대로 전달하는 것보다 네트워크 전송량을 줄일 수 있다.

이미지 포맷 변환

이미지 파일 크기는 해상도뿐만 아니라 이미지 포맷에도 영향을 받는다.

Next.js Image Optimization의 기본 출력 포맷 설정은 WebP다.

images: {
  formats: ['image/webp']
}

 

필요하다면 AVIF를 추가할 수도 있다.

module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'],
  },
};

 

브라우저는 HTTP 요청의 Accept 헤더를 통해 지원 가능한 이미지 포맷을 전달한다.

Browser

Accept
image/avif
image/webp
...
      │
      ▼
Image Optimizer
      │
      ▼
Supported Format

 

Next.js는 이를 바탕으로 설정된 포맷 중 브라우저가 지원하는 포맷을 사용할 수 있다.

AVIF는 WebP보다 더 작은 결과를 만들 수 있지만 인코딩 비용이 더 클 수 있다.

따라서 포맷 선택에서도 단순히 파일 크기만 볼 것이 아니라 최초 이미지 변환 비용과 캐시 재사용을 함께 고려할 필요가 있다.

최적화 이미지 캐싱

이미지 Resize와 포맷 변환에는 서버 연산이 필요하다.

동일한 이미지에 대해 같은 변환을 요청할 때마다 다시 처리하면 불필요한 연산이 반복된다.

 

Next.js는 최적화된 이미지 결과를 캐싱해 재사용할 수 있도록 한다.

처음 특정 이미지 variant가 요청된 상황을 단순화하면 다음과 같이 이해할 수 있다.

User A
   │
   ▼
640px Request
   │
   ▼
Optimized Result 없음
   │
   ▼
Original Image
   │
   ▼
Resize
Encode
   │
   ▼
640px Optimized Image
   │
   ├─────────── Cache
   │
   ▼
User A

 

동일한 최적화 결과를 다시 사용할 수 있는 상황에서는 기존 결과를 활용할 수 있다.

User B
   │
   ▼
640px Request
   │
   ▼
Cached Result
   │
   ▼
640px Optimized Image

 

이러한 Cache Hit와 Cache Miss 흐름은 일반적인 캐시 관점에서 Next.js의 최적화 이미지 캐싱을 단순화해 표현한 것이다.

최적화 이미지의 캐시 수명은 minimumCacheTTL 설정과 원본 이미지의 Cache-Control 정책 등에 영향을 받는다.

예를 들어 다음과 같이 최소 TTL을 설정할 수 있다.

module.exports = {
  images: {
    minimumCacheTTL: 14400,
  },
};

 

캐시 기간을 길게 설정하면 동일한 이미지 변환 결과를 오랫동안 재사용할 수 있지만 이미지가 변경되는 서비스에서는 오래된 이미지가 유지되는 문제도 고려해야 한다.

이미지 variant와 캐시 효율

Responsive Image에서는 이미지 크기를 세분화할수록 사용자의 실제 렌더링 크기에 가까운 이미지를 제공할 수 있다.

하지만 이미지 규격을 지나치게 많이 만들면 다른 문제가 생길 수 있다.

 

하나의 원본 이미지에 여러 크기와 포맷이 결합되면 생성 가능한 최적화 이미지의 종류도 증가한다.

Original Image

      │
      ├─ 640 WebP
      ├─ 750 WebP
      ├─ 828 WebP
      ├─ 640 AVIF
      ├─ 750 AVIF
      └─ 828 AVIF

사용자에게 정확한 크기의 이미지를 전달한다는 장점이 있지만 캐시해야 하는 variant가 많아질 수 있다.

반대로 제한된 이미지 규격을 여러 요청이 함께 사용하도록 하면 캐시된 이미지가 재사용될 가능성을 높일 수 있다.

 

따라서 이미지 최적화에서는 가장 작은 이미지를 제공하는 것만을 목표로 하기보다 불필요한 이미지 전송과 이미지 variant 수 사이의 균형을 고려할 필요가 있다.

이미지 공간 확보

width와 height는 이미지의 실제 CSS 렌더링 크기를 결정하기 위해서만 존재하는 값이 아니다.

<Image
  src="/product.jpg"
  width={800}
  height={600}
  alt="상품"
/>

 

Next.js는 이 값을 통해 브라우저가 이미지의 intrinsic aspect ratio를 알 수 있도록 한다.

브라우저는 이미지 다운로드가 끝나기 전에도 이미지가 차지할 공간을 확보할 수 있다.

Before Image Load

┌──────────────────────┐
│                      │
│    Reserved Space    │
│                      │
└──────────────────────┘

Product Name
Price
Button

 

이미지가 다운로드된 이후에도 동일한 공간에 이미지가 표시된다.

After Image Load

┌──────────────────────┐
│                      │
│        Image         │
│                      │
└──────────────────────┘

Product Name
Price
Button

 

이미지 영역이 사전에 확보되지 않으면 이미지가 나타난 뒤 주변 콘텐츠가 밀리는 Layout Shift가 발생할 수 있다.

따라서 이미지 크기 정보는 네트워크 최적화와 별개로 CLS를 줄이고 레이아웃을 안정적으로 유지하는 역할도 한다.

초기 이미지 요청 관리

이미지가 많은 페이지에서는 모든 이미지를 페이지 진입과 동시에 요청할 필요가 없다.

상품 이미지가 100개 존재하더라도 첫 화면에서 실제로 보이는 이미지는 일부일 수 있다.

┌────── Viewport ──────┐

Image 1
Image 2
Image 3
Image 4

└──────────────────────┘

Image 5
Image 6
Image 7
...
Image 100

 

화면 아래에 있는 이미지까지 모두 요청하면 초기 네트워크 자원을 불필요하게 사용하게 된다.

Next Image의 기본 loading 값은 lazy이며 브라우저의 native lazy loading을 이용한다.

Initial View

Image 1      Load
Image 2      Load
Image 3      Load

Image 20     Lazy
Image 21     Lazy
Image 22     Lazy

 

화면에서 멀리 떨어진 이미지의 요청을 지연시켜 초기 페이지 로드에서 발생하는 이미지 요청을 줄일 수 있다.

주요 이미지 요청 관리

Lazy Loading이 모든 이미지에 적합한 것은 아니다.

첫 화면의 Hero Image처럼 LCP가 될 가능성이 높은 이미지는 가능한 빠르게 발견되고 요청되는 것이 중요하다.

┌─────────────────────────────┐
│                             │
│         Hero Image          │
│                             │
└─────────────────────────────┘

          LCP Candidate

 

이러한 이미지를 늦게 요청하면 LCP 역시 늦어질 수 있다.

Next.js에서는 상황에 따라 loading="eager", fetchPriority="high" 또는 preload를 이용할 수 있다.

<Image
  src="/hero.jpg"
  fill
  sizes="100vw"
  fetchPriority="high"
  alt="메인 배너"
/>

 

명확한 LCP 이미지나 Hero Image를 미리 발견하도록 해야 하는 상황에서는 preload를 사용할 수도 있다.

<Image
  src="/hero.jpg"
  fill
  sizes="100vw"
  preload
  alt="메인 배너"
/>

 

preload를 사용하면 Next.js는 이미지 preload를 위한 <link>를 문서의 <head>에 추가한다.

다만 모든 주요 이미지에 무조건 preload를 적용하는 것은 적절하지 않다.

 

이미지가 여러 개 preload되면 서로 네트워크 우선순위를 경쟁할 수 있기 때문에 이미지의 중요도와 발견 시점을 기준으로 결정해야 한다.

전체적으로는 다음과 같이 구분할 수 있다.

                 Images
                    │
          ┌─────────┴─────────┐
          │                   │
       Critical          Non Critical
          │                   │
      Hero / LCP          Below Fold
          │                   │
   eager / high             lazy
   preload when
   appropriate

이미지 로딩 상태 표현

이미지 자체의 다운로드 시간을 줄이는 것과 사용자가 느끼는 로딩 경험을 개선하는 것은 별개의 문제다.

Next Image에서는 placeholder="blur"를 이용해 실제 이미지가 로드되기 전에 저해상도 placeholder를 표시할 수 있다.

<Image
  src={productImage}
  alt="상품"
  placeholder="blur"
/>

 

사용자에게는 다음과 같은 과정으로 보인다.

Blur Placeholder
       │
       ▼
Actual Image Loading
       │
       ▼
Full Image

 

Placeholder를 사용한다고 실제 이미지의 네트워크 다운로드 속도가 빨라지는 것은 아니다.

대신 이미지가 표시될 때까지 빈 공간만 노출되는 것을 줄여 사용자가 느끼는 로딩 경험을 개선하는 역할을 한다.

따라서 이미지 최적화를 다룰 때는 실제 네트워크 성능과 사용자 체감 성능을 구분해서 볼 필요가 있다.

Next.js와 브라우저의 역할

전체 과정을 이해하기 위해서는 Next.js와 브라우저가 담당하는 역할을 구분하는 것이 중요하다.

Next.js는 브라우저가 적절한 이미지를 선택할 수 있도록 필요한 정보를 제공하고 요청된 이미지를 최적화한다.

Next.js

Image Candidate 생성
srcset 생성
sizes 전달
Resize
Quality 적용
Format 변환
Optimized Image Cache
Image Size 정보 제공

 

브라우저는 자신의 환경을 기반으로 실제로 다운로드할 이미지를 결정한다.

Browser

Viewport 확인
DPR 확인
sizes 해석
srcset 비교
Image 선택
Image 요청
Decode
Render

 

두 역할을 하나의 흐름으로 연결하면 다음과 같다.

 

Next.js가 사용자의 화면에 맞는 이미지를 직접 선택하는 것이 아니라 브라우저가 적절한 선택을 할 수 있도록 이미지 후보를 제공하고 선택된 요청에 맞는 이미지를 최적화해 제공하는 구조다.

정리

Next.js Image Optimization은 하나의 최적화 기능으로 이루어져 있지 않다.

이미지가 브라우저에 전달되고 화면에 표시되는 여러 단계에서 각각 다른 역할을 수행한다.

 

 

이 과정에서 Next.js가 모든 결정을 대신해주는 것은 아니다.

 

개발자는 실제 레이아웃에 맞는 sizes를 정의하고 어떤 이미지가 LCP 후보인지 판단해야 한다. 서비스 특성에 따라 이미지 규격과 포맷, 캐시 정책도 결정해야 한다.

 

이미지 규격을 지나치게 세분화하면 생성 가능한 variant와 캐시 사용량이 증가할 수 있고 반대로 규격을 너무 제한하면 사용자가 필요로 하는 것보다 큰 이미지를 전달할 가능성이 높아진다.

 

Lazy Loading 역시 모든 이미지에 적용하는 것이 아니라 첫 화면의 중요한 이미지와 이후에 필요한 이미지를 구분해서 적용해야 한다.

Next.js Image Optimization은 단순히 이미지 파일을 압축하는 기능이 아니다.

 

브라우저가 현재 환경에 필요한 이미지를 선택할 수 있도록 적절한 후보를 제공하고 선택된 요청에 맞는 이미지를 생성하며 그 결과를 캐싱해 이미지 전달 과정에서 발생하는 불필요한 네트워크 비용과 반복 연산을 줄이는 구조라고 정리할 수 있다.

참고 자료

  • Next.js Image Component 공식 문서
    Image 컴포넌트의 sizes, preload, loading, placeholder, formats, minimumCacheTTL 등 주요 동작과 설정을 확인할 수 있다. 특히 WebP와 AVIF의 차이, 포맷별 캐시, 이미지 캐시 TTL에 대한 설명도 포함되어 있다.
  • Next.js Image Optimization 가이드
    next/image가 제공하는 이미지 크기 최적화, WebP 제공, Layout Shift 방지, Native Lazy Loading, 원격 이미지 Resize 등 전체적인 Image Optimization 구조를 정리한 공식 가이드다.
  • Next.js Image Configuration 공식 문서
    Next.js 기본 Image Optimization API 대신 외부 이미지 최적화 서비스나 CDN을 사용할 때의 Custom Loader 설정을 확인할 수 있다.