Nitro 완벽 가이드 | 유니버설 서버 엔진·라우팅·캐시·Edge·실전 API
이 글의 핵심
Nitro는 한 번 작성한 서버 코드를 Node·Bun·Lambda·Cloudflare Workers 등 여러 런타임에 배포할 수 있게 해 주는 유니버설 서버 엔진입니다. 파일 기반 라우팅, 라우트 규칙 기반 캐싱, Unstorage 기반 스토리지, WebSocket까지 한 흐름으로 익힐 수 있습니다.
이 글의 핵심
Nitro는 Nuxt 3와 Analog가 공통으로 사용하는 유니버설 서버 엔진입니다. HTTP 핸들러·미들웨어·빌드 출력·배포 프리셋을 하나의 추상화로 묶어, 동일한 소스에서 Node 서버, 서버리스 함수, Edge Worker로 재컴파일·재배포할 수 있게 설계되었습니다.
실무 관점: “프론트는 Nuxt, API는 별도 Express”로 나누면 배포 파이프라인과 인증·캐시 정책이 이중으로 갈립니다. Nitro 레이어에 API를 두면 한 애플리케이션 경계 안에서 라우트 규칙·스토리지·런타임 어댑터를 공유할 수 있어 운영 복잡도가 줄어듭니다.
들어가며: 왜 Nitro인가
풀스택 프레임워크는 서버 런타임이 플랫폼마다 다릅니다. AWS Lambda는 이벤트 객체가 있고, Cloudflare Workers는 Fetch API 중심이며, 전통 Node 서버는 장시간 TCP 연결과 파일 시스템에 의존합니다. Nitro는 이런 차이를 프리셋(preset) 과 런타임 어댑터 뒤로 숨기고, 개발자에게는 파일 기반 라우트와 defineEventHandler 같은 일관된 API를 제공합니다.
실무 문제 시나리오
시나리오 1: 스테이징은 Node인데 프로덕션은 Edge예요
동일 Nitro 빌드에 preset만 바꿔 재배포할 수 있습니다. 다만 Edge는 Node 전용 모듈·일부 네이티브 의존성이 제한되므로, 대상 프리셋을 초기에 고정하고 제약을 CI에서 검증하는 편이 안전합니다.
시나리오 2: 페이지는 ISR이 필요하고 API는 캐시하면 안 돼요
Nuxt의 routeRules·Nitro 라우트 규칙으로 경로별로 SWR·정적 프리렌더·캐시 헤더를 분리합니다. API 경로는 Cache-Control: private 또는 no-store로 두고, 공개 문서 경로만 s-maxage를 부여하는 식의 하이브리드 전략이 일반적입니다.
시나리오 3: 세션·레이트 리밋 저장소를 어디에 둘까요
Nitro Unstorage 추상화로 memory·redis·클라우드 KV 등을 드라이버로 교체할 수 있습니다. 로컬 개발은 파일·메모리, 프로덕션은 관리형 KV로 스위치하는 패턴이 흔합니다.
1. Nitro의 핵심 개념
1.1 유니버설 런타임과 프리셋
Nitro는 소스 트리를 단일 번들로 빌드한 뒤, nitro.config의 preset(또는 Nuxt의 nitro.preset)에 따라 출력물 형태가 달라집니다. 예를 들어 node-server는 node로 구동 가능한 서버 엔트리를, vercel은 Vercel Functions 레이아웃에 맞는 산출물을 생성합니다.
핵심은 “한 코드베이스 — 여러 배포 타겟” 입니다. 다만 “동일 동작”을 보장하려면 플랫폼 제한(실행 시간, 파일 시스템, TCP/WebSocket, 환경 변수 주입 방식)을 각 프리셋 문서와 함께 읽어야 합니다.
1.2 H3 이벤트 모델
Nitro의 HTTP 레이어는 h3를 기반으로 합니다. 요청 단위로 event 객체가 전달되며, 헤더·쿠키·본문 파싱, 리다이렉트, 스트리밍 응답이 이 모델 위에서 이루어집니다. Express의 req/res와 개념은 비슷하지만, 웹 표준 Request/Response에 가까운 추상화로 통일되어 Edge 런타임과도 맞춥니다.
1.3 UnJS 생태계
Nitro는 UnJS 계열 모듈(h3, unstorage, ofetch 등)과 잘 맞물립니다. 특히 unstorage는 “스토리지 추상화”로서 로컬·메모리·Redis·S3·클라우드 KV를 동일 API로 다루게 해 주어, 서버리스·Edge 전환 시 저장소 교체 비용을 줄입니다.
1.4 Nuxt·Analog와의 관계
- Nuxt 3:
server/디렉터리와nuxt.config의nitro·routeRules가 Nitro 빌드에 직접 연결됩니다. - Analog(Angular 풀스택): Nitro를 서버 엔진으로 사용하며, Angular 측 빌드와 Nitro 라우트가 결합됩니다.
즉 Nitro를 이해하면 Vue(Nuxt) 와 Angular(Analog) 양쪽의 서버 측 확장 지점을 같은 모델로 읽을 수 있습니다.
2. 파일 기반 라우팅
2.1 Nuxt에서의 관례
Nuxt 프로젝트에서 가장 흔한 구조는 다음과 같습니다.
server/api/**/*.ts: 기본적으로/api/**URL로 매핑됩니다.server/routes/**/*.ts: 루트 경로에 직접 매핑됩니다(프로젝트 설정에 따라 접두사를 붙일 수 있음).
파일 이름이 곧 URL 패턴이 됩니다. 동적 세그먼트는 [id].ts, 캐치올은 [...slug].ts 형태를 사용합니다.
// server/api/users/[id].ts
// GET /api/users/:id
import { defineEventHandler, getRouterParam } from 'h3';
export default defineEventHandler((event) => {
const id = getRouterParam(event, 'id');
return { id, source: 'nitro' };
});
위 핸들러는 한 파일이 한 라우트를 담당합니다. RESTful API를 만들 때는 디렉터리로 리소스를 나누고, 공통 검증은 서버 미들웨어로 뺍니다.
2.2 미들웨어 체인
server/middleware/에 두는 미들웨어는 요청이 핸들러에 도달하기 전에 실행됩니다. 인증 헤더 검사, 요청 ID 부여, 로깅 등 횡단 관심사에 적합합니다.
// server/middleware/01-request-id.ts
import { defineEventHandler, getRequestHeader, setResponseHeader } from 'h3';
import { randomUUID } from 'node:crypto';
export default defineEventHandler((event) => {
const incoming = getRequestHeader(event, 'x-request-id');
const id = incoming ?? randomUUID();
event.context.requestId = id;
setResponseHeader(event, 'x-request-id', id);
});
event.context는 이후 핸들러까지 전달되는 요청 범위 저장소로 쓰입니다. 다만 민감 정보를 넣을 때는 로그에 남지 않도록 주의합니다.
2.3 순수 Nitro 프로젝트
Nuxt 없이 Nitro만 쓸 때는 보통 routes/ 디렉터리에 핸들러를 두고 nitro.config.ts로 프리셋·별칭·런타임 설정을 관리합니다. 라우팅 규칙·플러그인·스토리지 바인딩은 모두 Nitro 설정 파일 한곳에서 조정할 수 있습니다.
3. 캐싱 전략과 하이브리드 렌더링
3.1 routeRules로 경로별 정책 분리
Nuxt에서는 routeRules로 정적 프리렌더·SSR·SWR(ISR 스타일)·헤더를 경로 단위로 지정합니다. 이는 Nitro가 빌드·런타임에 반영할 라우트 메타데이터로 연결됩니다.
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/docs/**': { swr: 3600 },
'/dashboard/**': { ssr: false },
'/api/**': {
cors: true,
headers: { 'cache-control': 'no-store' },
},
},
});
- prerender: 빌드 시 HTML을 생성합니다. 마케팅 랜딩·문서처럼 변하지 않는 구간에 적합합니다.
- swr: stale-while-revalidate 스타일로 캐시된 응답을 제공하고 백그라운드에서 재검증합니다. 트래픽이 크고 갱신 주기가 허용되는 페이지에 유리합니다.
- ssr: false: 해당 트리는 클라이언트 렌더링 위주로 두어 서버 부하를 줄이거나 인증된 앱 영역과 공개 영역을 나눌 때 쓰입니다.
- API: 기본적으로 캐시하지 않는 것이 안전합니다. 공개 GET API에 한해
s-maxage를 주려면 의도적으로 헤더를 설계합니다.
3.2 캐시 키와 무효화의 현실
HTTP 캐시는 키가 URL·헤더·Vary에 의해 결정됩니다. 개인화 쿠키가 있는 요청이 공용 캐시에 닿지 않게 Vary·private를 조정해야 합니다. 무효화는 CDN·호스팅 제공 기능(태그 퍼지, API)에 의존하는 경우가 많으므로, “캐시에 싣지 말아야 할 데이터” 를 먼저 정의하는 편이 안전합니다.
3.3 Nitro 내부 캐시와 스토리지
라우트 외에도 Nitro 스토리지에 짧은 TTL 객체를 두어 부가 캐시(예: 외부 API 응답, 계산 비용 큰 파생 데이터)를 저장할 수 있습니다. 이는 HTTP 캐시와는 별층이며, 일관성 요구·TTL·스탬피드 방지를 코드로 명시해야 합니다.
4. 배포 타겟: Vercel, Netlify, AWS
4.1 공통: 프리셋 선택
배포 문서에 맞는 preset을 지정합니다. 예시는 개념 설명용이며, 실제 버전별 이름은 Nitro·호스팅 문서를 확인합니다.
// nitro.config.ts (개념 예시)
import { defineNitroConfig } from 'nitro/config';
export default defineNitroConfig({
preset: 'node-server',
});
Nuxt에서는 export default defineNuxtConfig({ nitro: { preset: '...' } }) 형태로 동일하게 넘깁니다.
4.2 Vercel·Netlify
이러한 플랫폼은 서버리스 함수 또는 에지 함수로 배포됩니다. 장점은 글로벌 분산과 트래픽 기반 과금 구조이고, 제약은 실행 시간·페이로드 크기·파일 시스템입니다. 장시간 연결(WebSocket)이나 대용량 업로드는 별도 서비스(전용 Node, 객체 스토리지 직접 업로드)와 조합하는 경우가 많습니다.
4.3 AWS Lambda
API Gateway + Lambda 조합은 검증된 패턴입니다. Cold start·동시 실행 한도·VPC 연결 여부가 비용과 지연에 큰 영향을 줍니다. Nitro 출력이 Lambda 핸들러 형태로 제공되더라도, 연결 풀·DB 접근은 서버리스 친화적으로 재설계해야 합니다.
4.4 운영 체크리스트
- 환경 변수: 빌드 타임 vs 런타임 주입 방식이 플랫폼마다 다릅니다.
- 로그·트레이싱: 요청 ID를 미들웨어에서 통일해 분산 로그를 상관시킵니다.
- 리전: 데이터 규제가 있으면 스토리지·함수·CDN 리전을 함께 설계합니다.
5. Storage Layer와 KV
5.1 Unstorage 추상화
Nitro 생태계에서는 unstorage를 통해 키-값 저장소를 통일합니다. 로컬은 디스크나 메모리, 프로덕션은 클라우드 KV로 전환하는 시나리오가 대표적입니다.
// server/api/counter/[key].get.ts
import { defineEventHandler, getRouterParam } from 'h3';
export default defineEventHandler(async (event) => {
const key = getRouterParam(event, 'key') ?? 'default';
const storage = useStorage('data');
const raw = (await storage.getItem(`counter:${key}`)) ?? '0';
const n = Number(raw) || 0;
await storage.setItem(`counter:${key}`, String(n + 1));
return { key, count: n + 1 };
});
useStorage에 넘기는 네임스페이스(data)는 nitro 설정에서 드라이버와 매핑됩니다. 로컬에서는 .data/ 디렉터리, 프로덕션에서는 원격 KV가 되도록 분기합니다.
5.2 Cloudflare KV와 바인딩
Cloudflare에 배포할 때는 Wrangler 바인딩 이름과 Nitro 스토리지 설정을 맞춥니다. KV는 최종 일관성 모델이므로, 금융·재고처럼 강한 일관성이 필요한 경우에는 상위에서 비관적 락·DB 트랜잭션을 고려해야 합니다.
5.3 설계 시 유의점
- 드라이버 호환: Edge에서 Node 전용 경로에 쓰기 불가할 수 있습니다.
- 키 네이밍: 테넌트·환경 접두사를 붙여 충돌을 방지합니다.
- TTL: 세션·캐시에는 만료를 명시하고, 장기 보관 데이터는 KV 대신 관계형/문서 DB를 검토합니다.
6. WebSocket 지원
Nitro는 CrossWS와 h3를 통해 여러 런타임에서 WebSocket을 지원합니다. 설정에서 기능을 켜고, 라우트 파일에서 defineWebSocketHandler를 export합니다.
6.1 설정
// nitro.config.ts — WebSocket 활성화 (개념 예시)
import { defineNitroConfig } from 'nitro/config';
export default defineNitroConfig({
features: {
websocket: true,
},
});
Nuxt에서는 nitro: { features: { websocket: true } }로 동일 옵션을 전달합니다.
6.2 핸들러와 인증
// routes/chat.ts (순수 Nitro 예시)
import { defineWebSocketHandler } from 'nitro';
export default defineWebSocketHandler({
upgrade(request) {
const url = new URL(request.url);
const token = url.searchParams.get('token');
if (!token) {
throw new Response('Unauthorized', { status: 401 });
}
return { context: { token } };
},
message(peer, message) {
peer.send({ echo: message.text() });
},
});
upgrade에서 Response를 throw하면 핸드셰이크가 거절됩니다. 연결 이후에는 peer.send, peer.publish로 브로드캐스트할 수 있습니다. Pub/Sub 토픽은 동일 네임스페이스 내에서만 전파됩니다.
6.3 SSE 대안
실시간이 필요하지만 양방향이 아니라면 Server-Sent Events가 더 단순할 수 있습니다. HTTP 기반이라 일부 프록시·CDN과 궁합이 좋고, 자동 재연결 패턴도 표준화되어 있습니다.
7. 실전 API 서버 구축
7.1 모듈 구조
소규모라면 server/api 아래에 파일을 두면 충분합니다. 규모가 커지면 도메인 폴더로 서비스·검증·저장소를 나눕니다.
server/
api/
v1/
users/
index.get.ts
index.post.ts
[id].get.ts
middleware/
auth.ts
utils/
db.ts
REST 관례에 맞춰 index.get.ts·index.post.ts처럼 HTTP 메서드별 파일을 쓰면 라우팅 의도가 분명해집니다.
7.2 입력 검증과 오류 응답
import { defineEventHandler, readBody, createError } from 'h3';
import { z } from 'zod';
const BodySchema = z.object({
email: z.string().email(),
name: z.string().min(1),
});
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const parsed = BodySchema.safeParse(body);
if (!parsed.success) {
throw createError({
statusCode: 400,
statusMessage: 'Validation Error',
data: parsed.error.flatten(),
});
}
return { ok: true, user: parsed.data };
});
zod 같은 스키마와 createError를 함께 쓰면 클라이언트에 일관된 오류 포맷을 줄 수 있습니다.
7.3 보안 기본기
- CORS: 공개 API가 아니라면 출처를 제한합니다.
- Rate limiting: IP·토큰·세션 키 기준으로 스토리지에 카운터를 두고 차단합니다.
- 헤더:
Content-Security-Policy,X-Content-Type-Options등은 프론트·API 모두에서 점검합니다.
7.4 관측 가능성
구조화된 로그(JSON 한 줄)와 요청 ID, 외부 호출의 지연 분포를 남기면 장애 분석이 빨라집니다. Nitro·Nuxt 위에서 OpenTelemetry를 붙이는 사례도 늘고 있습니다.
8. 정리
Nitro는 “서버 코드의 이식성” 을 현실로 만드는 레이어입니다. 파일 기반 라우팅으로 API를 정의하고, routeRules로 하이브리드 렌더링·캐시를 설계하며, Unstorage로 환경별 스토리지를 교체하고, WebSocket·SSE로 실시간 요구를 보완할 수 있습니다.
프리셋마다 런타임 제약이 다르므로, 초기에 배포 타겟을 고정하고 제약을 CI에서 검증하며, 캐시·저장소·인증을 경로별로 명시적으로 나누는 것이 프로덕션에서 가장 큰 비용 절감으로 이어집니다.
참고로 읽으면 좋은 글
동일 블로그의 Nuxt 3 완벽 가이드, WebSocket 완벽 가이드, Cloudflare Workers 완벽 가이드를 이어 읽으면 클라이언트·엣지·실시간 계층을 한데 묶는 그림이 선명해집니다.