15.5 배포 전략 — SSR/SSG/SWA 모드, Vercel/Cloudflare 배포
Nuxt 3의 렌더링 모드
Nuxt 3는 하나의 코드베이스로 여러 렌더링 전략을 지원합니다. 프로젝트의 요구사항에 따라 최적의 전략을 선택하거나 혼합해서 사용할 수 있습니다.
| 모드 | 설명 | 적합한 상황 |
|---|---|---|
| SSR (Server-Side Rendering) | 요청마다 서버에서 HTML 생성 | 실시간 데이터, 개인화 콘텐츠 |
| SSG (Static Site Generation) | 빌드 타임에 모든 HTML 미리 생성 | 블로그, 문서 사이트, 마케팅 페이지 |
| SWA (Static Web App / SPA) | 클라이언트에서 모든 렌더링 | 관리자 대시보드, 인증 필요 앱 |
| 하이브리드 | 경로별로 다른 전략 적용 | 대부분의 프로덕션 앱 |
SSR 모드 (기본값)
SSR은 Nuxt 3의 기본 렌더링 모드입니다. 사용자 요청이 들어올 때마다 서버에서 HTML을 생성합니다.
특징
- SEO 우수: 완성된 HTML이 검색 엔진에 전달됨
- 첫 페이지 로드 빠름: 브라우저가 완성된 HTML을 즉시 렌더링
- 항상 최신 데이터: 요청 시점의 데이터가 반영됨
- 서버 필요: Node.js 서버 또는 서버리스 환경 필요
설정
// nuxt.config.ts — SSR이 기본값이므로 명시적 설정은 선택사항
export default defineNuxtConfig({
ssr: true, // 기본값
})
SSR 동작 확인
<!-- pages/ssr-demo.vue -->
<template>
<div>
<h1>SSR 데모</h1>
<p>렌더링 시간: {{ renderTime }}</p>
<p>사용자 에이전트: {{ userAgent }}</p>
<ul>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
</div>
</template>
<script setup lang="ts">
// 서버에서 실행 — 매 요청마다 최신 데이터 페칭
const { data: posts } = await useFetch('/api/posts')
// 서버 컨텍스트에 접근
const event = useRequestEvent()
const userAgent = event?.node.req.headers['user-agent'] ?? 'client'
const renderTime = new Date().toISOString()
</script>
SSG 모드 (정적 사이트 생성)
SSG는 빌드 시점에 모든 페이지를 HTML 파일로 미리 생성합니다. CDN을 통해 서빙하면 매우 빠른 응답 속도를 얻을 수 있습니다.
설정
// nuxt.config.ts
export default defineNuxtConfig({
// 전체 앱을 정적으로 생성
nitro: {
prerender: {
crawlLinks: true, // 링크를 따라가며 모든 페이지 자동 탐지
routes: ['/', '/about', '/contact'], // 명시적으로 추가할 라우트
},
},
})
동적 라우트 사전 렌더링
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
prerender: {
crawlLinks: true,
// API에서 데이터를 가져와 동적 라우트 생성
routes: async () => {
// 빌드 시점에 API 호출
const posts = await $fetch('https://api.example.com/posts')
return (posts as any[]).map((post) => `/blog/${post.slug}`)
},
},
},
})
nuxi generate 명령어
# 정적 사이트 생성
npx nuxi generate
# 생성된 파일은 .output/public/ 에 저장
# 모든 정적 파일 서버(Netlify, GitHub Pages, Cloudflare Pages 등)에 배포 가능
하이브리드 렌더링 (routeRules)
Nuxt 3의 가장 강력한 기능 중 하나는 경로별로 다른 렌더링 전략을 적용하는 하이브리드 모드입니다.
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// 홈페이지 — 5분마다 재생성 (ISR: Incremental Static Regeneration)
'/': { isr: 300 },
// 블로그 목록 — 빌드 시 사전 렌더링 + CDN 캐시 1시간
'/blog': { prerender: true, headers: { 'cache-control': 's-maxage=3600' } },
// 블로그 개별 포스트 — 사전 렌더링
'/blog/**': { prerender: true },
// 대시보드 — SPA 모드 (인증 필요)
'/dashboard/**': { ssr: false },
// API — 캐시 없음, 항상 최신
'/api/user/**': { headers: { 'cache-control': 'no-store' } },
// 공개 API — CDN 캐시 1분
'/api/posts': { cache: { maxAge: 60 } },
},
})
ISR (Incremental Static Regeneration)
ISR은 정적 생성의 속도와 SSR의 신선함을 모두 얻는 전략입니다:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// 60초 동안 캐시, 이후 백그라운드에서 재생성
'/products/**': { isr: 60 },
// 항상 캐시 (수동으로 무효화할 때까지)
'/about': { isr: true },
},
})
배포 프리셋 (Deployment Presets)
Nitro는 다양한 배포 환경을 위한 프리셋을 내장하고 있습니다. NITRO_PRESET 환경 변수나 nuxt.config.ts로 설정합니다.
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'vercel', // 또는 'cloudflare-pages', 'netlify', 'node-server' 등
},
})
주요 프리셋:
| 프리셋 | 환경 |
|---|---|
node-server | Node.js 서버 (기본) |
vercel | Vercel Serverless Functions |
vercel-edge | Vercel Edge Functions |
cloudflare-pages | Cloudflare Pages |
cloudflare-module | Cloudflare Workers |
netlify | Netlify Functions |
netlify-edge | Netlify Edge Functions |
aws-lambda | AWS Lambda |
static | 완전 정적 사이트 |
Vercel 배포
Vercel은 Nuxt 3와 가장 긴밀하게 통합된 플랫폼 중 하나입니다.
자동 배포 설정
# Vercel CLI 설치
npm install -g vercel
# 프로젝트 루트에서 실행
vercel
# 프로덕션 배포
vercel --prod
Vercel은 Nuxt 3 프로젝트를 자동으로 감지하고 최적의 설정을 적용합니다.
vercel.json 커스텀 설정
{
"buildCommand": "npm run build",
"outputDirectory": ".output",
"framework": "nuxtjs",
"regions": ["icn1"],
"env": {
"DATABASE_URL": "@database-url",
"JWT_SECRET": "@jwt-secret"
},
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" }
]
}
]
}
환경 변수 설정
# Vercel CLI로 환경 변수 추가
vercel env add DATABASE_URL production
vercel env add JWT_SECRET production
# 또는 .env 파일 기반 (Vercel 대시보드에서도 설정 가능)
nuxt.config.ts — Vercel 최적화
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'vercel',
// Vercel Edge Functions 사용 시
// preset: 'vercel-edge',
},
routeRules: {
// Vercel의 ISR 활용
'/blog/**': { isr: 3600 },
'/products/**': { isr: 300 },
},
})
Cloudflare Pages 배포
Cloudflare Pages는 전 세계 엣지 네트워크에 배포되어 낮은 레이턴시를 제공합니다.
nuxt.config.ts 설정
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-pages',
},
})
빌드 및 배포
# 프로덕션 빌드
npm run build
# Wrangler CLI로 배포
npm install -g wrangler
wrangler pages deploy .output/public
wrangler.toml 설정
name = "my-nuxt-app"
compatibility_date = "2024-01-01"
pages_build_output_dir = ".output/public"
[vars]
NUXT_PUBLIC_APP_NAME = "My Nuxt App"
# 보안 시크릿은 wrangler secret put 명령어로 설정
# wrangler secret put DATABASE_URL
Cloudflare KV 스토리지 연동
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-pages',
cloudflare: {
pages: {
routes: {
include: ['/*'],
exclude: ['/assets/*'],
},
},
},
storage: {
// Cloudflare KV 사용
cache: {
driver: 'cloudflare-kv-binding',
binding: 'CACHE', // wrangler.toml에서 정의한 KV 네임스페이스 바인딩 이름
},
},
},
})
GitHub Actions CI/CD (Cloudflare Pages)
# .github/workflows/deploy.yml
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
NUXT_PUBLIC_API_BASE: ${{ secrets.API_BASE_URL }}
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: my-nuxt-app
directory: .output/public
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
Node.js 서버 배포
직접 관리하는 서버에 배포하는 경우입니다.
빌드 및 실행
# 프로덕션 빌드
npm run build
# 빌드 결과물 확인
ls .output/
# server/ — 서버 코드
# public/ — 정적 에셋
# 서버 실행
node .output/server/index.mjs
PM2로 프로세스 관리
npm install -g pm2
# 서버 시작
pm2 start .output/server/index.mjs --name "nuxt-app"
# 재시작
pm2 restart nuxt-app
# 로그 확인
pm2 logs nuxt-app
# 시스템 부팅 시 자동 시작
pm2 startup
pm2 save
ecosystem.config.js (PM2)
// ecosystem.config.js
module.exports = {
apps: [
{
name: 'nuxt-app',
script: '.output/server/index.mjs',
instances: 'max', // CPU 코어 수만큼 클러스터
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
NITRO_HOST: '0.0.0.0',
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
},
],
}
Nginx 리버스 프록시
# /etc/nginx/sites-available/nuxt-app
server {
listen 80;
server_name example.com www.example.com;
# HTTP → HTTPS 리다이렉트
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# 정적 에셋 직접 서빙 (Node.js 우회)
location /_nuxt/ {
alias /var/www/nuxt-app/.output/public/_nuxt/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Nuxt 앱으로 프록시
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
Docker 컨테이너 배포
# Dockerfile
FROM node:20-alpine AS base
# 의존성 레이어
FROM base AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# 빌드 레이어
FROM base AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 프로덕션 레이어 (최소 이미지)
FROM base AS runner
WORKDIR /app
# 비루트 사용자로 실행 (보안)
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nuxt
COPY --from=builder --chown=nuxt:nodejs /app/.output ./.output
USER nuxt
EXPOSE 3000
ENV PORT=3000
ENV HOST=0.0.0.0
ENV NODE_ENV=production
CMD ["node", ".output/server/index.mjs"]
# docker-compose.yml
version: '3.8'
services:
nuxt-app:
build: .
ports:
- '3000:3000'
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
- JWT_SECRET=${JWT_SECRET}
restart: unless-stopped
healthcheck:
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3000/api/health']
interval: 30s
timeout: 10s
retries: 3
nginx:
image: nginx:alpine
ports:
- '80:80'
- '443:443'
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- nuxt-app
restart: unless-stopped
환경별 설정 분리
// nuxt.config.ts
const isDev = process.env.NODE_ENV === 'development'
const isProd = process.env.NODE_ENV === 'production'
export default defineNuxtConfig({
runtimeConfig: {
// 서버 전용 시크릿
databaseUrl: process.env.DATABASE_URL ?? '',
jwtSecret: process.env.JWT_SECRET ?? 'dev-secret',
// 클라이언트에 노출되는 설정
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE ?? '/api',
appEnv: process.env.NODE_ENV ?? 'development',
},
},
nitro: {
// 프로덕션에서만 프리셋 적용
preset: isProd ? (process.env.NITRO_PRESET ?? 'node-server') : undefined,
// 개발 환경 스토리지
storage: isDev
? {
cache: { driver: 'fs', base: './.nitro/cache' },
}
: undefined,
},
// 프로덕션 빌드 최적화
...(isProd && {
experimental: {
payloadExtraction: true,
renderJsonPayloads: true,
},
}),
})
고수 팁
1. 빌드 분석으로 번들 최적화
# 번들 분석기 실행
npx nuxi analyze
# 또는 빌드 시 자동 분석
NUXT_ANALYZE=true npm run build
// nuxt.config.ts
export default defineNuxtConfig({
build: {
analyze: {
// rollup-plugin-visualizer 옵션
filename: '.nuxt/analyze/bundle.html',
open: true, // 분석 완료 후 브라우저에서 자동 오픈
},
},
})
2. 멀티 존 아키텍처 (Micro-Frontend)
여러 Nuxt 앱을 하나의 도메인에서 운영하는 전략:
// nuxt.config.ts (메인 앱)
export default defineNuxtConfig({
routeRules: {
// /docs/** 경로는 별도 Nuxt 앱으로 프록시
'/docs/**': {
proxy: {
to: 'https://docs-app.example.com/**',
},
},
// /shop/** 경로는 다른 앱으로 프록시
'/shop/**': {
proxy: {
to: 'https://shop-app.example.com/**',
},
},
},
})
3. 헬스체크 엔드포인트
// server/api/health.ts
export default defineEventHandler(() => {
return {
status: 'ok',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version ?? '0.0.0',
uptime: process.uptime(),
}
})
4. 보안 헤더 설정
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/**': {
headers: {
'X-Frame-Options': 'SAMEORIGIN',
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
},
},
},
// nuxt-security 모듈 사용 권장
// modules: ['nuxt-security'],
})
5. 제로 다운타임 배포 전략
# Blue-Green 배포 스크립트 예시
#!/bin/bash
# 새 버전 빌드
npm run build
# 새 컨테이너 시작 (포트 3001)
docker run -d --name nuxt-green -p 3001:3000 my-nuxt-app:latest
# 헬스체크 통과 대기
until curl -sf http://localhost:3001/api/health; do
echo "헬스체크 대기 중..."
sleep 2
done
# Nginx 트래픽을 새 버전으로 전환
sed -i 's/proxy_pass http:\/\/localhost:3000/proxy_pass http:\/\/localhost:3001/' /etc/nginx/sites-available/nuxt-app
nginx -s reload
# 이전 컨테이너 종료
docker stop nuxt-blue
docker rm nuxt-blue
docker rename nuxt-green nuxt-blue
echo "배포 완료!"
6. 성능 모니터링
// server/plugins/monitoring.ts
export default defineNitroPlugin((nitroApp) => {
// 응답 시간 측정 훅
nitroApp.hooks.hook('request', (event) => {
event.context._startTime = Date.now()
})
nitroApp.hooks.hook('afterResponse', (event) => {
const duration = Date.now() - (event.context._startTime ?? Date.now())
const url = getRequestURL(event)
// 느린 요청 경고 (500ms 초과)
if (duration > 500) {
console.warn(`[SLOW] ${getMethod(event)} ${url.pathname} — ${duration}ms`)
}
// 실제 앱에서는 Datadog, New Relic 등 APM 도구로 전송
})
})