Claude Code로 Serverless Functions 만들기: Lambda와 Workers 실전
Claude Code로 서버리스 함수를 안전하게 만드는 방법. 요구사항 프롬프트, 플랫폼 선택, 환경 변수, 멱등성, 재시도, 테스트와 배포 체크를 다룹니다.
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 note | cache 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 Lambda | Cloudflare Workers |
|---|---|---|
| 강한 영역 | AWS 통합, 업무 API, 비동기 job | edge HTTP, routing, cache, lightweight API |
| 로컬 개발 | Node.js, SAM, AWS CLI | Wrangler |
| 권한 | IAM role/policy | binding, 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가 적혀 있다 |
| runtime | Lambda Node.js runtime 또는 Workers compatibility_date가 명확하다 |
| local proof | fixture와 node --test가 통과한다 |
| env/secrets | config와 secret을 분리했다 |
| idempotency | retry가 중복 결제나 중복 생성을 만들지 않는다 |
| timeout/retry | 느린 작업은 queue나 durable job으로 넘긴다 |
| observability | JSON 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을 명시하지 않으면 놓치기 쉽습니다. 첫 프롬프트에 로컬 재현, 멱등성, 로그, 삭제 절차를 넣는 것이 품질을 크게 안정시킵니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Obsidian 메모를 CLAUDE.md로 바꾸는 Claude Code 워크플로
Obsidian 작업 메모를 CLAUDE.md 운영 노트로 정리해 Claude Code 세션의 문맥 반복을 줄입니다.
Claude Code Revenue CTA Routing: 글에서 PDF, Gumroad, 상담으로 보내기
독자 의도에 따라 무료 PDF, Gumroad 상품, 상담으로 나누는 Claude Code CTA 설계입니다.
Claude Code 팀 인계 규칙: 리뷰 증거, 권한, 롤백, 수익 경로까지 넘기는 법
Claude Code 작업을 팀에 넘길 때 필요한 증거, 권한 규칙, 롤백, 무료 PDF, Gumroad, 상담 경로 체크리스트.