
Next.js를 S3에 배포했다가 백지 화면이 떴다 (Static Export의 악몽)
Vercel 대신 AWS S3에 정적 배포(Static Export)를 시도했다가 겪은 세 가지 악몽(이미지 최적화, API 라우트, 동적 라우팅)과 그 해결책을 공유합니다. '서버 없는 Next.js'가 어떤 제약이 있는지 확실히 이해하게 될 것입니다.

Vercel 대신 AWS S3에 정적 배포(Static Export)를 시도했다가 겪은 세 가지 악몽(이미지 최적화, API 라우트, 동적 라우팅)과 그 해결책을 공유합니다. '서버 없는 Next.js'가 어떤 제약이 있는지 확실히 이해하게 될 것입니다.
서버를 끄지 않고 배포하는 법. 롤링, 카나리, 블루-그린의 차이점과 실제 구축 전략. DB 마이그레이션의 난제(팽창-수축 패턴)와 AWS CodeDeploy 활용법까지 심층 분석합니다.

새벽엔 낭비하고 점심엔 터지는 서버 문제 해결기. '택시 배차'와 '피자 배달' 비유로 알아보는 오토 스케일링과 서버리스의 차이, 그리고 Spot Instance를 활용한 비용 절감 꿀팁.

내 서버가 해킹당하지 않는 이유. 포트와 IP를 검사하는 '패킷 필터링'부터 AWS Security Group까지, 방화벽의 진화 과정.

왜 넷플릭스는 멀쩡한 서버를 랜덤하게 꺼버릴까요? 시스템의 약점을 찾기 위해 고의로 장애를 주입하는 카오스 엔지니어링의 철학과 실천 방법(GameDay)을 소개합니다.

처음엔 Vercel을 썼습니다. 정말 편하고 빨랐죠. git push만 하면 배포가 되니까요.
하지만 프로젝트가 커지고 트래픽이 늘어나자, Vercel의 청구서(Pro Plan, Bandwidth)가 무서워지기 시작했습니다.
"그냥 HTML로 빌드해서 AWS S3에 올리고 CloudFront 붙이면 천 원도 안 나오지 않나?"
저는 호기롭게 next.config.js에 한 줄을 추가했습니다.
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // "HTML로 다 뱉어내라!"
};
그리고 npm run build를 누르는 순간, 지옥문이 열렸습니다.
단순히 HTML 파일만 생기는 게 아니라, 제가 사랑하던 Next.js의 기능들이 하나둘씩 죽어나갔기 때문입니다.
빌드 로그에 빨간 글씨가 떴습니다.
Error: Image Optimization using Next.js default loader is not compatible with "next export".
Next.js의 <Image /> 컴포넌트는 사실 백엔드 서버가 필요합니다.
사용자가 브라우저에서 이미지를 요청하면 -> Next.js 서버가 원본 이미지를 가져와서 -> 그 즉시(On-demand) 리사이징하고 -> WebP/AVIF로 변환해서 줍니다.
하지만 S3는 서버가 아니라 그냥 '파일 창고'입니다. 리사이징해 줄 요리사가 없는 거죠.
두 가지 길이 있습니다.
1. 가난한 해결책: 최적화 끄기 화질과 로딩 속도를 포기하고, 그냥 원본 이미지를 보여주는 겁니다.
// next.config.js
module.exports = {
images: { unoptimized: true }
}
이렇게 하면 <Image /> 컴포넌트가 그냥 <img> 태그처럼 동작합니다. 배포는 되지만, 성능 점수(Lighthouse)는 바닥을 칩니다.
2. 부자 해결책: 외부 이미지 서비스 (Loader) 사용 Cloudinary, Imgix 같은 전문 이미지 CDN을 쓰는 겁니다.
// next.config.js
module.exports = {
images: {
loader: 'custom',
loaderFile: './my-loader.ts',
},
}
Next.js는 URL만 만들어주고, 실제 변환은 외부 서비스가 담당합니다. 돈은 들지만 성능은 유지됩니다.
빌드에 성공하고 S3에 올렸습니다. 메인 화면이 떴습니다! "오, 성공인가?" 하지만 로그인 버튼을 누르자마자 404 Not Found가 떴습니다.
/api/login 경로가 감쪽같이 사라진 겁니다.
Next.js의 API Routes (/app/api/...)는 Node.js 서버(또는 Edge Runtime)가 있어야 돌아갑니다.
output: 'export'를 하면 Next.js는 HTML, CSS, JS 파일만 남기고 서버 관련 코드를 다 버립니다.
당연히 API 핸들러 함수들도 다 삭제되죠. HTML 파일 안에 백엔드 로직을 넣을 수는 없으니까요.
"서버가 필요하면 서버를 따로 만들어야지." 결국 백엔드 로직을 AWS Lambda(Serverless)로 분리하거나, 별도의 Express/NestJS 서버로 옮겨야 했습니다. S3에 정적 배포하려다 일이 더 커졌습니다. "서버리스(Serverless)"와 "정적(Static)"은 엄연히 다릅니다.
middleware.ts도 작동하지 않습니다. 로그인 안 한 사용자를 리다이렉트 시키거나, 쿠키를 검사하는 로직이 서버가 없으니 돌아갈 리가 없죠.
대신 클라이언트 사이드(useEffect)에서 검사해야 하는데, 이러면 화면이 깜빡이는(Flash of Unauthenticated Content) 현상이 생깁니다.
블로그 상세 페이지(/posts/[slug])에 들어갔는데 또 깨졌습니다.
Error: Page "/posts/[slug]" is missing "generateStaticParams"
정적 배포를 하려면 세상에 존재하는 모든 페이지의 주소를 빌드 타임에 알아야 합니다. 서버가 없으니, "사용자가 요청하면 그때 페이지 만들어서 줄게(SSR)"가 불가능하기 때문입니다.
결국 generateStaticParams (구 getStaticPaths)를 사용해서, 1번 글부터 100번 글까지 모든 URL을 미리 정의해 줘야 합니다.
주의: fallback: true 같은 건 못 씁니다. 미리 안 만들어둔 페이지는 영원히 404입니다.
| 특징 | Vercel | Docker (Node.js) | S3 (Static Export) |
|---|---|---|---|
| 비용 | 비쌈 (트래픽당 과금) | 중간 (EC2/ECS 비용) | 매우 저렴 |
| 이미지 최적화 | O (자동) | O (sharp 라이브러리 필요) | X (외부 서비스 필요) |
| API Routes | O (Serverless) | O (서버 내장) | X (불가능) |
| Middleware | O | O | X |
| SSR / ISR | O | O | X (오직 SSG만) |
| 재배포 속도 | 빠름 | 느림 (컨테이너 빌드) | 빠름 |
Next.js는 단순한 React 프레임워크가 아닙니다. 웹 서버 + React + 번들러가 합쳐진 풀스택 프레임워크입니다.
output: 'export'는 여기서 웹 서버라는 심장을 도려내고 껍데기(React)만 남기는 수술입니다.
당연히 팔다리(이미지 최적화, API, SSR, 미들웨어)가 잘려나갑니다.
천 원 아끼려다가 제 주말을 다 날렸습니다. 여러분은 부디 용도에 맞게 선택하세요.
At first, I used Vercel. It was incredibly fast and easy. Just git push and boom, deployed.
But as the project grew and traffic spiked, Vercel's bill (Pro Plan limits, Bandwidth overages) started looking scary.
"Wait, if I just build it as static HTML and put it on AWS S3 with CloudFront, it would cost pennies, right?"
I confidently added one line to next.config.js.
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // "Spit out everything as HTML!"
};
And the moment I hit npm run build, the gates of hell opened.
Running next export doesn't just create HTML files; it strips away the "Server" features that make Next.js great.
Red text filled my build log immediately.
Error: Image Optimization using Next.js default loader is not compatible with "next export".
Next.js's <Image /> component is deceptive. It looks like a UI component, but it requires a Backend Server.
When a user requests an image, the Next.js server fetches the original, resizes it on-demand using sharp, converts it to WebP/AVIF, and serves it.
But S3 is a "Object Storage", not a computing server. It cannot resize images. There is no chef in the warehouse.
You have two choices:
1. The Poor Fix: Turn off optimization Sacrifice quality and loading speed. Just serve the heavy original image.
// next.config.js
module.exports = {
images: { unoptimized: true }
}
This converts <Image /> back to a dumb <img> tag. Deployment works, but your Lighthouse score will tank.
2. The Rich Fix: Use External Loaders Use a dedicated Image CDN like Cloudinary, Imgix, or Akamai.
// next.config.js
module.exports = {
images: {
loader: 'custom',
loaderFile: './my-loader.ts',
},
}
Next.js generates the URL structure, and the external service handles the processing. It costs money but maintains performance.
I managed to build the app (by disabling images) and uploaded it to S3. The main page loaded! "Victory?" I thought. But when I clicked Login, I got a 404 Not Found.
The /api/login route had completely disappeared.
Next.js API Routes (/app/api/...) require a Node.js runtime (or Edge runtime) to execute.
When you set output: 'export', Next.js acts as a static site generator. It keeps HTML, CSS, client-side JS, and deletes all server-side code.
You cannot embed a database connection or secret keys into a static HTML file.
"If I need a server, I must build one." I had to migrate my backend logic to AWS Lambda (Serverless) or a separate Express/NestJS server. The complexity of my infrastructure exploded. I realized that "Serverless" and "Static" are completely different things.
middleware.ts relies on a server intercepting requests before they reach the page.
In a Static Export, there is no interceptor. The request goes directly to the HTML file in S3.
Authentication checks, redirects, and header manipulation in Middleware simply do not exist.
I went to a blog post details page (/posts/[slug]) and it crashed during build.
Error: Page "/posts/[slug]" is missing "generateStaticParams"
To perform a static export, Every Single URL must be known at build time. Since there is no server to render pages on-the-fly (SSR), you must pre-render absolutely everything.
The Fix:
Use generateStaticParams (formerly getStaticPaths) to define every possible slug from your database.
Warning: You cannot use fallback: true or blocking. If a new post is added to the DB, it won't show up until you rebuild and redeploy the site.
| Feature | Vercel | Docker (Self-Hosted) | S3 (Static Export) |
|---|---|---|---|
| Cost | High (Pro plan + limits) | Medium (EC2 costs) | Extremely Low |
| Image Opt | ✅ Automatic | ✅ Requires sharp | ❌ External Service Only |
| API Routes | ✅ Serverless | ✅ Built-in Node server | ❌ Impossible |
| Middleware | ✅ Works | ✅ Works | ❌ Doesn't work |
| SSR / ISR | ✅ Yes | ✅ Yes | ❌ SSG Only |
| Heads-up | Vendor Lock-in? | Maintenance Overhead | Feature Limitations |
Next.js is not just a UI framework. It is a Fullstack Framework (Web Server + React + Bundler).
Using output: 'export' is essentially performing surgery to remove the Web Server heart and leaving only the React skin.
You lose critical limbs: Image Optimization, API Routes, SSR, Middleware.
headers(), cookies()).I wasted my weekend trying to save a few dollars. Don't be like me. Choose the deployment strategy that fits your features, not just your wallet.