Next.js의 프로덕션 배포를 다룹니다. instrumentation.ts, OpenTelemetry, 보안 헤더, Docker 배포, Build Adapters, 셀프 호스팅 전략을 살펴봅니다.
11장에서 이미지, 폰트, 메타데이터 최적화를 다루었습니다. 이번 장에서는 애플리케이션을 프로덕션에 배포하고 운영하는 데 필요한 모니터링, 보안, 배포 전략을 살펴봅니다.
Next.js 15에서 안정화된 instrumentation.ts는 서버가 시작될 때 한 번 실행되는 초기화 파일입니다. register() 함수에서 모니터링, 로깅, 트레이싱 인프라를 설정합니다.
export async function register() {
// 서버 시작 시 한 번 실행
console.log('Server starting - initializing monitoring...');
if (process.env.NEXT_RUNTIME === 'nodejs') {
// Node.js 런타임에서만 실행되는 초기화
// Edge Runtime에서는 실행되지 않음
const { initOpenTelemetry } = await import('./lib/telemetry');
initOpenTelemetry();
}
if (process.env.NEXT_RUNTIME === 'edge') {
// Edge Runtime 전용 초기화
const { initEdgeMonitoring } = await import('./lib/edge-monitoring');
initEdgeMonitoring();
}
}register() 함수는 동적 import()를 사용하여 런타임에 따라 다른 모듈을 로드합니다. 이는 Edge Runtime에서 Node.js 전용 모듈이 번들에 포함되는 것을 방지합니다.
onRequestError 훅은 서버 컴포넌트, Server Action, Route Handler에서 발생하는 에러를 중앙에서 캡처합니다.
import type { Instrumentation } from 'next';
export const onRequestError: Instrumentation.onRequestError = async (
error,
request,
context
) => {
// context에서 에러 발생 위치 정보 획득
const { routerKind, routePath, routeType } = context;
// 에러 리포팅 서비스로 전송
await fetch('https://error-reporting.example.com/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: error.message,
stack: error.stack,
request: {
method: request.method,
url: request.url,
headers: Object.fromEntries(request.headers),
},
context: {
routerKind, // 'Pages Router' | 'App Router'
routePath, // '/posts/[slug]'
routeType, // 'render' | 'route' | 'action' | 'middleware'
},
timestamp: new Date().toISOString(),
}),
});
};Next.js는 OpenTelemetry를 공식 지원합니다. @vercel/otel 패키지로 간편하게 설정할 수 있습니다.
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
export function initOpenTelemetry() {
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: 'my-nextjs-app',
[ATTR_SERVICE_VERSION]: process.env.APP_VERSION ?? '1.0.0',
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4318',
}),
});
sdk.start();
}8장에서 다룬 Server Actions의 보안 기능을 프로덕션 관점에서 재정리합니다.
| 기능 | 설명 | 설정 |
|---|---|---|
| Origin 확인 | CSRF 공격 방지를 위한 출처 검증 | serverActions.allowedOrigins |
| 암호화된 클로저 | 서버 변수의 클라이언트 노출 방지 | 자동 적용 |
| 데드 코드 제거 | 미사용 Server Action을 빌드에서 제거 | 자동 적용 |
| Action ID | 함수명 대신 해시 ID로 Action 참조 | 자동 적용 |
CSP 헤더는 XSS 공격을 방지하는 핵심 보안 메커니즘입니다. 미들웨어에서 Nonce 기반 CSP를 설정합니다.
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import crypto from 'crypto';
export async function middleware(request: NextRequest) {
// 요청마다 고유한 nonce 생성
const nonce = crypto.randomBytes(16).toString('base64');
// CSP 헤더 구성
const cspHeader = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'unsafe-inline'`, // Tailwind CSS 인라인 스타일
`img-src 'self' data: https:`,
`font-src 'self'`,
`connect-src 'self' https://vitals.vercel-insights.com`,
`frame-src 'self' https://giscus.app`,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`frame-ancestors 'none'`,
].join('; ');
// nonce를 요청 헤더로 전달 (서버 컴포넌트에서 사용)
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
const response = NextResponse.next({
request: { headers: requestHeaders },
});
response.headers.set('Content-Security-Policy', cspHeader);
return response;
}import { headers } from 'next/headers';
import Script from 'next/script';
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const headersList = await headers();
const nonce = headersList.get('x-nonce') ?? '';
return (
<html lang="ko">
<body>
{children}
<Script
src="https://www.googletagmanager.com/gtag/js"
nonce={nonce}
strategy="afterInteractive"
/>
</body>
</html>
);
}Server Actions는 Origin 확인이 기본 적용되지만, API Route Handlers에서는 직접 CSRF 보호를 구현해야 합니다.
import { cookies, headers } from 'next/headers';
export async function validateCsrf(): Promise<boolean> {
const headersList = await headers();
const cookieStore = await cookies();
const origin = headersList.get('origin');
const host = headersList.get('host');
// Origin과 Host 일치 확인
if (!origin || !host) return false;
try {
const originUrl = new URL(origin);
return originUrl.host === host;
} catch {
return false;
}
}Vercel은 Next.js의 공식 호스팅 플랫폼으로, 별도 설정 없이 배포할 수 있습니다.
# Vercel CLI 설치 및 배포
pnpm add -g vercel
vercel
# 프로덕션 배포
vercel --prodVercel의 장점은 Edge Functions, ISR, Image Optimization이 플랫폼 레벨에서 통합된다는 것입니다. 단점은 비용과 vendor lock-in입니다.
자체 인프라나 클라우드 서비스에 배포할 때는 Docker를 사용합니다. output: 'standalone' 설정으로 독립 실행 가능한 서버를 생성합니다.
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
output: 'standalone',
};
export default nextConfig;FROM node:22-alpine AS base
# 의존성 설치
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile
# 빌드
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable pnpm && pnpm build
# 프로덕션
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# standalone 출력 복사
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]output: 'standalone'은 node_modules에서 필요한 파일만 추출하여, Docker 이미지 크기를 크게 줄입니다. 일반적으로 1GB 이상의 node_modules가 50-100MB 수준으로 줄어듭니다.
sharp는 output: 'standalone' 모드에서 자동으로 포함됩니다. Next.js 15부터 sharp가 기본 이미지 처리 라이브러리이므로, 별도 설치가 불필요합니다.
AWS Amplify는 AWS 서울 리전에서 Next.js를 호스팅할 수 있는 서비스입니다.
version: 1
frontend:
phases:
preBuild:
commands:
- corepack enable pnpm
- pnpm install --frozen-lockfile
build:
commands:
- pnpm build
artifacts:
baseDirectory: .next
files:
- '**/*'
cache:
paths:
- node_modules/**/*
- .next/cache/**/*Amplify는 SSR, ISR, Image Optimization을 지원하며, CloudFront CDN이 자동으로 연결됩니다.
셀프 호스팅 환경에서 ISR의 캐시 만료 시간을 설정합니다. Vercel에서는 자동 관리되지만, 셀프 호스팅에서는 직접 설정해야 합니다.
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
expireTime: 3600, // ISR 캐시 만료 시간 (초)
};
export default nextConfig;import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
async headers() {
return [
{
// 정적 자산: 1년 캐싱 (파일명에 해시 포함)
source: '/_next/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
{
// 폰트: 1년 캐싱
source: '/fonts/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
{
// HTML: 재검증 필요
source: '/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=0, must-revalidate',
},
],
},
];
},
};
export default nextConfig;Next.js 16.2에서 안정화된 Build Adapters는 커스텀 배포 플랫폼을 위한 빌드 출력 변환 인터페이스입니다.
Build Adapter를 사용하면 Next.js의 빌드 출력을 특정 플랫폼의 형식으로 변환할 수 있습니다. 공식 어댑터가 없는 플랫폼에서도 Next.js를 배포할 수 있게 됩니다.
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// 커스텀 빌드 어댑터 지정
buildAdapter: '@my-platform/nextjs-adapter',
};
export default nextConfig;Next.js 15부터 next.config.ts가 공식 지원됩니다. JavaScript 설정 파일에서 마이그레이션할 수 있습니다.
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// TypeScript의 자동 완성과 타입 검증을 활용
reactStrictMode: true,
poweredByHeader: false,
// 환경 변수 타입 안전성
env: {
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL ?? '',
},
// 리디렉트
async redirects() {
return [
{
source: '/blog/:slug',
destination: '/tech/:slug',
permanent: true,
},
];
},
};
export default nextConfig;// 1. 서버 전용 (브라우저에 노출되지 않음)
// DATABASE_URL=postgresql://...
// JWT_SECRET=my-secret-key
// OPENAI_API_KEY=sk-...
// 2. 클라이언트 노출 (NEXT_PUBLIC_ 접두사)
// NEXT_PUBLIC_SITE_URL=https://archive.kreathlab.com
// NEXT_PUBLIC_GA_ID=G-XXXXXXX
// 3. 런타임 vs 빌드 타임
// 빌드 타임: NEXT_PUBLIC_* 변수는 빌드 시 인라인됨
// 런타임: 서버 전용 변수는 런타임에 읽힘NEXT_PUBLIC_ 접두사가 붙은 변수는 클라이언트 JavaScript 번들에 인라인됩니다. API 키, 시크릿, 데이터베이스 URL에는 절대 이 접두사를 사용하지 마세요. 브라우저 개발자 도구에서 누구나 볼 수 있습니다.
| 특성 | Static Export | Dynamic Rendering |
|---|---|---|
| 설정 | output: 'export' | 기본값 |
| 서버 | 불필요 (정적 파일만) | Node.js 서버 필요 |
| SSR | 불가 | 가능 |
| API Routes | 불가 | 가능 |
| ISR | 불가 | 가능 |
| Image Optimization | 불가 (외부 서비스 필요) | next/image 사용 가능 |
| 호스팅 | S3, GitHub Pages, Netlify | Vercel, Docker, Amplify |
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
output: 'export',
// Static Export에서는 이미지 최적화 비활성화
images: {
unoptimized: true,
},
};
export default nextConfig;Static Export는 서버가 필요 없는 단순한 사이트에 적합합니다. 블로그, 문서 사이트, 포트폴리오 등이 좋은 사례입니다. 하지만 Server Actions, ISR, 미들웨어 등 서버 기능은 사용할 수 없습니다.
pnpm build가 에러 없이 통과하는지 확인next/image 사용, remotePatterns 설정instrumentation.ts에서 에러 리포팅과 트레이싱 설정expireTime 설정instrumentation.ts의 register() 함수로 서버 시작 시 모니터링 인프라를 초기화하고, onRequestError 훅으로 에러를 중앙에서 캡처합니다.output: 'standalone'으로 Docker 이미지 크기를 최소화하고, AWS Amplify나 Vercel로 간편하게 배포할 수 있습니다.next.config.ts의 TypeScript 지원으로 타입 안전한 설정이 가능합니다.expireTime, Cache-Control 헤더, sharp 자동 포함 등을 직접 관리해야 합니다.다음 장에서는 이 시리즈의 마지막으로, 지금까지 다룬 모든 기능을 활용하여 실전 프로젝트를 구축합니다. Link Shortener 앱을 만들며 시리즈 전체를 복습합니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
Link Shortener 앱을 구축하며 Next.js App Router의 모든 기능을 실전에 적용합니다. 프로젝트 구조 설계부터 배포까지 전 과정을 다룹니다.
Next.js의 이미지, 폰트, 메타데이터 최적화를 다룹니다. next/image, next/font, generateMetadata, OG 이미지 생성, Core Web Vitals 전략을 살펴봅니다.
Next.js의 미들웨어 진화를 다룹니다. middleware.ts에서 proxy.ts로의 전환, 인증, i18n, A/B 테스팅, 속도 제한 등 고급 패턴을 살펴봅니다.