블로그로 돌아가기

바이브 코더를 위한 Next.js 최적화 (1/3): 이미지 최적화

Next.js Image 컴포넌트가 해결하는 네 가지 문제와 Vercel 비용 구조, 자체 아키텍처 설계까지 정리했어요.

Next.js성능 최적화이미지바이브 코딩

Introduction

AI로 프로젝트를 만들었는데 Lighthouse 점수가 40점대라면, 원인은 높은 확률로 이미지예요.

5MB짜리 JPEG를 그대로 올리면 모바일에서는 로딩에 수 초가 걸리고, 포맷이 낡으면 같은 품질에서도 용량이 2~3배 차이 나죠. 지연 로딩(Lazy Loading)이 없으면 화면 밖 이미지까지 한꺼번에 받아오고, 크기 정보가 빠지면 레이아웃이 갑자기 밀려나는 CLS(Cumulative Layout Shift, 누적 레이아웃 이동)가 발생해요.

정리하면 이미지에는 네 가지 문제가 있어요.

  1. 용량: 원본 그대로 서빙하면 불필요하게 큰 파일을 전송해요.
  2. 포맷: PNG/JPEG보다 WebP/AVIF가 같은 품질에서 훨씬 가벼워요.
  3. 지연 로딩(Lazy Loading): 뷰포트 밖 이미지를 미리 로드하면 초기 로딩이 느려져요.
  4. CLS: 이미지 크기를 브라우저가 모르면 로드 후 레이아웃이 흔들려요.

Next.js의 <Image> 컴포넌트는 이 네 가지를 한 번에 처리해줘요.

이 글의 목표

바이브 코딩으로 만든 프로젝트에서 이미지 최적화가 왜 필요한지 이해하고, Next.js Image 컴포넌트의 설정과 비용 구조까지 파악하는 것이 목표예요.

이 글은 3부작 시리즈의 첫 번째 편이에요.


<img> vs <Image> 비교

HTML의 <img> 태그는 이미지를 있는 그대로 보여줘요. 리사이징, 포맷 변환, Lazy Loading 같은 최적화는 개발자가 직접 처리해야 하죠.

Next.js의 <Image> 컴포넌트는 이 작업을 빌드 타임과 서빙 시점에 자동으로 처리해줘요.

export default function BadPage() {
  return (
    <div>
      <h1>일반 이미지 태그</h1>
      <img
        src="/assets/large-photo.jpg"
        alt="큰 이미지"
        style={{ width: '100%', height: 'auto' }}
      />
    </div>
  );
}

로컬 이미지를 import로 가져오면 width, height, placeholder="blur"까지 자동으로 처리돼요. 외부 이미지는 빌드 타임에 Next.js가 파일 정보를 알 수 없기 때문에 widthheight를 직접 지정해야 해요.

Image 컴포넌트가 해결하는 4가지

  1. 디바이스 크기에 맞는 자동 리사이징: 5MB 원본도 뷰포트에 맞춰 30KB 수준으로 줄여줘요.
  2. WebP/AVIF 자동 변환: 브라우저가 지원하는 최적의 포맷으로 서빙해요. AVIF는 JPEG 대비 50% 이상, WebP는 25-35% 용량을 줄여줘요.
  3. 뷰포트 진입 시 Lazy Loading: 화면에 보일 때만 이미지를 로드해요.
  4. width/height 기반 CLS 방지: 이미지 공간을 미리 확보해서 레이아웃이 밀리지 않아요.

next.config.js 설정

외부 도메인의 이미지를 <Image> 컴포넌트에서 사용하려면 remotePatterns를 설정해야 해요. Next.js는 보안을 위해 기본적으로 외부 이미지를 차단하기 때문이에요.

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

module.exports = nextConfig;

protocol, hostname, pathname을 조합해서 허용할 외부 이미지의 범위를 지정해요. 와일드카드(**)를 사용하면 하위 경로 전체를 허용할 수 있어요.

AI가 생성한 외부 이미지에 주의하세요

AI가 Unsplash, Pexels 같은 외부 이미지를 코드에 넣어줄 때가 있어요. remotePatterns에 해당 도메인이 등록돼 있지 않으면 이미지가 차단됩니다. 빌드 에러 없이 이미지만 안 뜨는 경우가 많으니, 외부 이미지를 사용할 때는 설정을 먼저 확인하세요.


비용 구조

Vercel에 배포한다면 이미지 최적화 비용을 알아둘 필요가 있어요. <Image> 컴포넌트가 서빙하는 이미지는 Vercel의 이미지 최적화 API(Image Optimization API)를 거치는데, 이 API 호출에 비용이 발생해요.

항목Hobby (무료)Pro
Image Transformations월 5,000건1,000건당 $0.05
Image Cache Reads월 300,000건1M건당 $0.40
Image Cache Writes월 100,000건1M건당 $4.00

Image Transformation(이미지 변환)은 원본을 변환하는 작업이고, Cache Write(캐시 쓰기)는 변환 결과를 캐시에 저장하는 작업이에요. Cache Read(캐시 읽기)는 이미 캐시된 이미지를 서빙할 때 발생하죠. Cache Write가 Cache Read보다 10배 비싸기 때문에, 같은 이미지를 반복 변환하지 않는 게 중요해요.

캐시 TTL을 넉넉하게 설정하세요

이미지 URL이 고유하다면(UUID 기반 파일명 등) minimumCacheTTL 값을 길게 잡는 것을 권장해요. 캐시가 만료되면 동일한 이미지도 다시 변환하면서 Cache Write 비용이 발생하기 때문이에요.

next.config.js
const nextConfig = {
  images: {
    minimumCacheTTL: 2592000, // 30일 (초 단위)
  },
};

이미지 최적화 아키텍처

Vercel에 배포하면 이미지 최적화가 내장돼 있지만, 다른 플랫폼(AWS, GCP 등)에 배포한다면 직접 구축해야 해요. 전형적인 아키텍처는 CloudFront + S3 + Lambda 조합이에요.

이미지 최적화 아키텍처

전체 흐름을 단계별로 정리하면 이렇게 돼요.

  1. 사용자가 이미지를 요청해요. URL 쿼리 파라미터(Query Parameter)에 원하는 포맷(webp, avif)과 너비(w=640)를 지정하죠.
  2. CloudFront에서 캐시를 확인해요. 캐시에 있으면 바로 반환하고, 여기서 대부분의 요청이 처리돼요.
  3. 캐시 미스가 발생하면 S3에서 변환된 이미지를 확인해요. 이전에 변환해둔 결과가 있을 수 있기 때문이에요.
  4. S3에도 없으면 Lambda 함수가 원본 이미지를 가져와서 리사이징하고 포맷을 변환해요. 변환 결과를 S3에 저장한 뒤 CloudFront로 서빙하죠.

핵심은 "한 번 변환하고 계속 캐시에서 서빙한다"는 점이에요. Vercel의 Image Optimization API도 내부적으로 비슷한 구조로 동작해요. 차이가 있다면 Vercel은 이 모든 과정을 설정 없이 <Image> 컴포넌트만으로 제공한다는 것이죠.

자체 인프라를 운영하면 비용 통제가 유연해지는 대신 운영 부담이 생겨요. 트래픽 규모와 팀 역량에 따라 판단하면 돼요.


AI로 이미지 최적화 점검하기

바이브 코딩으로 프로젝트를 만들었다면, AI에게 이미지 최적화 상태를 점검해달라고 요청할 수 있어요. 아래 프롬프트를 그대로 사용하면 돼요.

AI 검사 프롬프트
내 Next.js 프로젝트의 이미지 최적화 상태를 점검해줘.

점검 항목:
1. next.config.js의 remotePatterns 설정이 보안상 안전한가?
2. <Image /> 컴포넌트에 width, height가 CLS를 방지하도록 명시돼 있는가?
3. LCP 요소에 priority 속성이 적절히 부여돼 있는가?
4. placeholder 처리가 돼 있는가?

각 항목별로 현재 상태와 개선 코드를 제시해줘.

LCP(Largest Contentful Paint, 최대 콘텐츠풀 페인트) 요소는 보통 페이지에서 가장 큰 이미지예요. 히어로 배너나 메인 이미지가 해당되는 경우가 많은데, 이런 이미지에는 priority 속성을 추가해서 Lazy Loading을 비활성화하고 즉시 로드하도록 해야 해요.

<Image
  src={heroImage}
  alt="히어로 배너"
  priority
/>

priority를 지정하면 <head><link rel="preload">가 자동으로 추가돼요. 사용자가 처음 보는 화면의 핵심 이미지가 빠르게 표시되도록 도와주죠.


TL;DR

  • HTML <img> 대신 Next.js <Image> 컴포넌트를 쓰면 리사이징, 포맷 변환, Lazy Loading, CLS 방지가 자동으로 처리돼요.
  • 외부 이미지를 사용할 때는 next.config.jsremotePatterns에 도메인을 등록해야 해요.
  • Vercel 배포 시 Cache Write 비용이 가장 비싸니, minimumCacheTTL을 넉넉하게 설정해서 불필요한 재변환을 줄이세요.
  • LCP에 해당하는 이미지에는 priority 속성을 추가해서 즉시 로드되도록 하세요.

참고 자료

같이 읽으면 좋은 글