1 / 2

03: Next.js는 왜 다른가 — "빌드 결과가 파일이 아니라 애플리케이션이다"

예상 시간: 5분

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.js3개높음기존 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가지 이유

  1. SSR: 매 요청마다 HTML 생성
  2. API Routes: 서버에서 API 엔드포인트 실행
  3. ISR: 주기적으로 정적 페이지 재생성
  4. Middleware: 요청 가로채기 (인증, A/B 테스트 등)
  5. Server Components: 서버에서만 실행되는 컴포넌트

핵심 한 줄

React를 빌드하면 "파일"이 나오고, Next.js를 빌드하면 "애플리케이션"이 나온다. 파일은 아무 서버로 서빙하고, 애플리케이션은 Node.js로 실행한다.


다음 단계

04-language-runtime-decides-ssr.md에서 "왜 Flask에서는 SSR을 못 하는 건가?"에 대해 알아봅니다. SSR 가능 여부를 결정하는 것이 프레임워크가 아니라 언어 런타임이라는 사실을 다룹니다.


작성: 2026-02-09 버전: 1.0 예상 독서 시간: 15분