03: Next.js는 왜 다른가 — "빌드 결과가 파일이 아니라 애플리케이션이다"
03: Next.js는 왜 다른가 — "빌드 결과가 파일이 아니라 애플리케이션이다"
이 문서에서 배우는 것
- 순수 React 빌드(Vite)와 Next.js 빌드의 근본적 차이
- Next.js가 Node.js 서버를 필요로 하는 이유 (SSR, API Routes, ISR, Middleware, Server Components)
output: 'export'옵션: 정적 빌드는 가능하지만 핵심 기능 포기- Flask + Next.js 조합이 어떻게 동작하는지
PM 요청 (김도연)
김도연 PM: 준혁님, 02편에서 CSR과 SSR의 차이를 배웠는데요. SSR을 하려면 Next.js를 써야 한다고 들었어요. 그런데 Next.js도 React 아닌가요? 왜 일반 React와 다른 취급을 받는 건가요?
이준혁 시니어: 핵심적인 질문이야! 일반 React(Vite로 빌드한)와 Next.js는 빌드 결과물의 본질이 완전히 달라. 하나는 "파일"이고 하나는 "애플리케이션"이야.
시니어 멘토링 (이준혁)
질문 1: React를 빌드하면 뭐가 나온다고 했지?
이준혁: 01편에서 배운 거 기억나? React를 빌드하면 뭐가 나온다고 했어?
김도연: HTML + JS + CSS 정적 파일이요!
이준혁: 맞아! 그럼 Next.js를 빌드하면 뭐가 나오는지 비교해보자.
순수 React (Vite) 빌드 결과
$ cd my-react-app
$ npm run build
$ ls dist/
index.html # 진입점 HTML
assets/
index-CdTgQbWo.js # JS 번들
index-DiwrgTda.css # CSS
$ du -sh dist/
1.2M dist/빌드 결과 = 정적 파일들
→ 어떤 서버든 서빙 가능 (Flask, Nginx, CDN, GitHub Pages)
→ 서버는 "파일을 전달"하기만 하면 됨
→ Node.js 필요 없음!Next.js 빌드 결과
$ cd my-nextjs-app
$ npm run build
$ ls -la
.next/ # ← 이게 빌드 결과물
server/ # 서버 사이드 코드
app/
page.js # 서버에서 실행되는 페이지 코드
api/
route.js # API 라우트 코드
chunks/ # 서버 청크
static/ # 정적 에셋
chunks/
app/
page-abc123.js # 클라이언트 JS
css/
app/layout.css # CSS
cache/ # 빌드 캐시
fetch-cache/
images/
$ du -sh .next/
45M .next/김도연: 45MB?! React는 1.2MB였는데요?
이준혁: 그렇지! 그리고 이 .next/ 폴더를 Nginx에 올려서 서빙할 수 있을까?
김도연: 음... 안 될 것 같은데요?
이준혁: 정답! .next/ 폴더는 그냥 파일이 아니야. 실행해야 하는 애플리케이션이야.
# React: 그냥 파일 → 아무 서버로 서빙
$ nginx -c /etc/nginx/nginx.conf # Nginx로 서빙 가능
$ python -m http.server 8000 # Python으로 서빙 가능
# Next.js: 애플리케이션 → Node.js 서버 필요
$ next start # Node.js 서버로 실행해야 함
$ node server.js # 또는 커스텀 Node.js 서버핵심 구분: 빌드 결과의 본질
┌───────────────────────────────────────────────────────────────┐
│ │
│ 순수 React (Vite) 빌드 결과 = "파일" │
│ ───────────────────────────────────── │
│ • HTML + JS + CSS 정적 파일 │
│ • 어떤 웹 서버에서든 서빙 가능 │
│ • 서버는 "택배 기사" — 파일을 전달만 함 │
│ │
│ Next.js 빌드 결과 = "애플리케이션" │
│ ───────────────────────────────────── │
│ • 서버 코드 + 클라이언트 코드 + 캐시 + 설정 │
│ • Node.js 런타임에서 실행해야 함 │
│ • 서버는 "요리사" — 매 요청마다 HTML을 만듦 │
│ │
└───────────────────────────────────────────────────────────────┘질문 2: 왜 Next.js는 서버가 필요해?
김도연: 왜 Next.js는 Node.js 서버 없이는 안 되는 건가요?
이준혁: Next.js의 핵심 기능 5가지가 전부 서버에서 코드를 실행해야 하기 때문이야. 하나씩 보자.
기능 1: SSR (Server-Side Rendering)
매 요청마다 서버에서 HTML을 새로 생성// app/products/[id]/page.tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
// 서버에서 실행됨 — 매 요청마다!
const product = await db.query('SELECT * FROM products WHERE id = $1', [params.id]);
return (
<div>
<h1>{product.name}</h1>
<p>가격: {product.price}원</p>
<p>재고: {product.stock}개</p> {/* 실시간 재고 */}
</div>
);
}왜 서버가 필요한가:
db.query()는 서버에서만 실행 가능 (브라우저에서 DB 접근 불가)- 매 요청마다 최신 재고를 반영한 HTML을 생성해야 함
- 정적 파일로는 "매 요청마다 다른 HTML"을 만들 수 없음
기능 2: API Routes
서버에서 API 엔드포인트 실행// app/api/users/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
// 서버에서 실행됨
const users = await db.query('SELECT * FROM users');
return NextResponse.json(users);
}
export async function POST(request: Request) {
// 서버에서 실행됨
const body = await request.json();
await db.query('INSERT INTO users (name) VALUES ($1)', [body.name]);
return NextResponse.json({ success: true });
}왜 서버가 필요한가:
- API는 서버에서 실행되는 코드
- DB 접근, 인증, 외부 API 호출 등 서버 작업
- 정적 파일에서는 API를 만들 수 없음
기능 3: ISR (Incremental Static Regeneration)
정적 페이지를 주기적으로 서버에서 재생성// app/blog/[slug]/page.tsx
export const revalidate = 60; // 60초마다 재생성
export default async function BlogPost({ params }: { params: { slug: string } }) {
// 빌드 시 + 60초마다 서버에서 실행
const post = await fetch(`https://cms.example.com/posts/${params.slug}`);
const data = await post.json();
return (
<article>
<h1>{data.title}</h1>
<div>{data.content}</div>
</article>
);
}왜 서버가 필요한가:
- "60초마다 재생성"하려면 서버가 주기적으로 코드를 실행해야 함
- 타이머를 돌리고, CMS에서 최신 데이터를 가져오고, HTML을 새로 만듦
- 정적 파일은 한번 만들면 끝 — 스스로 업데이트할 수 없음
기능 4: Middleware
요청이 페이지에 도달하기 전에 서버에서 가로채기// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 모든 요청에 대해 서버에서 실행됨
const token = request.cookies.get('auth-token');
// 로그인 안 했으면 로그인 페이지로 리다이렉트
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// A/B 테스트
const bucket = Math.random() > 0.5 ? 'a' : 'b';
const response = NextResponse.next();
response.cookies.set('ab-test', bucket);
return response;
}왜 서버가 필요한가:
- 요청을 "가로채서" 처리하는 건 서버에서만 가능
- 쿠키 확인, 리다이렉트, A/B 테스트, 지역화 등
- 정적 파일은 요청을 가로챌 수 없음
기능 5: React Server Components
컴포넌트 자체가 서버에서만 실행// app/dashboard/page.tsx — Server Component (기본값)
// 이 컴포넌트는 서버에서만 실행됨. 브라우저에 JS가 전송되지 않음!
import { headers } from 'next/headers';
export default async function Dashboard() {
const headersList = await headers();
const userId = headersList.get('x-user-id');
// 서버에서 직접 DB 조회 — 브라우저에 노출 안 됨!
const data = await db.query('SELECT * FROM dashboard WHERE user_id = $1', [userId]);
// 비밀 API 키도 안전하게 사용 가능
const analytics = await fetch('https://api.analytics.com/data', {
headers: { 'Authorization': `Bearer ${process.env.SECRET_API_KEY}` }
});
return (
<div>
<h1>대시보드</h1>
<p>방문자: {data.visitors}</p>
<p>매출: {data.revenue}원</p>
</div>
);
}
// → 이 컴포넌트의 JS는 브라우저에 전송되지 않음 (번들 크기 0!)왜 서버가 필요한가:
- Server Component는 서버에서만 실행되는 React 컴포넌트
- 결과 HTML만 클라이언트에 전송 (JS 코드는 전송 안 됨)
- DB 접근, 비밀 키 사용 등이 안전하게 가능
- 당연히 서버 없이는 실행 불가
질문 3: 그러면 Next.js를 정적 파일로 빌드할 수는 없어?
김도연: Next.js를 꼭 서버로 돌려야 하나요? 정적 파일로 만들 수는 없나요?
이준혁: 있긴 해! output: 'export'라는 옵션이 있어.
// next.config.js
const nextConfig = {
output: 'export', // 정적 빌드 모드
};
export default nextConfig;$ npm run build
# 이번에는 out/ 폴더에 정적 파일이 생성됨
$ ls out/
index.html
about.html
products/
1.html
2.html
_next/
static/
chunks/...
css/...이준혁: 이렇게 하면 .next/ 대신 out/ 폴더에 정적 HTML 파일이 생성돼. 이건 Nginx, Flask, CDN 어디서든 서빙할 수 있어.
김도연: 그럼 이걸 쓰면 되는 거 아닌가요?
이준혁: 문제는 핵심 기능을 포기해야 한다는 거야.
┌─────────────────────────────────────────────────────────────┐
│ output: 'export' 사용 시 포기하는 것들 │
│ │
│ ❌ SSR (Server-Side Rendering) │
│ → 매 요청마다 HTML 생성 불가 │
│ │
│ ❌ API Routes │
│ → 서버 API 엔드포인트 사용 불가 │
│ │
│ ❌ ISR (Incremental Static Regeneration) │
│ → 주기적 재생성 불가 │
│ │
│ ❌ Middleware │
│ → 요청 가로채기 불가 │
│ │
│ ❌ Server Components (동적) │
│ → 서버 전용 컴포넌트 사용 불가 │
│ │
│ ❌ Dynamic Routes (동적 params) │
│ → generateStaticParams 필수 │
│ │
│ ✅ 사용 가능한 것 │
│ → 정적 페이지, 클라이언트 컴포넌트, 정적 생성(SSG) │
│ │
└─────────────────────────────────────────────────────────────┘이준혁: output: 'export'를 쓰면 Next.js의 핵심 기능을 거의 다 포기하는 거야. 그러면 차라리 Vite + React를 쓰는 게 더 간단하고 가벼워.
Next.js + output: 'export' → 핵심 기능 포기, 빌드 느림, 복잡
Vite + React → 같은 결과, 빌드 빠름, 단순
→ 정적 빌드만 할 거면 Vite를 쓰는 게 낫다질문 4: 그럼 Flask랑 Next.js를 같이 쓸 수 있어?
김도연: 저희 Flask 백엔드를 유지하면서 Next.js를 쓰고 싶은데, 가능한가요?
이준혁: 가능해! 하지만 01편처럼 "Flask가 파일을 서빙하는" 방식이 아니라, 두 개의 서버를 별도로 실행하고 리버스 프록시로 연결해야 해.
┌────────────────────────────────────────────────────────────────┐
│ 리버스 프록시 (Nginx) │
│ │
│ 브라우저 ──→ Nginx (포트 80) │
│ │ │
│ ├── /api/* ──→ Flask (포트 5000) │
│ │ Python 백엔드 │
│ │ DB 접근, 비즈니스 로직 │
│ │ │
│ └── /* ──→ Next.js (포트 3000) │
│ Node.js 서버 │
│ SSR, 프론트엔드 │
└────────────────────────────────────────────────────────────────┘# nginx.conf
server {
listen 80;
# API 요청 → Flask
location /api/ {
proxy_pass http://localhost:5000;
}
# 나머지 → Next.js
location / {
proxy_pass http://localhost:3000;
}
}이준혁: 이렇게 하면:
- Flask: Python으로 API와 비즈니스 로직 처리
- Next.js: Node.js로 SSR과 프론트엔드 처리
- Nginx: 경로에 따라 요청을 적절한 서버로 분배
김도연: 서버가 3개나 필요한 거예요? (Nginx + Flask + Next.js)
이준혁: 맞아. 이게 Flask + Next.js 조합의 단점이야. 비교해보면:
| 구성 | 서버 수 | 복잡도 | 장점 |
|---|---|---|---|
| Flask + React (정적) | 1개 | 낮음 | 간단, CSR |
| Next.js 단독 | 1개 | 중간 | SSR + API + 프론트 올인원 |
| Flask + Next.js | 3개 | 높음 | 기존 Flask 유지 + SSR |
이준혁: 실무에서의 판단 기준:
Flask 백엔드를 이미 쓰고 있다면:
├── SSR 필요 없음 → Flask + React/Svelte 정적 빌드 (01편 방식)
├── SSR 필요 + 새 프로젝트 → Next.js 단독 (Flask 제거)
└── SSR 필요 + Flask 유지 필수 → Flask + Next.js + Nginx (복잡)비유로 정리
이준혁: 최종 비유로 정리할게.
React (Vite) 빌드 = 인쇄물
출판사(빌드) → 책(정적 파일) → 서점(웹 서버) → 독자(브라우저)
• 책은 한번 인쇄하면 내용이 바뀌지 않음
• 어떤 서점에서든 팔 수 있음 (Flask, Nginx, CDN)
• 서점은 책 내용을 모름 — 그냥 전달만 함Next.js 빌드 = 요리사 + 레시피
요리학교(빌드) → 요리사+레시피(.next/) → 레스토랑(Node.js) → 손님(브라우저)
• 손님이 주문할 때마다 요리를 새로 만듦 (SSR)
• 요리사가 없으면 레시피만으로는 요리 불가 (Node.js 필수)
• 레스토랑이 필요함 — 서점에서는 안 됨 (Nginx만으로 안 됨)┌─────────────────────────────────────────────────────────────┐
│ │
│ "React 빌드 결과는 파일이고, │
│ Next.js 빌드 결과는 애플리케이션이다." │
│ │
│ 파일은 어디서든 서빙 가능하고, │
│ 애플리케이션은 실행 환경이 필요하다. │
│ │
└─────────────────────────────────────────────────────────────┘핵심 정리
순수 React (Vite) vs Next.js 빌드 비교
| 항목 | React (Vite) | Next.js |
|---|---|---|
| 빌드 결과 | dist/ (정적 파일) | .next/ (애플리케이션) |
| 크기 | ~1MB | ~45MB |
| 서빙 방법 | 아무 웹 서버 | next start (Node.js) |
| SSR | 불가 | 자동 지원 |
| API | 불가 (별도 서버 필요) | 내장 API Routes |
| ISR | 불가 | 자동 지원 |
| Middleware | 불가 | 자동 지원 |
| Server Components | 불가 | 기본 지원 |
| 정적 빌드 가능 | 기본 동작 | output: 'export' (기능 제한) |
Next.js가 서버를 필요로 하는 5가지 이유
- SSR: 매 요청마다 HTML 생성
- API Routes: 서버에서 API 엔드포인트 실행
- ISR: 주기적으로 정적 페이지 재생성
- Middleware: 요청 가로채기 (인증, A/B 테스트 등)
- Server Components: 서버에서만 실행되는 컴포넌트
핵심 한 줄
React를 빌드하면 "파일"이 나오고, Next.js를 빌드하면 "애플리케이션"이 나온다. 파일은 아무 서버로 서빙하고, 애플리케이션은 Node.js로 실행한다.
다음 단계
04-language-runtime-decides-ssr.md에서 "왜 Flask에서는 SSR을 못 하는 건가?"에 대해 알아봅니다. SSR 가능 여부를 결정하는 것이 프레임워크가 아니라 언어 런타임이라는 사실을 다룹니다.
작성: 2026-02-09 버전: 1.0 예상 독서 시간: 15분