1.high-traffic-enduable-architecture
대규모 트래픽을 위한 Next.js 아키텍처 설계
1. 전체 아키텍처 개요
┌─────────────┐
│ 사용자 │
└──────┬──────┘
│
▼
┌─────────────────────────────────────────┐
│ CDN (CloudFront) │
│ • 정적 파일 (JS, CSS, 이미지) │
│ • ISR/SSG 페이지 캐싱 │
│ • Edge Functions (Optional) │
└──────┬──────────────────────────────────┘
│ (캐시 미스만)
▼
┌─────────────────────────────────────────┐
│ Next.js App (Vercel/AWS) │
│ • App Router (Server Components) │
│ • API Routes │
│ • ISR Revalidation │
└──────┬──────────────────────────────────┘
│
├──────────┬──────────┬─────────────┐
▼ ▼ ▼ ▼
┌──────────┐ ┌────────┐ ┌─────────┐ ┌──────────┐
│ Redis/ │ │Database│ │ Message │ │ Object │
│ KV Store │ │ (RDS) │ │ Queue │ │ Storage │
│ (ElastiCache)│ │ │ (SQS) │ │ (S3) │
└──────────┘ └────────┘ └─────────┘ └──────────┘
2. 계층별 상세 설계
2.1 CDN 레이어 (CloudFront / Vercel Edge Network)
역할: 전 세계 사용자에게 낮은 지연시간으로 콘텐츠 제공
javascript
// next.config.js - CDN 최적화 설정
module.exports = {
images: {
domains: ['cdn.example.com'],
loader: 'cloudinary', // 또는 'imgix'
formats: ['image/avif', 'image/webp'],
},
// 정적 파일 캐싱 설정
async headers() {
return [
{
source: '/_next/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
{
source: '/images/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=86400, s-maxage=604800',
},
],
},
];
},
};
CDN 캐싱 전략:
- 정적 자산: 1년 캐싱 (
immutable
) - ISR 페이지:
s-maxage
설정으로 CDN 레벨 캐싱 - API 응답: 적절한
Cache-Control
헤더로 CDN 캐싱
typescript
// app/products/[id]/page.tsx - ISR with CDN
export const revalidate = 3600; // 1시간
export async function generateMetadata({ params }) {
// 메타데이터도 캐싱됨
const product = await fetchFromCache(params.id);
return { title: product.name };
}
export default async function ProductPage({ params }) {
const product = await fetchFromCache(params.id);
return (
<div>
<ProductDetails product={product} />
</div>
);
}
2.2 K-V Storage (Redis/Vercel KV) 레이어
역할: 초고속 데이터 캐싱 및 세션 관리
typescript
// lib/redis.ts
import { createClient } from 'redis';
const redis = createClient({
url: process.env.REDIS_URL,
socket: {
reconnectStrategy: (retries) => Math.min(retries * 50, 1000),
},
});
redis.connect();
// 캐싱 유틸리티
export async function getCached<T>(
key: string,
fetcher: () => Promise<T>,
ttl: number = 3600
): Promise<T> {
// 1. Redis에서 먼저 확인
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
// 2. 캐시 미스 시 DB 조회
const data = await fetcher();
// 3. Redis에 저장
await redis.setEx(key, ttl, JSON.stringify(data));
return data;
}
// 캐시 무효화
export async function invalidateCache(pattern: string) {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(keys);
}
}
실전 적용 예시:
typescript
// app/api/products/[id]/route.ts
import { getCached, invalidateCache } from '@/lib/redis';
import { NextResponse } from 'next/server';
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const product = await getCached(
`product:${params.id}`,
async () => {
// DB 조회는 캐시 미스 시에만
return await db.product.findUnique({
where: { id: params.id },
include: { reviews: true, images: true },
});
},
3600 // 1시간 캐싱
);
return NextResponse.json(product, {
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
},
});
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
const body = await request.json();
// DB 업데이트
const updated = await db.product.update({
where: { id: params.id },
data: body,
});
// 캐시 무효화
await invalidateCache(`product:${params.id}*`);
return NextResponse.json(updated);
}
2.3 Redis Pub/Sub - 실시간 캐시 무효화
역할: 여러 서버 인스턴스 간 캐시 동기화
typescript
// lib/cache-sync.ts
import { createClient } from 'redis';
const publisher = createClient({ url: process.env.REDIS_URL });
const subscriber = createClient({ url: process.env.REDIS_URL });
await publisher.connect();
await subscriber.connect();
// 캐시 무효화 이벤트 발행
export async function publishCacheInvalidation(key: string) {
await publisher.publish('cache:invalidate', JSON.stringify({ key }));
}
// 다른 서버의 무효화 이벤트 구독
subscriber.subscribe('cache:invalidate', (message) => {
const { key } = JSON.parse(message);
console.log(`Cache invalidated: ${key}`);
// 로컬 캐시 무효화 처리
localCache.delete(key);
});
// 실시간 데이터 푸시
export async function publishProductUpdate(productId: string, data: any) {
await publisher.publish(
`product:${productId}:update`,
JSON.stringify(data)
);
}
실시간 업데이트 예시:
typescript
// app/products/[id]/realtime.tsx
'use client';
import { useEffect, useState } from 'react';
export function RealtimeStockUpdater({ productId, initialStock }) {
const [stock, setStock] = useState(initialStock);
useEffect(() => {
// Server-Sent Events로 실시간 재고 업데이트
const eventSource = new EventSource(`/api/products/${productId}/subscribe`);
eventSource.addEventListener('stock-update', (event) => {
const data = JSON.parse(event.data);
setStock(data.stock);
});
return () => eventSource.close();
}, [productId]);
return <span>재고: {stock}개</span>;
}
// app/api/products/[id]/subscribe/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const stream = new ReadableStream({
start(controller) {
// Redis Pub/Sub 구독
const subscriber = createClient({ url: process.env.REDIS_URL });
subscriber.connect();
subscriber.subscribe(`product:${params.id}:update`, (message) => {
const data = `data: ${message}\n\n`;
controller.enqueue(new TextEncoder().encode(data));
});
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
2.4 Message Queue (AWS SQS / RabbitMQ) 레이어
역할: 비동기 작업 처리 및 부하 분산
typescript
// lib/queue.ts
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
const sqsClient = new SQSClient({ region: 'ap-northeast-2' });
export async function enqueueTask(queueName: string, task: any) {
const command = new SendMessageCommand({
QueueUrl: process.env[`${queueName.toUpperCase()}_QUEUE_URL`],
MessageBody: JSON.stringify(task),
MessageAttributes: {
TaskType: {
DataType: 'String',
StringValue: task.type,
},
},
});
await sqsClient.send(command);
}
// 이메일 발송 큐
export async function sendEmailAsync(to: string, subject: string, body: string) {
await enqueueTask('email', {
type: 'send_email',
data: { to, subject, body },
});
}
// 이미지 처리 큐
export async function processImageAsync(imageUrl: string, productId: string) {
await enqueueTask('image', {
type: 'process_image',
data: { imageUrl, productId },
});
}
// 분석 이벤트 큐
export async function trackEventAsync(userId: string, event: string, data: any) {
await enqueueTask('analytics', {
type: 'track_event',
data: { userId, event, data, timestamp: Date.now() },
});
}
실전 적용 - 주문 처리:
typescript
// app/api/orders/route.ts
import { enqueueTask, invalidateCache } from '@/lib/queue';
export async function POST(request: Request) {
const order = await request.json();
// 1. 주문 DB 저장 (빠른 응답)
const createdOrder = await db.order.create({
data: {
...order,
status: 'pending',
},
});
// 2. 비동기 작업들을 큐에 등록
await Promise.all([
// 재고 차감 (중요, 우선순위 높음)
enqueueTask('inventory', {
type: 'decrease_stock',
data: { orderId: createdOrder.id, items: order.items },
priority: 'high',
}),
// 결제 처리
enqueueTask('payment', {
type: 'process_payment',
data: { orderId: createdOrder.id, amount: order.total },
priority: 'high',
}),
// 이메일 발송 (낮은 우선순위)
enqueueTask('email', {
type: 'order_confirmation',
data: { orderId: createdOrder.id, email: order.email },
priority: 'low',
}),
// 분석 이벤트
enqueueTask('analytics', {
type: 'order_placed',
data: { orderId: createdOrder.id, value: order.total },
priority: 'low',
}),
]);
// 3. 즉시 응답 (빠른 사용자 경험)
return NextResponse.json(createdOrder, { status: 201 });
}
워커 프로세스 예시:
typescript
// workers/inventory-worker.ts
import { SQSClient, ReceiveMessageCommand, DeleteMessageCommand } from '@aws-sdk/client-sqs';
const sqsClient = new SQSClient({ region: 'ap-northeast-2' });
async function processInventoryQueue() {
while (true) {
const { Messages } = await sqsClient.send(
new ReceiveMessageCommand({
QueueUrl: process.env.INVENTORY_QUEUE_URL,
MaxNumberOfMessages: 10,
WaitTimeSeconds: 20, // Long polling
})
);
if (!Messages) continue;
await Promise.all(
Messages.map(async (message) => {
try {
const task = JSON.parse(message.Body);
if (task.type === 'decrease_stock') {
// 재고 차감 처리
await db.$transaction(async (tx) => {
for (const item of task.data.items) {
await tx.product.update({
where: { id: item.productId },
data: {
stock: { decrement: item.quantity },
},
});
}
});
// 캐시 무효화
await invalidateCache('product:*');
// 실시간 재고 업데이트 pub
await publishProductUpdate(task.data.items[0].productId, {
stock: updatedStock,
});
}
// 메시지 삭제
await sqsClient.send(
new DeleteMessageCommand({
QueueUrl: process.env.INVENTORY_QUEUE_URL,
ReceiptHandle: message.ReceiptHandle,
})
);
} catch (error) {
console.error('Failed to process message:', error);
// DLQ로 이동하거나 재시도 로직
}
})
);
}
}
processInventoryQueue();
3. 통합 시나리오: 대용량 쇼핑몰
시나리오: 동시 사용자 10만명, 플래시 세일
typescript
// app/flash-sale/[id]/page.tsx
export const revalidate = 10; // 10초마다 ISR
export default async function FlashSalePage({ params }) {
// 1. CDN에서 페이지 제공 (대부분의 트래픽)
const sale = await getCached(
`sale:${params.id}`,
async () => db.sale.findUnique({ where: { id: params.id } }),
10 // Redis 10초 캐싱
);
return (
<div>
<h1>{sale.title}</h1>
{/* 서버 컴포넌트: 초기 데이터 */}
<ProductList products={sale.products.slice(0, 20)} />
{/* 클라이언트 컴포넌트: 실시간 재고 */}
<RealtimeStockUpdater products={sale.products} />
</div>
);
}
// app/api/flash-sale/purchase/route.ts
export async function POST(request: Request) {
const { saleId, productId, userId } = await request.json();
// 1. Redis로 재고 확인 (초고속)
const stock = await redis.get(`stock:${productId}`);
if (stock <= 0) {
return NextResponse.json({ error: 'Sold out' }, { status: 400 });
}
// 2. Redis로 낙관적 재고 차감
const newStock = await redis.decr(`stock:${productId}`);
if (newStock < 0) {
await redis.incr(`stock:${productId}`); // 롤백
return NextResponse.json({ error: 'Sold out' }, { status: 400 });
}
// 3. 주문 생성 (DB)
const order = await db.order.create({
data: { userId, productId, status: 'pending' },
});
// 4. 비동기 처리 큐에 등록
await enqueueTask('payment', {
type: 'process_payment',
data: { orderId: order.id },
});
// 5. 실시간 재고 업데이트 pub
await publishProductUpdate(productId, { stock: newStock });
// 6. 즉시 응답 (빠른 UX)
return NextResponse.json({ orderId: order.id, stock: newStock });
}
트래픽 분산 효과
100,000 동시 사용자 시나리오:
1. 페이지 로드
- 95,000명: CDN에서 직접 제공 (서버 부하 0)
- 4,000명: ISR 캐시에서 제공 (서버 부하 낮음)
- 1,000명: 서버 렌더링 (새로운 상품, 캐시 미스)
2. 재고 조회
- 100,000명: Redis에서 조회 (ms 단위 응답)
- DB 부하: 0 (모든 재고 데이터는 Redis에)
3. 구매 요청
- 10,000명: 실제 구매 시도
- Redis: 재고 차감 (원자적 연산)
- DB: 주문 생성만 (빠름)
- Queue: 결제/이메일 비동기 처리
4. 결과
- 평균 응답 시간: 50ms (CDN) ~ 200ms (구매)
- 서버 CPU: 30% 이하
- DB 부하: 최소화
4. 성능 최적화 체크리스트
CDN 레이어
- 정적 자산 모두 CDN에서 제공
- ISR로 자주 변경되는 페이지 캐싱
- 이미지 최적화 (WebP, AVIF)
- Edge Functions로 간단한 로직 처리
K-V Storage 레이어
- 핫 데이터는 모두 Redis에 캐싱
- TTL 전략 수립 (데이터 특성별)
- 캐시 워밍 (인기 상품 미리 로드)
- Redis Cluster로 고가용성 확보
Pub/Sub 레이어
- 실시간 업데이트가 필요한 데이터 식별
- SSE/WebSocket으로 클라이언트 연결
- 여러 서버 간 캐시 동기화
Message Queue 레이어
- 무거운 작업은 모두 비동기 처리
- 우선순위 큐 분리 (중요/일반/낮음)
- Dead Letter Queue 설정
- 워커 프로세스 Auto Scaling
Database 레이어
- Read Replica 사용
- Connection Pooling
- 인덱스 최적화
- 쿼리 최적화 (N+1 방지)
5. 비용 최적화
월 1000만 페이지뷰 기준:
전통적인 SSR:
- EC2 (t3.large x 10): $730/월
- RDS: $200/월
- 총: $930/월
최적화된 아키텍처:
- Vercel (Pro): $20/월
- CloudFront: $50/월
- Redis (ElastiCache t3.micro): $15/월
- SQS: $5/월
- 총: $90/월
절감: 90% ↓
6. 모니터링 및 알림
typescript
// lib/monitoring.ts
import * as Sentry from '@sentry/nextjs';
export function trackPerformance(metric: string, value: number) {
// CloudWatch / Datadog
console.log(`[Metric] ${metric}: ${value}ms`);
}
export async function monitorCacheHitRate() {
const hits = await redis.get('cache:hits');
const misses = await redis.get('cache:misses');
const hitRate = hits / (hits + misses);
if (hitRate < 0.8) {
// 캐시 히트율 80% 미만 시 알림
await sendAlert('Low cache hit rate', { hitRate });
}
}
export async function monitorQueueDepth() {
const depth = await getQueueDepth('payment');
if (depth > 1000) {
// 큐 적체 시 워커 증설
await scaleWorkers('payment-worker', { desired: 5 });
}
}
이러한 아키텍처를 통해 서버 부하를 최소화하고 무한 확장 가능한 Next.js 애플리케이션을 구축할 수 있습니다.