Published on

Next.js 이미지 최적화 느림·깨짐 9가지 원인

Authors

서론

Next.js의 next/image는 “자동 최적화(리사이즈, WebP/AVIF 변환, 지연 로딩, 캐싱)”를 제공하지만, 운영 환경에서는 느려지거나(로딩 지연/서버 부하) 혹은 깨지는(404/500/빈 이미지/레이아웃 튐) 문제가 의외로 자주 발생합니다. 특히 CDN, 리버스 프록시(Nginx), 사설 스토리지(S3 호환), 외부 이미지 호스팅을 섞어 쓰면 작은 설정 하나로 최적화 파이프라인이 무너질 수 있습니다.

이 글은 흔한 증상을 단서로 9가지 원인을 빠르게 좁히고, 재현/검증 포인트와 함께 실전 해결책을 제공합니다. (대상: Next.js 13~15, App Router/Pages Router 공통)


1) images.remotePatterns/domains 누락으로 원격 이미지 차단

증상

  • 개발에서는 보이는데 배포 후 이미지가 깨짐
  • 콘솔에 Invalid src prop 또는 “hostname is not configured” 류 오류
  • / _next/image?url=... 요청이 400/500

원인

Next.js는 보안상 원격 이미지 도메인을 명시적으로 허용해야 합니다. next.config.js에서 images.domains(구 방식) 또는 images.remotePatterns(권장)를 설정하지 않으면 최적화 요청 자체가 실패합니다.

해결

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.example.com',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: 'cdn.example.net',
        pathname: '/assets/**',
      },
    ],
  },
};

module.exports = nextConfig;

검증 포인트: 브라우저에서 깨지는 이미지의 네트워크 탭을 열고, 실제 호출되는 최적화 엔드포인트(/_next/image?...)가 400/500인지 확인합니다.


2) width/height 또는 fill 설정 불일치로 레이아웃 붕괴/빈 이미지

증상

  • 이미지 영역이 0px로 접히거나, CLS(레이아웃 점프)가 심함
  • fill을 썼는데 이미지가 안 보임

원인

next/image는 레이아웃 계산을 위해 width/height 또는 fill+부모 컨테이너 스타일이 필요합니다. 특히 fill은 **부모가 position: relative**이고 명시적인 크기가 있어야 합니다.

해결

import Image from 'next/image'

export default function Hero() {
  return (
    <div style={{ position: 'relative', width: '100%', height: 320 }}>
      <Image
        src="https://images.example.com/hero.jpg"
        alt="hero"
        fill
        sizes="(max-width: 768px) 100vw, 1200px"
        style={{ objectFit: 'cover' }}
        priority
      />
    </div>
  )
}

검증 포인트: 부모 요소의 computed style에서 height가 0이 아닌지, position: relative가 적용됐는지 확인합니다.


3) sizes 미설정으로 과도한 리사이즈/대역폭 낭비 → 느림

증상

  • 모바일에서 특히 느림
  • 이미지가 필요 이상으로 큰 크기로 다운로드됨

원인

fill 또는 반응형 레이아웃에서 sizes를 지정하지 않으면, 브라우저가 큰 리소스를 선택하거나 Next가 불필요하게 큰 변환을 수행할 수 있습니다.

해결

<Image
  src="/product.jpg"
  alt="product"
  width={1200}
  height={800}
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 600px"
/>

검증 포인트: DevTools → Network에서 이미지 리소스의 실제 다운로드 크기와 srcset 선택 결과를 확인합니다.


4) 프록시/CDN이 /_next/image를 캐시하지 않거나, 반대로 잘못 캐시해서 깨짐

증상

  • 첫 요청이 매우 느리고 이후도 계속 느림(캐시 미스 반복)
  • 특정 이미지가 가끔 깨지거나 다른 이미지로 바뀜(캐시 키 충돌)

원인

/_next/image는 쿼리스트링(url, w, q) 기반으로 동작합니다. CDN/프록시가 쿼리스트링을 무시하거나, 캐시 정책이 비정상(캐시 안 함/너무 짧음/키 충돌)일 때 문제가 납니다.

또한 일부 프록시는 긴 URL을 제한해 414/413 류 에러를 유발할 수 있습니다. 이때는 “요청 크기 제한”도 함께 점검해야 합니다. 비슷한 성격의 장애 대응 관점은 Kubernetes API 413 Request Entity Too Large 해결 글의 트러블슈팅 흐름이 참고가 됩니다.

해결(개념)

  • CDN에서 /_next/image 경로를 쿼리스트링 포함 캐시
  • 캐시 키에 url, w, q 포함
  • 오리진의 Cache-Control/ETag/Vary: Accept 존중

Nginx 예시(쿼리 포함 캐시 키를 명시적으로 구성하는 편이 안전):

location /_next/image {
  proxy_pass http://next_upstream;

  # 쿼리스트링 포함 캐시 키
  proxy_cache my_cache;
  proxy_cache_key "$scheme$proxy_host$request_uri";

  proxy_cache_valid 200 301 302 60m;
  add_header X-Cache-Status $upstream_cache_status;
}

검증 포인트: 응답 헤더에 X-Cache-Status(HIT/MISS), Cache-Control, Vary를 확인하고, 같은 URL로 반복 요청 시 HIT가 나는지 확인합니다.


5) Vary: Accept 손실로 WebP/AVIF 협상이 꼬여 “깨진 이미지”처럼 보임

증상

  • Safari/특정 브라우저에서만 이미지가 깨짐
  • 어떤 환경에서는 정상, 어떤 환경에서는 “지원하지 않는 포맷”처럼 표시

원인

Next는 클라이언트의 Accept 헤더에 따라 AVIF/WebP/원본 포맷을 선택합니다. 이때 CDN/프록시가 Vary: Accept를 제거하거나, 잘못된 캐시로 다른 포맷을 내려주면 특정 브라우저에서 디코딩 실패가 발생합니다.

해결

  • Vary: Accept가 오리진에서 내려오면 CDN이 이를 유지하도록 설정
  • CDN이 강제로 이미지 변환을 한다면 Next의 변환과 충돌하지 않게 정리(한쪽만 담당)

검증 포인트: 같은 이미지 URL을 Chrome과 Safari에서 요청했을 때 Content-Type이 어떻게 달라지는지 비교합니다.


6) 외부 이미지 서버가 느리거나 리다이렉트/인증이 끼어 최적화가 병목

증상

  • /_next/image 자체 응답이 느림(TTFB 증가)
  • 원본 이미지 서버가 간헐적으로 302/403/429/5xx

원인

Next의 최적화는 원본 이미지를 가져온 뒤 변환합니다. 원본이 느리면 최적화도 같이 느려집니다.

  • 302 체인(HTTP→HTTPS, 지역 리다이렉트)
  • 서명 URL 만료
  • 핫링크 방지로 403
  • 레이트 리밋(429)

레이트 리밋이 원인이라면 “재시도/백오프/큐잉” 같은 패턴이 근본 대응이 됩니다. 이미지 파이프라인은 다르지만 장애 대응 설계 관점은 OpenAI 429/Rate Limit 대응 - 재시도·백오프·큐잉 글과 유사합니다.

해결

  • 원본 이미지를 CDN에 올려 지리적으로 가깝게 제공
  • 불필요한 리다이렉트 제거
  • 서명 URL을 쓰면 캐시 전략 재설계(만료 전 충분히 캐시되도록)
  • 핫링크 방지 규칙에 /_next/image 오리진을 예외 처리

검증 포인트: /_next/image가 내부적으로 가져오는 원본 URL을 직접 curl로 호출해 응답 시간/리다이렉트/상태코드를 확인합니다.


7) 서버리스/컨테이너에서 이미지 변환이 CPU/메모리 병목 → 타임아웃/500

증상

  • 트래픽이 늘면 이미지가 500으로 깨짐
  • 로그에 타임아웃, 메모리 부족, 프로세스 재시작

원인

AVIF/WebP 변환은 CPU를 많이 쓰고, 큰 원본 이미지는 메모리도 많이 사용합니다. 서버리스 함수 제한(메모리/실행시간)이나 컨테이너 리소스 제한이 낮으면 변환 중 실패합니다.

쿠버네티스 환경에서 OOM이 반복되면 이미지 최적화 워커가 계속 죽어 “간헐적 깨짐”으로 보일 수 있습니다. 이런 경우는 애플리케이션 코드보다 리소스/GC/Limit 튜닝이 우선인 경우가 많습니다. 관련 트러블슈팅 흐름은 EKS Pod OOMKilled 반복 원인과 메모리·GC·Limit 튜닝도 참고할 만합니다.

해결

  • next/image 최적화를 오리진에서 하지 말고 CDN/이미지 전용 서비스로 위임
  • 혹은 변환 품질/사이즈 정책을 제한(너무 큰 w 허용 금지)
  • 런타임 리소스 상향(메모리/CPU), 동시성 제한

검증 포인트: 피크 시간대에 /_next/image의 p95/p99 latency와 5xx 비율, 컨테이너 OOM/재시작 횟수를 함께 봅니다.


8) unoptimized/커스텀 loader 오용으로 “최적화가 안 되거나” URL이 깨짐

증상

  • 이미지가 항상 원본 그대로 내려와 느림
  • 혹은 loader가 만든 URL이 404

원인

  • unoptimized를 켜면 Next 변환 파이프라인을 건너뜁니다.
  • 커스텀 loader는 반드시 올바른 변환 URL 규칙을 만들어야 합니다. (예: Cloudinary, imgix)

해결

Cloudinary 예시:

import Image from 'next/image'

const cloudinaryLoader = ({ src, width, quality }) => {
  const q = quality || 75
  return `https://res.cloudinary.com/<cloud>/image/fetch/f_auto,q_${q},w_${width}/${encodeURIComponent(src)}`
}

export default function Card() {
  return (
    <Image
      loader={cloudinaryLoader}
      src="https://images.example.com/a/b/c.jpg"
      alt="card"
      width={640}
      height={360}
    />
  )
}

검증 포인트: loader가 생성한 최종 URL을 복사해 브라우저에서 직접 열어 200이 나는지 확인합니다.


9) next start가 아닌 방식/엣지 런타임 제약으로 최적화가 비활성/오동작

증상

  • 로컬에서는 되는데 서버에서만 최적화가 동작하지 않음
  • next export(정적 export)에서 next/image가 기대와 다르게 동작

원인

  • next/image의 기본 최적화는 Node 서버 기능에 의존합니다.
  • 정적 export 환경에서는 최적화가 제한되거나 별도 설정이 필요합니다.
  • Edge Runtime에서는 일부 네이티브 모듈/변환 경로가 제약을 받을 수 있고, 배포 플랫폼에 따라 동작이 다릅니다.

해결

  • SSR/Node 런타임으로 운영한다면 표준대로 next build + next start
  • 정적 사이트라면 이미지 최적화는 빌드 타임(사전 변환) 또는 CDN 이미지 변환으로 설계
  • 플랫폼별 권장 방식(Vercel Image Optimization, CloudFront Functions/Lambda@Edge, Cloudflare Images 등) 검토

간단 체크 스크립트(운영 서버에서 실행)로 실제 실행 커맨드/환경변수를 확인해두면 좋습니다.

ps aux | grep next
node -p "process.versions"
node -p "process.env.NEXT_RUNTIME"

빠른 진단 체크리스트(현장용)

  • /_next/image 요청이 400/403/404/500 중 무엇인지 먼저 본다.
  • 깨지는 이미지의 src가 원격이면 remotePatterns부터 확인한다.
  • fill을 썼다면 부모의 position/size를 확인한다.
  • Network에서 실제 다운로드된 이미지 크기가 과한지(sizes 부재) 확인한다.
  • CDN/프록시가 쿼리스트링과 Vary: Accept를 제대로 캐시하는지 확인한다.
  • 원본 이미지 서버의 응답(리다이렉트/인증/레이트리밋)을 직접 호출로 확인한다.
  • 피크 타임에 CPU/메모리/타임아웃으로 변환이 실패하는지 확인한다.

결론

Next.js 이미지 최적화 문제는 “Next가 느리다/깨진다”로 보이지만, 실제로는 (1) 원격 도메인 허용, (2) 레이아웃 파라미터, (3) sizes로 대표되는 리사이즈 정책, (4) 프록시/CDN 캐시와 헤더, (5) 원본 서버 품질, (6) 런타임 리소스로 쪼개서 보면 빠르게 해결됩니다.

운영에서 안정성을 최우선으로 한다면, next/image를 그대로 쓰되 캐시/헤더/리소스 한계를 먼저 정리하고, 트래픽이 커질수록 “오리진 변환”을 줄이는 방향(CDN/이미지 전용 서비스 위임)으로 아키텍처를 발전시키는 것이 가장 비용 대비 효과가 좋습니다.