Use Cases (업데이트: 2026. 6. 2.)

Claude Code로 Vercel Edge Functions 안전하게 쓰기

Claude Code와 Vercel Edge Runtime 실무 가이드. Middleware, 서명 검증, A/B 테스트, 캐시 전처리와 함정을 다룹니다.

Claude Code로 Vercel Edge Functions 안전하게 쓰기

Edge는 빠르다는 느낌만으로 선택하지 않는다

Vercel Edge Functions는 Edge Runtime에서 JavaScript를 실행한다. Edge Runtime은 일반적인 Node.js 프로세스가 아니라 fetch, Request, Response, URL, TextEncoder, Web Crypto 같은 Web API 중심의 작은 실행 환경이다. 쉽게 말하면, 페이지나 API에 도달하기 전에 요청의 URL, Header, Cookie, 작은 body를 보고 즉시 판단하는 가벼운 관문이다.

Claude Code가 유용한 이유는 Edge 작업이 한 파일로 끝나지 않기 때문이다. 국가별 리다이렉트는 middleware.ts, Vercel Header, 로컬과 프로덕션 차이를 함께 본다. A/B 테스트는 Cookie, 요청 Header, 분석 이벤트, 롤백 기준이 필요하다. Webhook은 raw body, 서명, 환경 변수, body size 제한, 내부 API 전달까지 묶인다. 따라서 Claude Code에는 “Edge 함수 하나 만들어줘”보다 “Edge Runtime 제약을 지키면서 코드, 테스트, 로그, 배포 차이를 리뷰해줘”라고 요청하는 것이 안전하다.

2026년 6월 기준 Vercel의 Edge Runtime 공식 문서는 Edge를 만능 가속 장치로 설명하지 않는다. 지원 API, 코드 크기, 실행 시간, region, 일부 워크로드의 Node.js 이전 권장 사항을 함께 다룬다. Next.js의 MiddlewareRoute Handlers 문서도 Web Request와 Response API를 중심으로 설명한다. 실무 결론은 단순하다. 작은 요청 판단은 Edge에 두고, 영속화와 재시도와 복잡한 비즈니스 로직은 운영하기 쉬운 백엔드에 남긴다.

Webhook 재시도와 멱등성이 필요하다면 Claude Code Webhook 구현 가이드를, 성능과 캐시 전체를 보려면 Claude Code 성능 최적화도 함께 읽으면 좋다.

실무에서 자주 쓰는 다섯 가지 유스케이스

Edge에 맞는 작업은 요청 메타데이터나 작은 서명 payload만으로 답을 낼 수 있는 작업이다. 큰 의존성, 긴 DB 트랜잭션, private network 연결, 큰 업로드, 긴 LLM streaming은 Edge에 맞지 않는다.

유스케이스Edge에 맞는 이유Node.js나 백엔드에 남길 것
국가별 리다이렉트x-vercel-ip-country 같은 Header로 입구에서 분기할 수 있다사용자 설정 저장, 가격 정책, 계정 정책
A/B 테스트Cookie로 stable bucket을 만들고 렌더링 전 Header에 전달할 수 있다분석 집계, 실험 판단, 중단 결정
가벼운 인증과 서명 체크Preview 또는 Webhook의 잘못된 요청을 초기에 차단한다세션 발급, 권한 관리, 감사 로그
캐시 전처리URL과 query를 정규화해 cache key를 안정화한다재검증 작업, 재고 갱신, 비싼 계산
Webhook 수신작은 raw body를 검증하고 내부 API로 넘긴다결제 확정, 이메일, 재시도, CRM 업데이트

이 표는 Claude Code 프롬프트로 그대로 써도 좋다. Edge에 둘 일과 백엔드에 둘 일을 분리해두면, 생성된 코드가 Node-only API를 가져오거나 데이터베이스에 직접 연결하거나 secret을 로그에 남기는 실수를 줄일 수 있다.

flowchart LR
  A["User request"] --> B["Next.js Middleware"]
  B --> C{"Small decision"}
  C --> D["Country redirect"]
  C --> E["A/B bucket"]
  C --> F["Light auth"]
  B --> G["Edge Route Handler"]
  G --> H["HMAC signature check"]
  H --> I["Internal API or queue"]

이 그림에서 Edge는 완성된 백엔드가 아니다. Middleware는 요청을 분류하고 안정적인 메타데이터를 붙인다. Route Handler는 작은 Webhook을 검증한다. 영속성, 재시도, 부작용은 내부 API, queue, workflow worker가 담당한다.

복사해서 쓸 수 있는 Next.js Middleware

다음 middleware.ts는 국가별 리다이렉트, A/B bucket, Preview용 간단 인증, 보안 Header를 하나로 묶은 예시다. request.geo 대신 Vercel Header를 쓰는 이유는 Next.js 버전과 로컬 실행 차이에 덜 민감하게 만들기 위해서다. 로컬에서는 x-vercel-ip-country가 보통 없으므로 국가별 분기는 Vercel Preview Deployment에서 확인해야 한다.

// middleware.ts
import { NextRequest, NextResponse } from "next/server";

const PUBLIC_FILE = /\.(?:png|jpg|jpeg|gif|svg|webp|ico|css|js|map|txt)$/i;
const SECRET_HEADER = "x-edge-shared-secret";

export const config = {
  matcher: ["/((?!api/webhooks|_next/static|_next/image|favicon.ico).*)"],
};

function chooseBucket(request: NextRequest): "a" | "b" {
  const current = request.cookies.get("ab_bucket")?.value;
  if (current === "a" || current === "b") return current;

  const random = new Uint8Array(1);
  crypto.getRandomValues(random);
  return random[0] < 128 ? "a" : "b";
}

function localeFromCountry(country: string | null): string | null {
  switch (country?.toUpperCase()) {
    case "JP":
      return "ja";
    case "KR":
      return "ko";
    case "CN":
    case "TW":
    case "HK":
      return "zh";
    case "BR":
      return "pt";
    case "ES":
    case "MX":
      return "es";
    default:
      return null;
  }
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  if (PUBLIC_FILE.test(pathname)) {
    return NextResponse.next();
  }

  if (pathname === "/") {
    const country = request.headers.get("x-vercel-ip-country");
    const locale = localeFromCountry(country);
    if (locale) {
      return NextResponse.redirect(new URL(`/${locale}/`, request.url), 307);
    }
  }

  if (pathname.startsWith("/beta")) {
    const bucket = chooseBucket(request);
    const requestHeaders = new Headers(request.headers);
    requestHeaders.set("x-ab-bucket", bucket);

    const response = NextResponse.next({
      request: { headers: requestHeaders },
    });

    if (!request.cookies.has("ab_bucket")) {
      response.cookies.set("ab_bucket", bucket, {
        maxAge: 60 * 60 * 24 * 30,
        path: "/",
        sameSite: "lax",
        secure: request.nextUrl.protocol === "https:",
      });
    }

    return response;
  }

  if (pathname.startsWith("/preview")) {
    const expected = process.env.EDGE_SHARED_SECRET;
    const actual = request.headers.get(SECRET_HEADER);
    if (!expected || actual !== expected) {
      return NextResponse.redirect(new URL("/login", request.url), 307);
    }
  }

  const response = NextResponse.next();
  response.headers.set("x-content-type-options", "nosniff");
  response.headers.set("referrer-policy", "strict-origin-when-cross-origin");
  return response;
}

예시는 의도적으로 작게 유지했다. A/B 테스트는 bucket만 정하고 승자 판단은 하지 않는다. Preview 인증은 입구를 막는 수준이며 전체 인증 시스템이 아니다. 국가별 리다이렉트도 홈에서만 실행해 리다이렉트 루프 위험을 줄인다.

Edge Route Handler에서 Webhook 서명 검증하기

다음은 app/api/webhooks/provider/route.ts 예시다. HMAC은 공유 secret과 원문 body로 계산하는 서명이며, body가 바뀌지 않았는지 확인하는 용도다. Edge Runtime에서는 Node.js의 crypto.createHmacBuffer를 쓰지 않고 Web Crypto와 TextEncoder만 사용한다.

// app/api/webhooks/provider/route.ts
export const runtime = "edge";
export const preferredRegion = ["iad1", "hnd1"];

const MAX_BODY_BYTES = 256_000;

function hexToBytes(hex: string): Uint8Array {
  const clean = hex.replace(/^sha256=/, "").trim();
  if (!/^[0-9a-f]+$/i.test(clean) || clean.length % 2 !== 0) {
    return new Uint8Array();
  }

  const bytes = new Uint8Array(clean.length / 2);
  for (let index = 0; index < clean.length; index += 2) {
    bytes[index / 2] = Number.parseInt(clean.slice(index, index + 2), 16);
  }
  return bytes;
}

async function hmacSha256(secret: string, payload: string): Promise<Uint8Array> {
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    "raw",
    encoder.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );
  const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
  return new Uint8Array(signature);
}

function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
  if (a.length !== b.length) return false;

  let diff = 0;
  for (let index = 0; index < a.length; index += 1) {
    diff |= a[index] ^ b[index];
  }
  return diff === 0;
}

export async function POST(request: Request) {
  const secret = process.env.WEBHOOK_SECRET;
  const internalOrigin = process.env.INTERNAL_API_ORIGIN;
  const internalToken = process.env.INTERNAL_API_TOKEN;

  if (!secret || !internalOrigin || !internalToken) {
    return Response.json({ error: "server is not configured" }, { status: 500 });
  }

  const contentLength = Number(request.headers.get("content-length") ?? "0");
  if (contentLength > MAX_BODY_BYTES) {
    return Response.json({ error: "payload too large" }, { status: 413 });
  }

  const rawBody = await request.text();
  const rawBodyBytes = new TextEncoder().encode(rawBody);
  if (rawBodyBytes.byteLength > MAX_BODY_BYTES) {
    return Response.json({ error: "payload too large" }, { status: 413 });
  }

  const provided = hexToBytes(request.headers.get("x-signature-sha256") ?? "");
  const expected = await hmacSha256(secret, rawBody);
  if (!constantTimeEqual(provided, expected)) {
    return Response.json({ error: "invalid signature" }, { status: 401 });
  }

  const event = JSON.parse(rawBody) as { id?: string; type?: string };
  if (!event.id || !event.type) {
    return Response.json({ error: "invalid event" }, { status: 400 });
  }

  await fetch(`${internalOrigin}/api/webhook-events`, {
    method: "POST",
    headers: {
      authorization: `Bearer ${internalToken}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({
      id: event.id,
      type: event.type,
      receivedAt: new Date().toISOString(),
    }),
  });

  return Response.json({ ok: true });
}

중요한 것은 순서다. 먼저 크기를 제한하고, raw body를 읽고, 서명을 검증한 다음 JSON을 파싱한다. 검증 후에도 Edge에서 결제 확정이나 이메일 발송까지 처리하지 않는다. 내부 API나 queue로 넘겨서 멱등성과 재시도를 다루는 편이 안전하다.

Claude Code 리뷰 지시와 최소 테스트

Claude Code에는 Edge Runtime 규칙을 명확히 적어준다. 다음 프롬프트는 그대로 재사용할 수 있다.

Review this Next.js Edge implementation.

Scope:
- middleware.ts
- app/api/webhooks/provider/route.ts
- related tests and environment variable names

Check:
- no Node-only APIs such as fs, net, tls, Buffer, or node:crypto in Edge files
- no direct database connection from Edge Runtime
- country redirect does not loop
- A/B bucket is stable by cookie and not written on every request
- webhook verifies the raw body before JSON parsing
- secrets, signatures, cookies, and authorization headers are not logged
- body size and production-only Vercel headers are documented

Return blockers first, then suggested tests.

로컬 테스트에서는 HMAC 생성을 위해 Node.js를 사용해도 된다. 이 코드는 Edge 안에서 실행되는 것이 아니라 테스트 도구이기 때문이다.

npm run lint
npm run build
vercel dev

BODY='{"id":"evt_123","type":"checkout.completed"}'
SIG=$(node -e "const crypto=require('crypto'); const body=process.argv[1]; console.log('sha256='+crypto.createHmac('sha256', process.env.WEBHOOK_SECRET).update(body).digest('hex'))" "$BODY")

curl -i http://localhost:3000/api/webhooks/provider \
  -X POST \
  -H "content-type: application/json" \
  -H "x-signature-sha256: $SIG" \
  --data "$BODY"

curl -I http://localhost:3000/beta
curl -I http://localhost:3000/preview

Preview Deployment에서는 국가 Header, HTTPS Cookie, redirect loop, function log, region 가정을 확인한다. 로컬 테스트는 필수지만 충분하지 않다.

자주 발생하는 함정

첫 번째는 Node.js API를 실수로 쓰는 것이다. fs, Buffer, crypto.createHmac, native module, TCP 기반 DB client는 Edge 파일에 넣지 않는다. 직접 import가 없어도 helper library가 내부에서 가져올 수 있으므로 의존 경로까지 확인해야 한다.

두 번째는 Edge에서 데이터베이스 연결을 너무 많이 잡는 것이다. 코드가 사용자 근처에서 실행돼도 DB가 한 region에 있으면 결국 그곳으로 왕복한다. 연결 수도 늘어난다. HTTP API, managed data API, queue, 또는 DB 가까이에 있는 Node.js Function을 쓰는 편이 낫다.

세 번째는 cold start와 region을 오해하는 것이다. Edge는 입구 판단 지연을 줄일 수 있지만 먼 데이터 소스를 가까이 가져오지는 않는다. preferredRegion은 유용하지만 실제 경로는 로그와 지표로 확인해야 한다.

네 번째는 secret과 로그 유출이다. Webhook body, signature, Cookie, Authorization Header, preview secret은 그대로 출력하지 않는다. 디버깅 로그가 필요하면 masking을 먼저 설계한다.

다섯 번째는 body size와 streaming이다. Edge Route Handler는 작은 서명 요청과 빠른 판단에 적합하다. 큰 파일 업로드, CSV import, 이미지 처리, 긴 LLM stream은 다른 인프라로 보내는 것이 안전하다.

여섯 번째는 로컬과 프로덕션 차이다. vercel dev는 유용하지만 국가 Header, 실제 Edge region, Preview log, secure Cookie 동작을 완전히 재현하지 않는다. 로컬, build, Preview를 나눠 확인한다.

ClaudeCodeLab에서 팀 규칙으로 만들기

개인 프로젝트라면 위 샘플로 충분히 시작할 수 있다. 팀에서는 어떤 파일이 Edge Runtime을 써도 되는지, 어떤 API를 금지할지, 환경 변수 이름을 어떻게 관리할지, Preview 검증을 누가 담당할지가 더 중요하다.

ClaudeCodeLab은 Claude Code 도입, CLAUDE.md, Edge Runtime 리뷰 프롬프트, Webhook 검증 기록, Vercel 배포 체크를 팀 규칙으로 정리하는 작업을 돕는다. 실제 저장소에 맞춰 적용하고 싶다면 Claude Code training and consultation에서 시작할 수 있다. 목적은 절차를 늘리는 것이 아니라 작은 middleware 변경이 전체 사이트 장애로 번지는 것을 막는 것이다.

실제로 시도한 결과

이 구조로 작업해 보니 가장 큰 효과는 속도보다 경계의 명확성이었다. Middleware는 redirect, A/B bucket, Header, 가벼운 차단에만 집중했다. Edge Route Handler는 작은 signed webhook을 검증하고 이벤트를 전달했다. Claude Code에 명확한 리뷰 지시를 주면 Buffer 혼입, raw body 검증 전 JSON parsing, Vercel 전용 Header의 로컬 부재, 과도한 로그를 더 잘 잡아냈다. Edge Functions는 마법 같은 가속 도구가 아니지만, 입구 판단을 작고 테스트 가능하게 유지하면 실무에서 충분히 가치가 있다.

#Claude Code #Vercel #Edge Functions #edge computing #serverless
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.