서버리스 플랫폼 비교 분석 2025

22 min read
compareserverlesslambdavercelcloudflareedgefaascloud

AWS Lambda vs Vercel vs Cloudflare Workers - 서버리스 플랫폼 선택 가이드

⚡ 서버리스 플랫폼 비교 분석 2025

서버리스 아키텍처를 위한 최적의 플랫폼 선택 가이드


📊 개요

비교 대상

  • AWS Lambda: 서버리스 컴퓨팅의 선구자
  • Vercel: 프론트엔드 중심 서버리스
  • Cloudflare Workers: 엣지 컴퓨팅 플랫폼
  • Netlify Functions: JAMstack 친화적
  • Google Cloud Functions: GCP 통합
  • Azure Functions: 엔터프라이즈 서버리스

평가 기준

  • 성능 (콜드 스타트, 응답 시간)
  • 가격
  • 개발 경험
  • 생태계
  • 제한 사항
  • 확장성

📈 상세 비교표

핵심 특성 비교

| 특성 | AWS Lambda | Vercel | Cloudflare Workers | Netlify | | ------------------ | ----------- | ------------------- | ------------------ | --------- | | 콜드 스타트 | 100-300ms | 50-150ms | <10ms | 100-200ms | | 최대 실행 시간 | 15분 | 10초(무료)/300초 | 30초 | 10초 | | 메모리 | 128MB-10GB | 1GB-3GB | 128MB | 1GB | | 언어 지원 | 다양함 | Node/Python/Go/Ruby | JS/Wasm | JS/Go | | 엣지 배포 | CloudFront | 글로벌 | 280+ 도시 | CDN 통합 | | 가격 | 사용량 기반 | 무료 시작 | 저렴함 | 무료 시작 |

기술적 제한사항

| 항목 | AWS Lambda | Vercel | Cloudflare Workers | Netlify | | ----------------- | ---------- | --------- | ------------------ | --------- | | 패키지 크기 | 250MB | 50MB | 1MB | 50MB | | 환경 변수 | 4KB | 64KB | 1MB | 무제한 | | 동시 실행 | 1000(기본) | 무제한 | 무제한 | 무제한 | | HTTP 페이로드 | 6MB | 4.5MB | 100MB | 6MB | | 파일 시스템 | /tmp 512MB | 읽기 전용 | 없음 | 읽기 전용 |

가격 비교 (월 기준)

| 사용량 | AWS Lambda | Vercel | Cloudflare Workers | Netlify | | --------------- | ---------------- | ------ | ------------------ | ------- | | 100만 요청 | $0.20 | $0 | $5 | $0 | | 1000만 요청 | $2 | $20 | $50 | $19 | | 1억 요청 | $20 | $200 | $500 | $190 | | 컴퓨팅 시간 | $0.0000166/GB-초 | 포함 | 포함 | 포함 |


💼 사용 사례별 구현

🌐 글로벌 API 서비스

추천: Cloudflare Workers

// Cloudflare Workers - 엣지에서 실행되는 API
addEventListener("fetch", (event) => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  // 지역별 라우팅
  const country = request.cf?.country || "US";
  const colo = request.cf?.colo || "Unknown";

  // KV 스토어에서 캐시 확인
  const cache = caches.default;
  const cacheKey = new Request(request.url.toString(), request);
  const cachedResponse = await cache.match(cacheKey);

  if (cachedResponse) {
    return cachedResponse;
  }

  // API 라우팅
  const url = new URL(request.url);

  switch (url.pathname) {
    case "/api/users":
      return handleUsers(request, country);
    case "/api/products":
      return handleProducts(request);
    case "/api/geo":
      return handleGeoData(request, country, colo);
    default:
      return new Response("Not Found", { status: 404 });
  }
}

async function handleUsers(request, country) {
  // Durable Objects를 사용한 상태 관리
  const id = USERS_NAMESPACE.idFromName(country);
  const stub = USERS_NAMESPACE.get(id);

  return stub.fetch(request);
}

// Durable Object 클래스
export class UserCounter {
  constructor(state, env) {
    this.state = state;
    this.env = env;
  }

  async fetch(request) {
    const url = new URL(request.url);

    if (request.method === "GET") {
      const value = (await this.state.storage.get("count")) || 0;
      return new Response(JSON.stringify({ count: value }));
    }

    if (request.method === "POST") {
      let value = (await this.state.storage.get("count")) || 0;
      value++;
      await this.state.storage.put("count", value);

      return new Response(JSON.stringify({ count: value }));
    }
  }
}

// WebSocket 지원
async function handleWebSocket(request) {
  const upgradeHeader = request.headers.get("Upgrade");
  if (upgradeHeader !== "websocket") {
    return new Response("Expected websocket", { status: 400 });
  }

  const [client, server] = Object.values(new WebSocketPair());

  server.accept();
  server.addEventListener("message", (event) => {
    server.send(
      JSON.stringify({
        type: "echo",
        data: event.data,
        timestamp: Date.now(),
      }),
    );
  });

  return new Response(null, {
    status: 101,
    webSocket: client,
  });
}

// R2 스토리지 활용
async function handleFileUpload(request) {
  const formData = await request.formData();
  const file = formData.get("file");

  if (!file) {
    return new Response("No file uploaded", { status: 400 });
  }

  const key = `uploads/${Date.now()}-${file.name}`;
  await R2_BUCKET.put(key, file.stream(), {
    httpMetadata: {
      contentType: file.type,
    },
  });

  return new Response(
    JSON.stringify({
      key,
      size: file.size,
      type: file.type,
    }),
    {
      headers: { "Content-Type": "application/json" },
    },
  );
}

🚀 Next.js 풀스택 앱

추천: Vercel

// Vercel Functions - Next.js API Routes
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'

// Edge Runtime for better performance
export const runtime = 'edge'

const productSchema = z.object({
  name: z.string().min(1),
  price: z.number().positive(),
  description: z.string(),
  category: z.string()
})

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const category = searchParams.get('category')
  const limit = Number(searchParams.get('limit')) || 10

  // Vercel KV for caching
  const cacheKey = `products:${category || 'all'}:${limit}`
  const cached = await kv.get(cacheKey)

  if (cached) {
    return NextResponse.json(cached)
  }

  const products = await prisma.product.findMany({
    where: category ? { category } : undefined,
    take: limit,
    orderBy: { createdAt: 'desc' }
  })

  // Cache for 5 minutes
  await kv.setex(cacheKey, 300, products)

  return NextResponse.json(products)
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const validated = productSchema.parse(body)

    const product = await prisma.product.create({
      data: validated
    })

    // Revalidate cache
    revalidatePath('/products')

    // Send to analytics
    await fetch('https://analytics.vercel.com/api/events', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.VERCEL_ANALYTICS_TOKEN}`
      },
      body: JSON.stringify({
        event: 'product_created',
        properties: {
          product_id: product.id,
          category: product.category,
          price: product.price
        }
      })
    })

    return NextResponse.json(product, { status: 201 })
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: error.errors },
        { status: 400 }
      )
    }

    return NextResponse.json(
      { error: 'Internal Server Error' },
      { status: 500 }
    )
  }
}

// Vercel Cron Jobs
// app/api/cron/cleanup/route.ts
export async function GET(request: NextRequest) {
  // Verify cron secret
  const authHeader = request.headers.get('authorization')
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return new Response('Unauthorized', { status: 401 })
  }

  // Cleanup old data
  const deleted = await prisma.product.deleteMany({
    where: {
      createdAt: {
        lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // 30 days
      }
    }
  })

  return NextResponse.json({
    deleted: deleted.count,
    timestamp: new Date().toISOString()
  })
}

// vercel.json
{
  "crons": [{
    "path": "/api/cron/cleanup",
    "schedule": "0 2 * * *"
  }]
}

💼 엔터프라이즈 워크플로우

추천: AWS Lambda

# AWS Lambda - 복잡한 비즈니스 로직
import json
import boto3
from aws_lambda_powertools import Logger, Tracer, Metrics
from aws_lambda_powertools.metrics import MetricUnit
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.utilities.typing import LambdaContext

logger = Logger()
tracer = Tracer()
metrics = Metrics()
app = APIGatewayRestResolver()

dynamodb = boto3.resource('dynamodb')
s3 = boto3.client('s3')
sns = boto3.client('sns')
sqs = boto3.client('sqs')

# Step Functions 워크플로우의 일부
@tracer.capture_lambda_handler
@logger.inject_lambda_context
@metrics.log_metrics
def process_order_handler(event: dict, context: LambdaContext) -> dict:
    """주문 처리 Lambda"""

    order_id = event['orderId']
    logger.info(f"Processing order: {order_id}")

    try:
        # 1. DynamoDB에서 주문 조회
        table = dynamodb.Table('orders')
        response = table.get_item(Key={'orderId': order_id})
        order = response.get('Item')

        if not order:
            raise ValueError(f"Order {order_id} not found")

        # 2. 재고 확인 (다른 Lambda 호출)
        lambda_client = boto3.client('lambda')
        inventory_response = lambda_client.invoke(
            FunctionName='check-inventory',
            InvocationType='RequestResponse',
            Payload=json.dumps({
                'items': order['items']
            })
        )

        inventory_result = json.loads(
            inventory_response['Payload'].read()
        )

        if not inventory_result['available']:
            raise ValueError("Insufficient inventory")

        # 3. 결제 처리
        payment_result = process_payment(order)

        # 4. S3에 영수증 저장
        receipt = generate_receipt(order, payment_result)
        s3.put_object(
            Bucket='receipts-bucket',
            Key=f'receipts/{order_id}.pdf',
            Body=receipt,
            ContentType='application/pdf'
        )

        # 5. 주문 상태 업데이트
        table.update_item(
            Key={'orderId': order_id},
            UpdateExpression='SET #status = :status, paymentId = :paymentId',
            ExpressionAttributeNames={'#status': 'status'},
            ExpressionAttributeValues={
                ':status': 'PAID',
                ':paymentId': payment_result['transactionId']
            }
        )

        # 6. SNS 알림 발송
        sns.publish(
            TopicArn='arn:aws:sns:region:account:order-notifications',
            Message=json.dumps({
                'orderId': order_id,
                'status': 'PAID',
                'customerId': order['customerId']
            }),
            MessageAttributes={
                'eventType': {
                    'DataType': 'String',
                    'StringValue': 'ORDER_PAID'
                }
            }
        )

        # 7. 메트릭 기록
        metrics.add_metric(
            name="OrderProcessed",
            unit=MetricUnit.Count,
            value=1
        )
        metrics.add_metric(
            name="OrderAmount",
            unit=MetricUnit.Count,
            value=order['totalAmount']
        )

        return {
            'statusCode': 200,
            'body': json.dumps({
                'orderId': order_id,
                'status': 'PAID',
                'paymentId': payment_result['transactionId']
            })
        }

    except Exception as e:
        logger.exception(f"Error processing order {order_id}")

        # Dead Letter Queue로 전송
        sqs.send_message(
            QueueUrl='https://sqs.region.amazonaws.com/account/dlq',
            MessageBody=json.dumps({
                'orderId': order_id,
                'error': str(e),
                'timestamp': context.aws_request_id
            })
        )

        raise

# EventBridge 이벤트 처리
@tracer.capture_lambda_handler
def event_processor_handler(event: dict, context: LambdaContext) -> dict:
    """EventBridge 이벤트 처리"""

    for record in event.get('Records', []):
        event_detail = json.loads(record['body'])
        event_type = event_detail.get('detail-type')

        if event_type == 'ORDER_CREATED':
            handle_order_created(event_detail['detail'])
        elif event_type == 'PAYMENT_COMPLETED':
            handle_payment_completed(event_detail['detail'])
        elif event_type == 'SHIPMENT_DISPATCHED':
            handle_shipment_dispatched(event_detail['detail'])

    return {'statusCode': 200}

# Lambda Extensions로 설정 관리
import requests

def get_config():
    """Lambda Extension에서 설정 가져오기"""
    response = requests.get(
        'http://localhost:2773/systemsmanager/parameters/get',
        params={'name': '/myapp/config'},
        headers={'X-Aws-Parameters-Secrets-Token': os.environ.get('AWS_SESSION_TOKEN')}
    )
    return response.json()

🎮 실시간 게임 백엔드

추천: Cloudflare Workers + Durable Objects

// Cloudflare Durable Objects - 게임 룸 상태 관리
export class GameRoom {
  state: DurableObjectState;
  sessions: Map<WebSocket, PlayerInfo>;
  gameState: GameState;

  constructor(state: DurableObjectState) {
    this.state = state;
    this.sessions = new Map();
    this.gameState = {
      players: new Map(),
      board: createEmptyBoard(),
      turn: 0,
      status: "waiting",
    };
  }

  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);

    switch (url.pathname) {
      case "/websocket":
        return this.handleWebSocket(request);
      case "/state":
        return this.getGameState();
      case "/action":
        return this.handleAction(request);
      default:
        return new Response("Not found", { status: 404 });
    }
  }

  async handleWebSocket(request: Request): Promise<Response> {
    const pair = new WebSocketPair();
    const [client, server] = Object.values(pair);

    this.state.acceptWebSocket(server);

    const playerId = new URL(request.url).searchParams.get("playerId");
    this.sessions.set(server, { playerId, ws: server });

    server.addEventListener("message", async (event) => {
      await this.handleMessage(server, event.data);
    });

    server.addEventListener("close", () => {
      this.handleDisconnect(server);
    });

    // 현재 게임 상태 전송
    server.send(
      JSON.stringify({
        type: "gameState",
        data: this.serializeGameState(),
      }),
    );

    return new Response(null, { status: 101, webSocket: client });
  }

  async handleMessage(ws: WebSocket, message: string) {
    const data = JSON.parse(message);
    const player = this.sessions.get(ws);

    switch (data.type) {
      case "move":
        await this.processMove(player.playerId, data.move);
        break;
      case "chat":
        this.broadcastChat(player.playerId, data.message);
        break;
    }
  }

  async processMove(playerId: string, move: Move) {
    // 게임 로직 검증
    if (!this.isValidMove(playerId, move)) {
      return;
    }

    // 상태 업데이트
    this.gameState.board[move.x][move.y] = playerId;
    this.gameState.turn++;

    // 상태 저장
    await this.state.storage.put("gameState", this.gameState);

    // 모든 플레이어에게 브로드캐스트
    this.broadcast({
      type: "move",
      playerId,
      move,
      nextTurn: this.getNextPlayer(),
    });

    // 승리 조건 확인
    if (this.checkWinCondition(playerId)) {
      this.endGame(playerId);
    }
  }

  broadcast(message: any) {
    const data = JSON.stringify(message);
    this.sessions.forEach(({ ws }) => {
      ws.send(data);
    });
  }
}

// Workers KV를 사용한 리더보드
export async function updateLeaderboard(
  playerId: string,
  score: number,
  env: Env,
) {
  const key = `leaderboard:${new Date().toISOString().slice(0, 10)}`;

  const current = (await env.LEADERBOARD.get(key, "json")) || [];
  current.push({ playerId, score, timestamp: Date.now() });

  // 상위 100명만 유지
  const sorted = current.sort((a, b) => b.score - a.score).slice(0, 100);

  await env.LEADERBOARD.put(key, JSON.stringify(sorted), {
    expirationTtl: 86400 * 30, // 30일 보관
  });

  return sorted;
}

🏢 실제 기업 사례

AWS Lambda 사용

  • Netflix: 비디오 인코딩
  • iRobot: IoT 데이터 처리
  • 카카오: 이미지 처리
  • 토스: 일부 마이크로서비스

Vercel 사용

  • TikTok: 웹 프론트엔드
  • McDonald's: 주문 시스템
  • Hulu: 마케팅 사이트
  • 당근마켓: 웹 서비스

Cloudflare Workers 사용

  • Discord: API 라우팅
  • Shopify: 엣지 로직
  • DoorDash: 지역별 처리

🔧 개발 도구 및 프레임워크

프레임워크 지원

| 플랫폼 | 지원 프레임워크 | | -------------- | ------------------------------ | | AWS Lambda | Serverless Framework, SAM, CDK | | Vercel | Next.js, SvelteKit, Nuxt | | Cloudflare | Wrangler, Pages, Remix | | Netlify | Next.js, Gatsby, Nuxt |

로컬 개발

# AWS SAM
sam local start-api
sam local invoke

# Vercel CLI
vercel dev

# Wrangler (Cloudflare)
wrangler dev

# Netlify CLI
netlify dev

💰 비용 최적화 전략

비용 절감 팁

  1. 적절한 메모리 할당: 과도한 메모리는 비용 증가
  2. 콜드 스타트 최소화: 워밍 전략 사용
  3. 캐싱 활용: CDN, 메모리 캐시
  4. 배치 처리: 개별 호출보다 효율적

예상 비용 계산

// 월간 비용 예상
const calculateCost = (platform, requests, avgDuration) => {
  switch (platform) {
    case "lambda":
      const gbSeconds = requests * (avgDuration / 1000) * 0.5; // 512MB
      return requests * 0.0000002 + gbSeconds * 0.0000166667;

    case "vercel":
      return requests > 100000 ? (requests - 100000) * 0.000002 : 0;

    case "cloudflare":
      return requests > 100000 ? (requests - 100000) * 0.0000005 : 0;
  }
};

🎯 선택 가이드

AWS Lambda 선택 시

✅ AWS 서비스 통합 필요
✅ 긴 실행 시간 (최대 15분)
✅ 복잡한 워크플로우
✅ 엔터프라이즈 요구사항
❌ 낮은 레이턴시 필요
❌ 간단한 웹 API

Vercel 선택 시

✅ Next.js 프로젝트
✅ 프론트엔드 중심
✅ 빠른 배포
✅ 프리뷰 배포
❌ 긴 실행 시간
❌ 복잡한 백엔드

Cloudflare Workers 선택 시

✅ 글로벌 저지연
✅ 엣지 컴퓨팅
✅ 높은 동시성
✅ WebSocket 지원
❌ 큰 패키지
❌ 파일 시스템

Netlify Functions 선택 시

✅ JAMstack 프로젝트
✅ 정적 사이트 + API
✅ 간단한 백엔드
✅ Git 기반 배포
❌ 복잡한 로직
❌ 대규모 트래픽


📚 추가 리소스

공식 문서

모범 사례

  • 콜드 스타트 최적화
  • 환경 변수 관리
  • 에러 처리 및 재시도
  • 모니터링 및 로깅

커뮤니티

  • Serverless Stack Discord
  • Vercel Community
  • Cloudflare Developers Discord
  • AWS Serverless Forum

💡 핵심 조언: 서버리스는 "서버가 없다"가 아니라 "서버를 관리하지 않는다"입니다. 간단한 API는 Cloudflare Workers로, Next.js는 Vercel로, 복잡한 워크플로우는 AWS Lambda로 시작하세요. 중요한 것은 벤더 락인을 피하고 필요에 따라 이동할 수 있는 아키텍처를 설계하는 것입니다.

Found this helpful? Share it with others!
Tweet

🔗 Related Content

You might also be interested in these articles

🏢 company

🏢 Vercel 기술 스택 분석

버셀이 프론트엔드 배포의 미래를 만드는 기술 스택 심층 분석 - Next.js, Edge Functions, 그리고 글로벌 엣지 네트워크

28 min read
vercel, nextjs +9
Read more
🏗️ stack

🚀 Modern SaaS MVP Stack

4주 안에 SaaS를 출시하기 위한 검증된 기술 스택 - Next.js, Supabase, Stripe로 빠르게 시작하기

8 min read
nextjs, supabase +13
Read more
🌟 awesome

🌟 Awesome AWS

AWS 개발자와 클라우드 엔지니어를 위한 최고의 도구, 서비스, 리소스 모음

5 min read
awesome, aws +4
Read more
🔁 alternative

🔁 Vercel Alternatives

Vercel을 대체할 수 있는 프론트엔드 및 풀스택 호스팅 플랫폼 비교

9 min read
alternative, vercel +4
Read more

Found this helpful?

Help us improve this content by contributing on GitHub or sharing your feedback with the community.