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 애플리케이션을 구축할 수 있습니다.