본문으로 건너뛰기
Advertisement

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-serverNode.js 서버 (기본)
vercelVercel Serverless Functions
vercel-edgeVercel Edge Functions
cloudflare-pagesCloudflare Pages
cloudflare-moduleCloudflare Workers
netlifyNetlify Functions
netlify-edgeNetlify Edge Functions
aws-lambdaAWS 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 도구로 전송
})
})
Advertisement