
내 이미지가 다 엑박이네? (Next.js Image 보안 에러와 최적화 원리)
멀쩡하던 이미지가 Next.js의 `<Image>` 컴포넌트를 쓰자마자 에러를 뿜어냈습니다. 단순한 설정 문제인 줄 알았지만, 알고 보니 Next.js가 서버 자원을 보호하기 위한 강력한 보안 장치였습니다. `remotePatterns` 설정 방법과 Image Optimization의 내부 동작 원리를 깊이 있게 파헤칩니다.

멀쩡하던 이미지가 Next.js의 `<Image>` 컴포넌트를 쓰자마자 에러를 뿜어냈습니다. 단순한 설정 문제인 줄 알았지만, 알고 보니 Next.js가 서버 자원을 보호하기 위한 강력한 보안 장치였습니다. `remotePatterns` 설정 방법과 Image Optimization의 내부 동작 원리를 깊이 있게 파헤칩니다.
프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

분명히 클래스를 적었는데 화면은 그대로다? 개발자 도구엔 클래스가 있는데 스타일이 없다? Tailwind 실종 사건 수사 일지.

안드로이드는 Xcode보다 낫다고요? Gradle 지옥에 빠져보면 그 말이 쏙 들어갈 겁니다. minSdkVersion 충돌, Multidex 에러, Namespace 변경(Gradle 8.0), JDK 버전 문제, 그리고 의존성 트리 분석까지 완벽하게 해결해 봅니다.

Debug에선 잘 되는데 Release에서만 죽나요? 범인은 '난독화'입니다. R8의 원리, Mapping 파일 분석, 그리고 Reflection을 사용하는 라이브러리를 지켜내는 방법(@Keep)을 정리해봤습니다.

사용자 프로필 사진 기능을 만들고 있었습니다. 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
처음엔 화가 났습니다. "아니, 그냥 보여주면 되지 왜 허락을 받아야 해?" 하지만 원리를 알고 나니 납득이 갔습니다.
<Image> 컴포넌트는 브라우저가 이미지를 직접 다운로드하는 게 아닙니다.
Next.js 서버가 중간에서 이미지를 다운로드하고, 리사이징(최적화)한 뒤에 브라우저에 줍니다.
브라우저 <-> Next.js 서버(Image Optimizer) <-> AWS S3 (원본)
만약 Next.js가 모든 도메인을 허용한다면 어떻게 될까요? 해커가 제 서버를 공격용 프록시로 쓸 수 있습니다.
/_next/image?url=https://victim-site.com/huge-file.jpg&w=1080&q=75victim-site.com에서 파일을 다운로드합니다.victim-site.com은 DDoS 공격을 받고, 제 서버의 CPU와 대역폭 요금은 폭발합니다.그래서 Next.js는 "주인이 허락한 도메인(Allowlist)의 이미지만 처리하겠다"고 막는 겁니다. 이건 단순한 설정 에러가 아니라, 서버 자원을 보호하기 위한 방화벽입니다.
예전에는 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).w=640)에 맞춰 크기를 줄입니다. (원본이 4000px이어도 640px로 줍니다!)이 모든 과정이 CPU를 꽤 많이 씁니다. 그래서 Vercel 무료 티어에는 이미지 최적화 한도(1000건)가 있습니다.
실제로는 이 비용을 아끼기 위해 Cloudinary 같은 전용 서비스를 쓰거나, 아예 next export를 하고 최적화를 포기하기도 합니다.
<Image> 컴포넌트는 로딩 중에 보여줄 Blur Placeholder를 자동으로 생성합니다.
이미지가 다 로드되었는데도 흐릿하다면, placeholder="blur" 속성을 썼는데 blurDataURL을 제대로 안 넣어줬거나, 스타일링 문제일 수 있습니다.
개발 모드(npm run dev)에서는 캐시를 안 하기 때문입니다.
새로고침할 때마다 매번 S3에서 다운받고 변환하는 과정을 반복합니다.
프로덕션 빌드(npm run start)에서는 캐시가 작동하므로 훨씬 빠릅니다.
외부 서비스가 도메인을 바꾸거나 리다이렉트를 하면 깨질 수 있습니다. 가장 안전한 방법은 외부 이미지를 믿지 말고, 내 서버(S3)로 업로드해서 서빙하는 것입니다.
개발하다 보면 "보안 설정"들이 귀찮게 느껴질 때가 많습니다. CORS 에러가 그렇고, 이 Image Domain 에러가 그렇습니다.
하지만 이 불편함이 내 서버가 해커들의 놀이터가 되는 걸 막아주고 있습니다. 빨간 에러 메시지를 보면 짜증 내지 말고, "Next.js가 내 지갑을 지켜주고 있구나"라고 생각합시다.
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
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)
What if Next.js allowed ANY domain by default? A hacker could use your server as an Attack Proxy.
<Image src="https://victim-site.com/huge-file.jpg" />.victim-site.com.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.
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.)
What happens inside the black box of next/image?
/_next/image?url=...&w=640&q=75..next/cache/images) for a pre-processed version. If found, it returns immediately (HIT).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).
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.
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.
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.
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.
By forcing you to use <Image>, Next.js ensures:
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."