Published on

Next.js 이미지 최적화 실패? remotePatterns·403 해결

Authors

서버 렌더링(SSR)과 성능 최적화를 위해 Next.js의 next/image를 도입했는데, 배포 후 갑자기 이미지가 안 뜨거나 콘솔에 Image Optimization using Next.js' default loader is not compatible with ... 류의 메시지, 혹은 네트워크 탭에서 403이 떨어지는 상황을 자주 만납니다. 특히 외부 도메인(예: S3, CloudFront, 사내 CDN, 인증이 걸린 이미지 서버)에서 이미지를 가져오는 경우 remotePatterns 설정과 원격 서버의 접근 정책이 동시에 얽히면서 문제를 더 복잡하게 만듭니다.

이 글에서는 (1) Next.js 설정(remotePatterns) 문제(2) 원격 서버가 Next.js 최적화 요청을 403으로 거부하는 문제를 분리해서, 재현 → 진단 → 해결까지 한 번에 정리합니다.

증상: “이미지 최적화 실패”는 대부분 두 갈래다

Next.js의 이미지 최적화는 브라우저가 원본 이미지를 직접 받는 구조가 아니라, 보통 아래처럼 동작합니다.

  1. 페이지에서 <Image src="https://remote/image.jpg" /> 렌더링
  2. 브라우저는 실제로 /_next/image?url=<encoded>&w=...&q=... 로 요청
  3. Next.js 서버(또는 Vercel/런타임)가 원격 url로 원본 이미지를 서버 측에서 fetch
  4. 최적화(리사이즈/포맷 변환) 후 브라우저로 전달

따라서 실패 원인은 크게 두 가지입니다.

  • A. Next.js가 원격 URL을 허용하지 않음: next.config.jsimages.remotePatterns/domains 설정 누락 또는 패턴 불일치
  • B. 원격 서버가 Next.js 서버의 fetch를 거부: 핫링크 차단, User-Agent/Referer 기반 차단, 서명 URL/쿠키 필요, WAF, IP allowlist, S3/CloudFront 정책 등으로 403

특히 B는 “브라우저에서 직접 열면 잘 보이는데 Next.js에서만 403” 같은 형태로 나타납니다. 이때는 브라우저가 아니라 Next.js 서버가 요청 주체라는 점이 핵심입니다.

1) remotePatterns 제대로 설정하기 (가장 흔한 원인)

remotePatterns vs domains: 무엇을 써야 하나?

  • images.domains: 호스트만 허용(구버전부터 많이 사용). 경로/프로토콜/포트 제한이 약함.
  • images.remotePatterns: 프로토콜/호스트/포트/패스까지 패턴으로 제한. 보안적으로 권장.

가능하면 remotePatterns를 쓰는 게 안전합니다.

예시: S3/CloudFront/사내 CDN 패턴 허용

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example-bucket.s3.ap-northeast-2.amazonaws.com',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: 'd111111abcdef8.cloudfront.net',
        pathname: '/images/**',
      },
      {
        protocol: 'https',
        hostname: 'cdn.company.internal',
        pathname: '/assets/**',
      },
    ],
  },
}

module.exports = nextConfig

자주 하는 실수 체크리스트

  • protocol 누락 또는 http/https 불일치
  • hostname에 와일드카드 기대(버전에 따라 제한적). 안전하게 정확한 호스트를 명시
  • pathname가 실제 URL과 불일치(예: /image/**로 해놓고 실제는 /images/...)
  • 이미지 URL에 쿼리스트링이 붙는 경우(예: ?v=123)는 대개 pathname과 무관하지만, 서버 측에서 서명 검증 로직이 쿼리에 민감하면 403의 단초가 됩니다.

개발 환경에서는 되는데 배포에서만 실패?

로컬에서는 next dev가 느슨하게 보이거나, 이미지가 캐시된 상태로 보일 수 있습니다. 배포 환경(특히 서버리스/엣지)에서는 정확히 설정된 패턴만 통과하므로 remotePatterns를 기준으로 다시 검증하세요.

2) 403의 본질: Next.js가 “서버에서” 이미지를 가져오다 막힌다

/_next/image는 Next.js 서버가 원격 이미지를 fetch합니다. 즉, 원격 서버 입장에서는 요청이 이렇게 보입니다.

  • 클라이언트 IP가 아니라 Next.js 서버 IP(Vercel이면 Vercel egress)
  • Referer가 없거나 기대와 다름
  • User-Agent가 브라우저가 아니라 Node/undici 계열
  • 쿠키/세션/Authorization 헤더가 없음

따라서 “브라우저에서 직접 열면 200인데, /_next/image만 403”이면 원격 서버의 정책을 의심해야 합니다.

2-1) 진단: Next.js가 실제로 어떤 URL을 요청하는지 확인

브라우저 네트워크 탭에서 이미지 요청 URL이 보통 다음 형태입니다.

/_next/image?url=https%3A%2F%2Fd111111abcdef8.cloudfront.net%2Fimages%2Fcat.jpg&w=640&q=75

이때 서버 로그(Next.js 런타임 로그)에서 403이 난다면, 원격 서버가 거부한 것입니다.

로컬에서도 동일하게 재현하려면, 원격 이미지 URL을 서버에서 직접 fetch해보세요.

node -e "fetch('https://remote.example.com/images/cat.jpg').then(r=>console.log(r.status)).catch(console.error)"

여기서도 403이면 Next.js 문제가 아니라 원격 서버 접근 정책 문제가 맞습니다.

2-2) CloudFront/S3에서 흔한 403 원인

  • S3 버킷이 퍼블릭이 아니고 OAC/OAI로 CloudFront만 허용하는데, Next.js가 S3로 직접 접근
  • CloudFront가 Signed URL / Signed Cookie를 요구
  • WAF/보안 정책이 특정 User-Agent/헤더/지역/IP를 차단
  • 핫링크 방지: Referer 기반 차단

S3 403의 원인을 더 넓게 점검해야 할 때는 아래 글의 체크리스트가 도움이 됩니다.

(Next.js가 EKS에서 떠 있거나, 이미지가 S3 기반이면 진단 포인트가 겹칩니다.)

3) 해결 전략 1: “최적화는 하되” 원격 서버가 허용하도록 맞춘다

3-1) 이미지 서버에 핫링크/UA 차단이 있다면 정책 수정

원격 서버가 User-Agent가 브라우저가 아닐 때 막는 경우가 있습니다. Next.js는 내부적으로 fetch를 사용하므로 UA가 일반 브라우저와 다를 수 있습니다.

  • 가능하면 특정 UA를 허용하거나
  • 특정 경로(/images/)만 공개**하고 나머지는 보호
  • 또는 Next.js 서버의 egress IP allowlist(고정 IP가 가능한 인프라에서)

다만 Vercel처럼 egress IP가 고정되지 않는 환경이면 allowlist 전략이 어려울 수 있습니다.

3-2) Signed URL이 필요한 이미지는 next/image 최적화가 구조적으로 불리할 수 있다

Signed URL은 만료 시간이 있고, 요청 주체/헤더 조건이 붙기도 합니다. Next.js 최적화는 서버가 원본을 다시 가져오기 때문에:

  • 서명 만료로 403
  • 서명 검증 조건 불일치
  • 캐시된 최적화 이미지와 원본 URL 만료 타이밍 불일치

이 경우는 아래 “해결 전략 2”로 우회하는 편이 실전에서 더 안정적입니다.

4) 해결 전략 2: 최적화 파이프라인을 바꾼다(우회/대체)

4-1) 특정 이미지만 unoptimized로 처리

원격 정책 때문에 최적화가 실패한다면, 해당 이미지만 최적화를 끄고 브라우저가 원본을 직접 받게 할 수 있습니다.

import Image from 'next/image'

export default function Avatar() {
  return (
    <Image
      src="https://secure.example.com/avatar?id=123"
      alt="avatar"
      width={80}
      height={80}
      unoptimized
    />
  )
}

장점: 빠른 해결, 403 회피 가능 단점: 리사이즈/포맷 변환 이점 상실(대신 CDN에서 최적화 제공하면 괜찮음)

4-2) loader로 “이미 최적화된 CDN URL”을 반환

이미지 CDN(Cloudinary, Imgix, Akamai Image Manager, CloudFront Functions/Lambda@Edge 기반 변환 등)을 쓰는 경우, Next.js 서버가 원본을 fetch하지 않도록 loader에서 변환 URL을 직접 만들어 브라우저가 CDN을 바로 치게 할 수 있습니다.

import Image, { ImageLoaderProps } from 'next/image'

const cdnLoader = ({ src, width, quality }: ImageLoaderProps) => {
  const q = quality ?? 75
  // 예: https://cdn.example.com/resize?url=<src>&w=<width>&q=<q>
  return `https://cdn.example.com/resize?url=${encodeURIComponent(src)}&w=${width}&q=${q}`
}

export default function ProductImage() {
  return (
    <Image
      loader={cdnLoader}
      src="https://origin.example.com/images/p1.jpg"
      alt="p1"
      width={800}
      height={600}
    />
  )
}

이 방식은 Next.js의 기본 최적화 엔드포인트(/_next/image)를 우회하므로, 원격 origin이 Next.js 서버를 403으로 막는 문제를 피해갈 수 있습니다.

4-3) 아예 next/image 대신 <img>를 쓰는 게 나은 경우

  • 인증이 필요한 이미지(쿠키/세션 기반)
  • 사용자별로 권한이 달라지는 이미지
  • 매우 짧은 TTL의 서명 URL

이런 케이스는 next/image 최적화가 오히려 장애 포인트가 됩니다. 성능은 CDN/브라우저 캐시/loading="lazy"로 보완하고, 접근 제어는 원격 서버에서 일관되게 처리하는 편이 안정적입니다.

5) 운영 환경에서의 체크 포인트(로그/인프라 관점)

5-1) Next.js가 떠 있는 환경의 네트워크 제약

EKS/사내망에서 Next.js를 운영한다면, 원격 이미지 서버로 나가는 egress가 NAT/프록시/WAF를 거치며 403/401/timeout이 섞여 보일 수 있습니다. 특히 “인증 실패처럼 보이는 네트워크 문제”도 많습니다.

5-2) 403이 아니라 504/timeout로 보인다면

원격 이미지 서버가 느리거나, ALB/Ingress의 idle timeout에 걸리면 이미지 최적화 요청이 타임아웃으로 실패합니다. 겉으로는 “이미지 최적화가 안 됨”으로 보이지만 실제는 네트워크/타임아웃 문제일 수 있습니다.

6) 빠른 해결을 위한 결론: 원인별 처방전

A. 에러가 “허용되지 않은 호스트/패턴”이면

  1. next.config.jsimages.remotePatterns에 프로토콜/호스트/패스를 정확히 추가
  2. 배포 후 재시작(빌드 산출물에 반영 필요)

B. /_next/image에서만 403이면

  1. 서버에서 fetch(remoteUrl)로 403 재현 → 원격 서버 정책 문제 확정
  2. 원격 서버에서 핫링크/UA/서명/쿠키 요구사항 점검
  3. 해결이 어렵다면
    • 해당 이미지만 unoptimized 적용
    • 또는 커스텀 loader로 이미지 CDN을 직접 호출
    • 또는 <img>로 전환

Next.js 이미지 최적화는 강력하지만, **원격 리소스를 “서버가 대신 가져오는 구조”**라는 사실 때문에 보안 정책과 충돌하기 쉽습니다. remotePatterns로 허용 범위를 정확히 잡고, 403이 뜨면 “Next.js 서버가 원격에서 거부당했다”는 관점으로 원격 정책/서명/핫링크 방지를 우선 점검하면 대부분 빠르게 해결됩니다.