내 이미지가 다 엑박이네? (Next.js Image 보안 에러와 최적화 원리)
1. "HTML에서는 잘 나오는데..."
사용자 프로필 사진 기능을 만들고 있었습니다. AWS S3에 이미지를 업로드하고, URL을 받아서 화면에 뿌렸죠.
/* 잘 작동함 */
<img src="https://my-bucket.s3.ap-northeast-2.amazonaws.com/avatar.jpg" />
"오케이, 이제 최적화해야지."
Lighthouse 점수를 높이기 위해 Next.js의 <Image> 컴포넌트로 바꿨습니다.
/* 에러 발생! */
import Image from 'next/image';
<Image
src="https://my-bucket.s3.ap-northeast-2.amazonaws.com/avatar.jpg"
width={100}
height={100}
alt="User Avatar"
/>
그러자 화면의 이미지가 사라지고(엑박), 콘솔에는 무시무시한 에러가 떴습니다.
Error: Invalid src prop. Hostname "my-bucket..." is not configured under images in your next.config.js
2. 왜 막는 걸까? (Next.js는 프록시 서버다)
처음엔 화가 났습니다. "아니, 그냥 보여주면 되지 왜 허락을 받아야 해?" 하지만 원리를 알고 나니 납득이 갔습니다.
<Image> 컴포넌트는 브라우저가 이미지를 직접 다운로드하는 게 아닙니다.
Next.js 서버가 중간에서 이미지를 다운로드하고, 리사이징(최적화)한 뒤에 브라우저에 줍니다.
브라우저 <-> Next.js 서버(Image Optimizer) <-> AWS S3 (원본)
보안 시나리오 - 내 서버가 공격자가 된다면?
만약 Next.js가 모든 도메인을 허용한다면 어떻게 될까요? 해커가 제 서버를 공격용 프록시로 쓸 수 있습니다.
- 해커가 제 서버에 요청을 보냅니다:
/_next/image?url=https://victim-site.com/huge-file.jpg&w=1080&q=75 - 제 Next.js 서버는 순진하게
victim-site.com에서 파일을 다운로드합니다. - 해커가 이 요청을 1초에 10만 번 보냅니다.
- 결과:
victim-site.com은 DDoS 공격을 받고, 제 서버의 CPU와 대역폭 요금은 폭발합니다.
그래서 Next.js는 "주인이 허락한 도메인(Allowlist)의 이미지만 처리하겠다"고 막는 겁니다. 이건 단순한 설정 에러가 아니라, 서버 자원을 보호하기 위한 방화벽입니다.
3. 해결책 - remotePatterns (더 안전하게)
예전에는 domains 배열을 썼지만, Next.js 13부터는 remotePatterns를 권장합니다.
도메인뿐만 아니라 프로토콜, 포트, 경로까지 제어할 수 있어서 훨씬 안전합니다.
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'my-bucket.s3.ap-northeast-2.amazonaws.com',
port: '',
pathname: '/uploads/**', // uploads 폴더 아래만 허용!
},
{
protocol: 'https',
hostname: '*.googleusercontent.com', // 구글 로그인 프로필 사진 (와일드카드 가능)
}
],
},
};
module.exports = nextConfig;
이렇게 설정하고 서버를 재시작(npm run dev)하면 에러가 사라집니다.
(설정 파일인 next.config.js를 수정하면 반드시 서버를 껐다 켜야 합니다. 이것 때문에 1시간 날른 적이 있습니다...)
이미지 최적화는 어떻게 작동하나? 더 알아보기
Next.js 서버 내부에서는 무슨 일이 일어날까요?
오픈소스인 next/image 코드를 까보면 대략 이런 과정이 일어납니다.
- 요청 수신: 브라우저가
/_next/image?url=...&w=640&q=75로 요청을 보냅니다. - 캐시 확인: 이미 변환된 파일이
.next/cache/images폴더에 있는지 확인합니다. 있으면 바로 반환(HIT). - 다운로드: 없으면 원본 서버(S3 등)에서 이미지를 다운로드합니다.
- 변환(Transform):
- Resizing: 요청된 너비(
w=640)에 맞춰 크기를 줄입니다. (원본이 4000px이어도 640px로 줍니다!) - Format: 브라우저가 WebP를 지원하면 WebP로, AVIF를 지원하면 AVIF로 변환합니다.
- Resizing: 요청된 너비(
- 응답: 변환된 이미지를 보내고, 동시에 캐시에 저장합니다.
이 모든 과정이 CPU를 꽤 많이 씁니다. 그래서 Vercel 무료 티어에는 이미지 최적화 한도(1000건)가 있습니다.
실제로는 이 비용을 아끼기 위해 Cloudinary 같은 전용 서비스를 쓰거나, 아예 next export를 하고 최적화를 포기하기도 합니다.
5. 자주 묻는 질문 (FAQ)
Q. 이미지가 흐릿하게 보여요!
<Image> 컴포넌트는 로딩 중에 보여줄 Blur Placeholder를 자동으로 생성합니다.
이미지가 다 로드되었는데도 흐릿하다면, placeholder="blur" 속성을 썼는데 blurDataURL을 제대로 안 넣어줬거나, 스타일링 문제일 수 있습니다.
Q. 로컬(localhost)에서는 이미지가 왜 이렇게 느리게 뜨죠?
개발 모드(npm run dev)에서는 캐시를 안 하기 때문입니다.
새로고침할 때마다 매번 S3에서 다운받고 변환하는 과정을 반복합니다.
프로덕션 빌드(npm run start)에서는 캐시가 작동하므로 훨씬 빠릅니다.
Q. 외부 이미지(Unsplash 등)가 자꾸 깨져요.
외부 서비스가 도메인을 바꾸거나 리다이렉트를 하면 깨질 수 있습니다. 가장 안전한 방법은 외부 이미지를 믿지 말고, 내 서버(S3)로 업로드해서 서빙하는 것입니다.
6. 마무리 - 보안은 불편하다, 그래서 안전하다
개발하다 보면 "보안 설정"들이 귀찮게 느껴질 때가 많습니다. CORS 에러가 그렇고, 이 Image Domain 에러가 그렇습니다.
하지만 이 불편함이 내 서버가 해커들의 놀이터가 되는 걸 막아주고 있습니다. 빨간 에러 메시지를 보면 짜증 내지 말고, "Next.js가 내 지갑을 지켜주고 있구나"라고 생각합시다.
My Images Disappeared and Console Turned Red (Next.js Image Security)
1. "It Worked perfectly with HTML..."
I was building a user profile feature. I uploaded an image to AWS S3 and displayed it using the URL.
/* Works Fine */
<img src="https://my-bucket.s3.ap-northeast-2.amazonaws.com/avatar.jpg" />
"Okay, let's optimize it."
I switched to Next.js's <Image> component to boost my Core Web Vitals.
/* ERROR! */
import Image from 'next/image';
<Image
src="https://my-bucket.s3.ap-northeast-2.amazonaws.com/avatar.jpg"
width={100}
height={100}
alt="Avatar"
/>
Suddenly, the image turned into a broken icon, and the console screamed.
Error: Invalid src prop. Hostname "my-bucket..." is not configured under images in your next.config.js
2. Why Block It? (Next.js is a Proxy)
At first, I was annoyed. "Why do I need permission just to show an image? Isn't it just a link?" But once I understood the underlying mechanism, it made perfect sense.
The <Image> component doesn't just let the browser download the image directly.
The Next.js Server downloads the image, resizes (optimizes) it using a library like Sharp, and THEN sends it to the browser.
Browser <-> Next.js Server (Optimizer) <-> AWS S3 (Source)
Security Scenario: DoSing Yourself
What if Next.js allowed ANY domain by default? A hacker could use your server as an Attack Proxy.
- Hacker sends thousands of requests for
<Image src="https://victim-site.com/huge-file.jpg" />. - My Next.js Server obediently fetches that file from
victim-site.com. - My server essentially launches a DDoS attack on the victim.
- Result: My server's CPU melts down, and my AWS bandwidth bill skyrockets.
That's why Next.js says "I will only process images from domains you explicitly allow (Allowlist)." It is not a bug; it is a Firewall protecting your infrastructure.
3. Solution: remotePatterns (The Safer Way)
We used to use domains in the config, but now remotePatterns is the recommended standard.
It allows you to restrict access not just by domain, but by protocol, port, and even specific paths.
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'my-bucket.s3.ap-northeast-2.amazonaws.com',
port: '',
pathname: '/uploads/**', // Only allow the /uploads folder!
},
{
protocol: 'https',
hostname: '*.googleusercontent.com', // Allow massive subdomains like Google User Content
}
],
},
};
module.exports = nextConfig;
Configure this and Restart the Server (npm run dev). The error will vanish.
(Seriously, restart it. next.config.js changes are not hot-reloaded.)
4. Deep Dive: How Optimization Works Under the Hood
What happens inside the black box of next/image?
- Request: The browser requests
/_next/image?url=...&w=640&q=75. - Cache Check: It checks the file system (
.next/cache/images) for a pre-processed version. If found, it returns immediately (HIT). - Fetch: If not found (MISS), it downloads the original full-size image from the source (S3).
- Transform:
- Resizing: It shrinks a 4000px photo down to the requested 640px. This saves massive bandwidth for mobile users.
- Transcoding: It converts JPEGs/PNGs into modern formats like WebP or AVIF if the browser supports them.
- Response: It serves the optimized image and caches it for future requests.
This process is CPU-intensive. That's why platforms like Vercel have limits on Image Optimization (e.g., 1000 source images/month on the free tier).
5. Troubleshooting Guide
Q. My images are blurry!
Next.js generates a Blur Placeholder while the image loads.
If the image stays blurry forever, you might be using the placeholder="blur" prop without providing a valid blurDataURL for dynamic images. Or your CSS might be stretching a tiny thumbnail.
Q. It's so slow on localhost!
In development mode (npm run dev), Next.js disables caching to help you see changes immediately.
This means every page refresh triggers the download-resize-convert cycle.
In production (npm run start), the cache kicks in, and it becomes instant.
Q. What about Unsplash?
If you rely on random image services like Unsplash, their domains can change or redirect. Host critical assets on your own storage (S3, R2, Cloudinary) to ensure reliability.
6. Conclusion: Security is Inconvenient, That's Why It's Safe
Security settings like CORS and Image Domains always feel like hurdles during development. You just want the feature to work. But this inconvenience is what prevents your server from becoming a zombie in a botnet.
Why Optimization Matters (Beyond Security)
By forcing you to use <Image>, Next.js ensures:
- Faster LCP (Largest Contentful Paint): Critical for SEO.
- No Layout Shift: Required width/height prevents "Jumping content," improving CLS scores.
- Automatic Format Selection: Chrome gets WebP, Safari gets what it needs.
So when you see that red error message, don't get mad. Think, "Next.js is protecting my wallet and my user's data plan."