매일 쓰면서 설명 못 하는 것
Nuxt를 도입하기로 결정한 후 막힌 건 코드가 아니었다.
“Vite는 들어봤다. 근데 Nitro는 뭐지? Node랑 또 어떤 관계지?”
문서를 봐도 셋이 같이 등장하는데 어떤 layer에 어떤 도구가 있는지는 명확히 안 나온다. PHP/Laravel만 10년 한 사람에게는 모델이 안 잡힌다.
이 글은 그 3 layer를 한 번에 정리한다.
잘못 알기 쉬운 모델
PHP 시니어가 흔히 갖는 모델:
Vite, Nitro, Node — 다 같은 빌드 도구 아닌가?
이대로면 셋 중 하나만 알면 된다는 결론이 나온다. 실제로는 셋이 완전히 다른 layer에 있다.
3 layer 정리
┌──────────────────────────────────┐
│ Vite │ → 브라우저용 프론트 번들러
│ (Vue SFC, TS, CSS, HMR) │ + 개발 서버
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Nitro │ → SSR 서버 번들러
│ (server routes, middleware, │ + 배포 target preset
│ prerender, route rules) │
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Node.js │ → 런타임
│ (Nitro 결과물 실행) │ (Vercel/CF/Bun/Deno도 가능)
└──────────────────────────────────┘
비유하면:
| Layer | Nuxt | 비유 |
|---|---|---|
| 빌드 도구 (frontend) | Vite | webpack 대체 |
| 빌드 시스템 (server) | Nitro | 서버 빌드 + 배포 target 결정 |
| 런타임 | Node.js | 실행 엔진 |
핵심: Vite는 Nitro의 대체가 아니다. Node도 Nitro의 대체가 아니다. 셋은 같이 작동한다.
빌드 결과물을 보면 모델이 잡힌다
pnpm build (또는 bun run build) 한 번 돌리면 .output/이 나온다.
.output/
├── server/
│ └── index.mjs ← Nitro가 만든 서버 번들
└── public/
├── _nuxt/ ← Vite가 만든 브라우저 자산
│ └── *.js, *.css
└── ...static files
실행은 Node가 한다.
node .output/server/index.mjs
즉:
public/= Vite의 작품. 브라우저에 보낼 JS/CSS/이미지.server/index.mjs= Nitro의 작품. SSR 요청 처리하는 서버 코드.- Node =
server/index.mjs를 실행하는 런타임.
이 디렉터리 구조 하나가 3 layer 분리를 그대로 보여준다.
Vite의 이중 역할 — 헷갈림 정정
위에서 Vite는 브라우저용이라고 했는데, 사실 개발 모드에서는 SSR 변환에도 관여한다.
| 모드 | Vite의 역할 |
|---|---|
dev | 브라우저 번들 + SSR 코드 변환 + HMR |
build | 브라우저 번들만 (SSR 번들은 Nitro 담당) |
운영 결과물 책임자는 Nitro라는 점은 변하지 않는다. 하지만 개발 중에는 Vite가 SSR 변환 파이프라인에도 손을 댄다는 점은 짚어둘 만하다 — 디버깅 시 어디 문제인지 추적할 때 도움 된다.
Nitro preset — 한 코드, 여러 환경
Nitro의 진짜 강력한 부분은 배포 환경 추상화다. 같은 Nuxt 코드를 여러 런타임에 맞춰 빌드할 수 있다.
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'node-server', // 기본
// preset: 'vercel',
// preset: 'cloudflare-pages',
// preset: 'bun',
// preset: 'deno-server',
}
})
각 preset마다 .output/의 형태가 살짝 달라진다. 예를 들어:
node-server→.output/server/index.mjs(Node로 실행)cloudflare-pages→.output/_worker.js(Cloudflare Workers 형식)vercel→.output/이 Vercel Serverless Functions 형식
같은 코드를 환경에 맞춰 output 형식만 바꿔 배포할 수 있다는 게 Nitro의 핵심 가치다.
webpack은 안 되나?
Nuxt builder는 기본이 Vite지만 webpack도 지원한다.
export default defineNuxtConfig({
builder: '@nuxt/webpack-builder',
})
다만 신규 프로젝트라면 Vite 그대로 가는 게 표준이다. webpack을 고려할 만한 상황은 제한적:
- 기존 webpack loader/plugin 자산이 많음
- 특정 라이브러리가 Vite에서 문제를 냄
- Nuxt 2/webpack 기반 코드를 Nuxt 3로 점진 이전 중
- 회사 빌드 파이프라인이 webpack 플러그인에 강하게 묶여 있음
PHP 레거시를 Nuxt로 이전하는 신규 프로젝트라면 webpack을 고를 이유가 거의 없다.
Nitro vs Node — 이게 가장 헷갈리는 부분
“그래서 Nitro는 Node 대체야? Node 위에서 도는 거야?”
답: Nitro는 빌드 시스템이고, Node는 런타임이다.
Nuxt 코드
↓ (Nitro가 빌드)
.output/server/index.mjs
↓ (Node가 실행)
HTTP 서버
Nitro는 코드를 만든다. Node는 코드를 실행한다. 같은 layer가 아니다.
만약 preset을 cloudflare-pages로 바꾸면? 그땐 Node가 아니라 Cloudflare Workers 런타임이 실행한다. Nitro는 그 환경에 맞는 output을 만들어주는 번들러 + 빌드 시스템이지 런타임이 아니다.
정리 — 한 줄 모델
Vite (Vue 앱 빌드/개발)
+
Nitro (Nuxt 서버 빌드 + 배포 환경 추상화)
+
Node (Nitro 결과물 실행 — 또는 Vercel/CF/Bun/Deno)
또는 흐름으로:
Nuxt → Nitro가 서버 번들 생성 → Node.js가 실행
이 한 줄이 머릿속에 박히면 .output/이 왜 그렇게 생겼는지, nitro.preset이 왜 환경별로 다른지, Vite와 Nitro가 왜 대체 관계가 아닌지 다 보인다.
결론
PHP/Laravel에서 넘어온 시니어가 Nuxt 도입할 때 가장 시간 잡아먹는 건 코드가 아니라 빌드 도구의 layer 분리다. Vite, Nitro, Node가 셋 다 다른 일을 한다는 게 박히면 그 다음은 빠르다.
Nuxt 도입 결정 후 가장 먼저 해야 할 일은 pnpm build 한 번 돌리고 .output/ 디렉터리 안을 한 번 열어보는 것이다. 3 layer가 거기 그대로 보인다.