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

Claude Code로 Serverless Functions 만들기: Lambda와 Workers 실전

Claude Code로 서버리스 함수를 안전하게 만드는 방법. 요구사항 프롬프트, 플랫폼 선택, 환경 변수, 멱등성, 재시도, 테스트와 배포 체크를 다룹니다.

Claude Code로 Serverless Functions 만들기: Lambda와 Workers 실전

Serverless functions는 상시 실행 서버를 직접 관리하지 않고, 이벤트나 HTTP 요청이 들어올 때마다 짧게 코드를 실행하는 방식입니다. Webhook, 작은 JSON API, 이미지 처리의 시작점, CSV import, edge redirect에는 잘 맞지만, timeout, retry, secret, 권한, 로그, 비용을 대충 넘기면 운영에서 바로 문제가 됩니다.

Claude Code는 handler, event fixture, 테스트, 배포 메모, 리뷰 체크리스트를 한 문맥에서 만들 수 있어서 이 작업에 유용합니다. 다만 안전한 흐름은 “AI가 바로 배포”가 아니라 “요구사항 작성 → 런타임 선택 → 로컬 재현 → 환경 변수와 secret 분리 → 멱등성 설계 → 실패 테스트 → 사람이 공개 범위와 비용 검토”입니다.

공식 문서는 작업 전 기준으로 삼으세요: AWS Lambda Documentation, Lambda Node.js 문서, Cloudflare Workers development and testing, Workers get started guide. 관련 글은 AWS Lambda 가이드, Cloudflare Workers 가이드, API 개발 가이드, secret 관리 가이드를 같이 보면 좋습니다.

먼저 사용 사례를 고른다

서버리스 함수는 작업이 짧고, 이벤트 단위로 나뉘며, 재시도되어도 안전하게 처리할 수 있을 때 강합니다.

사용 사례적합한 이유Claude Code에 맡길 부분사람이 확인할 부분
결제/폼 Webhook요청 하나가 이벤트 하나로 끝난다서명 검증 초안, fixture, 실패 응답secret, 중복 이벤트, replay 처리
이미지 resize/CSV import 입구무거운 처리는 storage나 queue로 넘길 수 있다입력 검증, job ID, 구조화 로그파일 크기, timeout, 실패 시 정리
내부 JSON API작은 endpoint에 상시 서버가 필요 없다handler, test, route인증, CORS, 공개 범위, rate limit
edge redirect/cache사용자 가까운 위치에서 빠르게 응답한다Worker route, cache header, rollout notecache purge, 개인정보, SEO 영향
flowchart LR
  A[요구사항 프롬프트] --> B[Lambda 또는 Workers 선택]
  B --> C[로컬 event 재현]
  C --> D[env와 secret 분리]
  D --> E[멱등성과 retry 설계]
  E --> F[test 실행]
  F --> G[dev 배포]
  G --> H[log와 cleanup 확인]

Claude Code에 줄 프롬프트

기능 이름만 주면 그럴듯한 코드는 나오지만 리뷰 가능한 변경이 되지 않습니다. 입력, 출력, 실패 응답, 편집 범위, 사람이 결정할 지점을 같이 줍니다.

Node.js로 최소 서버리스 함수를 만들어 주세요.

목표:
- POST /orders를 처리하고 주문 접수 응답을 반환
- node local-test.mjs로 로컬 확인 가능
- AWS Lambda HTTP API v2 event를 기준으로 작성

요구사항:
- index.mjs, events/create-order.json, local-test.mjs, index.test.mjs를 설명
- idempotency-key header가 없으면 400
- 같은 idempotency-key가 다시 오면 같은 응답 반환
- invalid JSON, invalid input, unsupported route를 구분
- log는 JSON 형식, secret과 개인정보는 출력 금지
- 배포 전 체크리스트 포함

제약:
- 외부 npm package 사용 금지
- production 멱등성 저장소는 DynamoDB, KV, DB unique key 등으로 교체
- IAM, public URL, 과금 리소스는 사람 확인 필요

Lambda와 Workers 선택 기준

AWS 이벤트, IAM, S3, DynamoDB, SQS, EventBridge와 연결할 일이 많으면 AWS Lambda가 자연스럽습니다. HTTP edge 처리, redirect, cache, 간단한 인증, KV/D1/R2 조합이면 Cloudflare Workers가 잘 맞습니다. Vercel Functions는 Next.js 앱 안의 API나 OG 이미지 생성에 편하지만, 여기서는 기본 개념이 명확한 Lambda와 Workers에 집중합니다.

기준AWS LambdaCloudflare Workers
강한 영역AWS 통합, 업무 API, 비동기 jobedge HTTP, routing, cache, lightweight API
로컬 개발Node.js, SAM, AWS CLIWrangler
권한IAM role/policybinding, secret, account permission
주의점과도한 IAM, VPC/NAT 비용, 로그량binding 차이, 실행 제한, KV consistency

로컬에서 먼저 도는 Lambda 예제

다음 코드는 외부 package 없이 실행됩니다. HTTP API event를 받고, idempotency-key를 요구하며, 같은 key가 반복되면 같은 결과를 돌려줍니다.

// index.mjs
import crypto from "node:crypto";

const localIdempotencyStore = new Map();

function json(statusCode, body) {
  return {
    statusCode,
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  };
}

function readHeader(headers = {}, name) {
  const target = name.toLowerCase();
  const found = Object.entries(headers).find(([key]) => key.toLowerCase() === target);
  return found?.[1];
}

function parseBody(event) {
  if (!event.body) return {};
  const raw = event.isBase64Encoded
    ? Buffer.from(event.body, "base64").toString("utf8")
    : event.body;
  return JSON.parse(raw);
}

export async function handler(event = {}, context = {}) {
  const method = event.requestContext?.http?.method ?? event.httpMethod ?? "GET";
  const path = event.rawPath ?? event.path ?? "/";
  const requestId = context.awsRequestId ?? crypto.randomUUID();

  console.log(JSON.stringify({ level: "info", message: "request.start", requestId, method, path }));

  if (method !== "POST" || path !== "/orders") {
    return json(404, { error: "not_found" });
  }

  const idempotencyKey = readHeader(event.headers, "idempotency-key");
  if (!idempotencyKey) {
    return json(400, { error: "idempotency_key_required" });
  }

  if (localIdempotencyStore.has(idempotencyKey)) {
    return json(200, { ...localIdempotencyStore.get(idempotencyKey), replay: true });
  }

  let body;
  try {
    body = parseBody(event);
  } catch {
    return json(400, { error: "invalid_json" });
  }

  if (!Number.isFinite(body.amount) || body.amount <= 0 || typeof body.currency !== "string") {
    return json(400, { error: "invalid_order" });
  }

  const accepted = {
    orderId: crypto.randomUUID(),
    status: "accepted",
    amount: body.amount,
    currency: body.currency,
  };

  localIdempotencyStore.set(idempotencyKey, accepted);
  console.log(JSON.stringify({ level: "info", message: "order.accepted", requestId, orderId: accepted.orderId }));

  return json(202, accepted);
}
{
  "version": "2.0",
  "routeKey": "POST /orders",
  "rawPath": "/orders",
  "headers": {
    "content-type": "application/json",
    "idempotency-key": "demo-key-001"
  },
  "requestContext": {
    "http": {
      "method": "POST",
      "path": "/orders"
    }
  },
  "body": "{\"amount\":3200,\"currency\":\"KRW\"}",
  "isBase64Encoded": false
}
// local-test.mjs
import { readFile } from "node:fs/promises";
import { handler } from "./index.mjs";

const eventPath = process.argv[2] ?? "events/create-order.json";
const event = JSON.parse(await readFile(eventPath, "utf8"));

const first = await handler(event, { awsRequestId: "local-001" });
const second = await handler(event, { awsRequestId: "local-002" });

console.log("first:", first.statusCode, first.body);
console.log("second:", second.statusCode, second.body);
node local-test.mjs events/create-order.json

Map은 로컬 demo일 뿐입니다. production에서는 DynamoDB conditional write, database unique key, Cloudflare KV/D1 같은 durable store를 사용해야 합니다.

실패 테스트

// index.test.mjs
import crypto from "node:crypto";
import test from "node:test";
import assert from "node:assert/strict";
import { handler } from "./index.mjs";

function event(overrides = {}) {
  return {
    rawPath: "/orders",
    headers: { "idempotency-key": crypto.randomUUID() },
    requestContext: { http: { method: "POST" } },
    body: JSON.stringify({ amount: 1200, currency: "KRW" }),
    isBase64Encoded: false,
    ...overrides,
  };
}

test("requires idempotency-key", async () => {
  const result = await handler(event({ headers: {} }), {});
  assert.equal(result.statusCode, 400);
});

test("accepts a valid order", async () => {
  const result = await handler(event(), {});
  assert.equal(result.statusCode, 202);
  assert.equal(JSON.parse(result.body).status, "accepted");
});

test("rejects invalid JSON", async () => {
  const result = await handler(event({ body: "not-json" }), {});
  assert.equal(result.statusCode, 400);
});
node --test index.test.mjs

Workers 버전

Workers는 fetch(request, env)가 entry point입니다. KV binding에 멱등성 결과를 저장하고, webhook secret은 Worker secret으로 둡니다.

// src/worker.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    if (request.method !== "POST" || url.pathname !== "/orders") {
      return Response.json({ error: "not_found" }, { status: 404 });
    }

    if (request.headers.get("x-webhook-secret") !== env.WEBHOOK_SECRET) {
      return Response.json({ error: "unauthorized" }, { status: 401 });
    }

    const idempotencyKey = request.headers.get("idempotency-key");
    if (!idempotencyKey) {
      return Response.json({ error: "idempotency_key_required" }, { status: 400 });
    }

    const existing = await env.IDEMPOTENCY_KV.get(idempotencyKey, "json");
    if (existing) {
      return Response.json({ ...existing, replay: true });
    }

    const body = await request.json();
    if (!Number.isFinite(body.amount) || typeof body.currency !== "string") {
      return Response.json({ error: "invalid_order" }, { status: 400 });
    }

    const accepted = {
      orderId: crypto.randomUUID(),
      status: "accepted",
      amount: body.amount,
      currency: body.currency,
    };

    await env.IDEMPOTENCY_KV.put(idempotencyKey, JSON.stringify(accepted), {
      expirationTtl: 86400,
    });

    return Response.json(accepted, { status: 202 });
  },
};
npm create cloudflare@latest serverless-orders-worker
cd serverless-orders-worker
npx wrangler kv namespace create IDEMPOTENCY_KV
npx wrangler secret put WEBHOOK_SECRET
npx wrangler dev

흔한 함정과 배포 체크

첫 번째 함정은 exactly-once 실행을 믿는 것입니다. Webhook provider, queue, async event, browser 모두 retry할 수 있습니다. 두 번째는 secret을 env에 넣고 log에 출력하는 것입니다. 세 번째는 Resource: "*"처럼 권한을 넓게 여는 것입니다. 네 번째는 public endpoint를 만들고 auth, CORS, rate limit, log retention, cleanup date를 정하지 않는 것입니다.

체크확인할 내용
요구사항input, output, owner, failure response가 적혀 있다
runtimeLambda Node.js runtime 또는 Workers compatibility_date가 명확하다
local prooffixture와 node --test가 통과한다
env/secretsconfig와 secret을 분리했다
idempotencyretry가 중복 결제나 중복 생성을 만들지 않는다
timeout/retry느린 작업은 queue나 durable job으로 넘긴다
observabilityJSON log, error rate, alert, retention을 정했다
cleanup삭제 command 또는 dashboard 정리 절차가 있다
zip function.zip index.mjs
aws lambda update-function-code \
  --function-name serverless-orders-dev \
  --zip-file fileb://function.zip

npx wrangler deploy

마지막으로 Claude Code에 이렇게 리뷰를 요청합니다.

이 serverless function을 공개 전 리뷰해 주세요.
blocking / non-blocking / human confirmation으로 나누고,
idempotency, timeout/retry, secret 노출, IAM 또는 binding 과다 권한,
개인정보 로그, 로컬 테스트 재현성, cleanup, 공식 링크와 내부 링크를 확인해 주세요.

ClaudeCodeLab은 이런 패턴을 Claude Code products and templates로 정리하고 있습니다. 팀에서 AWS 권한, CLAUDE.md, 리뷰 프롬프트, 배포 승인 규칙까지 설계해야 한다면 Claude Code training and consultation을 활용하세요.

실제로 이 흐름을 시험했을 때 가장 효과가 큰 것은 event fixture를 먼저 만든 일이었습니다. Claude Code는 빠르지만, retry, secret, cleanup을 명시하지 않으면 놓치기 쉽습니다. 첫 프롬프트에 로컬 재현, 멱등성, 로그, 삭제 절차를 넣는 것이 품질을 크게 안정시킵니다.

#Claude Code #serverless functions #AWS Lambda #Cloudflare Workers #Vercel
무료

무료 PDF: Claude Code 치트시트

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

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

Masa

작성자 소개

Masa

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