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

Claude Code로 CORS 설정 완전 가이드: 안전한 Cross-Origin API

Claude Code로 CORS를 안전하게 설정합니다. preflight, credentials, origin allowlist, 테스트와 리뷰 프롬프트 포함.

Claude Code로 CORS 설정 완전 가이드: 안전한 Cross-Origin API

Claude Code로 CORS를 제대로 설정하기

프론트엔드는 localhost:3000, API는 localhost:8787에서 실행하는 것만으로도 브라우저는 CORS 오류를 낼 수 있습니다. 이때 Access-Control-Allow-Origin: *를 붙이면 된다고 생각하기 쉽지만, API가 Cookie, Authorization header, 관리자 화면을 다루는 순간 위험한 설정이 됩니다.

CORS(Cross-Origin Resource Sharing)는 서버가 어떤 다른 origin의 브라우저 코드에 응답 읽기를 허용할지 정하는 메커니즘입니다. origin은 scheme, host, port의 조합입니다. https://app.example.com, https://api.example.com, http://localhost:3000, http://localhost:5173은 모두 서로 다른 origin입니다.

이 글은 Claude Code가 생성한 CORS 코드를 사람이 검토할 수 있도록 단계별로 나눕니다. Express, Fastify, Cloudflare Workers, Next.js Route Handler의 복사해서 실행 가능한 예제와 preflight, credentials, origin allowlist, 테스트 명령어, Claude Code 리뷰 요청 템플릿을 제공합니다.

가장 중요한 전제는 CORS가 인증이 아니라는 점입니다. CORS는 브라우저 JavaScript가 cross-origin 응답을 읽을 수 있는지를 제어할 뿐입니다. curl, 서버 간 호출, 권한 없는 사용자를 막지 못합니다. 인증, 인가, CSRF, rate limit, 보안 헤더는 별도로 설계해야 합니다.

sequenceDiagram
  participant Browser as Browser
  participant API as API server
  Browser->>API: OPTIONS /api/messages<br/>Origin + Access-Control-Request-*
  API-->>Browser: 204 + Access-Control-Allow-*
  Browser->>API: POST /api/messages<br/>Cookie or Authorization
  API-->>Browser: 200 + Access-Control-Allow-Origin

코드 작성 전에 정할 것

Claude Code에 CORS 코드를 요청하기 전에 아래 네 가지를 먼저 정합니다. 요구사항이 모호하면 데모에는 편하지만 운영에는 위험한 전체 허용 코드가 나오기 쉽습니다.

결정 항목예시주의점
허용할 originhttps://app.example.com, https://admin.example.compath와 trailing slash는 넣지 않음
credentials 사용 여부Cookie, Authorization headerCookie는 SameSite=None; Secure도 확인
허용할 methodGET,POST,PUT,PATCH,DELETE,OPTIONS실제 사용하는 method만 허용
허용할 headerContent-Type,Authorization,X-Request-IDpreflight 요청과 일치해야 함

preflight는 실제 요청 전에 브라우저가 보내는 권한 확인 요청입니다. JSON POST, Authorization, PUT, DELETE, custom header는 보통 먼저 OPTIONS를 보냅니다. 응답에 맞는 Access-Control-Allow-MethodsAccess-Control-Allow-Headers가 없으면 실제 요청은 보내지 않습니다.

Express 설정

다음 예제는 Node.js 20 이상을 가정합니다. Express 공식 cors middleware는 origin에 함수를 받을 수 있으므로 요청마다 allowlist를 확인할 수 있습니다. credentials를 사용하기 때문에 허용된 origin만 반사하고 credentials: true를 설정합니다.

npm init -y
npm install express cors
node server.mjs
// server.mjs
import express from "express";
import cors from "cors";

const app = express();

const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
  "http://localhost:5173",
]);

function isAllowedOrigin(origin) {
  if (!origin) return true;
  if (allowedOrigins.has(origin)) return true;
  return process.env.NODE_ENV !== "production" && /^http:\/\/localhost:\d+$/.test(origin);
}

const corsOptions = {
  origin(origin, callback) {
    callback(null, isAllowedOrigin(origin));
  },
  credentials: true,
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
  allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
  exposedHeaders: ["X-Request-ID"],
  maxAge: 86400,
  optionsSuccessStatus: 204,
};

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (origin && !isAllowedOrigin(origin)) {
    return res.status(403).json({ error: "Origin not allowed" });
  }
  next();
});

app.use(cors(corsOptions));
app.use(express.json());

app.get("/api/health", (_req, res) => {
  res.setHeader("X-Request-ID", crypto.randomUUID());
  res.json({ ok: true });
});

app.post("/api/messages", (req, res) => {
  res.setHeader("X-Request-ID", crypto.randomUUID());
  res.json({ ok: true, received: req.body });
});

app.listen(8787, () => {
  console.log("API listening on http://localhost:8787");
});

운영에서는 NODE_ENV=production으로 실행하고 allowedOrigins에는 실제 서비스 도메인만 남기세요. Origin header가 없는 요청은 브라우저 CORS 요청이 아니므로 여기서는 허용하지만, API key, JWT, 사용자 권한 검사는 별도 middleware에서 처리해야 합니다.

Fastify 설정

Fastify에서는 @fastify/cors를 사용합니다. 공식 README는 origin에 boolean, string, array, RegExp, function을 허용하지만, Set 기반 완전 일치가 가장 검토하기 쉽습니다. 넓은 정규식은 꼭 필요한 경우가 아니면 피하세요.

npm init -y
npm install fastify @fastify/cors
node server.mjs
// server.mjs
import Fastify from "fastify";
import cors from "@fastify/cors";

const app = Fastify({ logger: true });

const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
  "http://localhost:5173",
]);

function isAllowedOrigin(origin) {
  if (!origin) return true;
  if (allowedOrigins.has(origin)) return true;
  return process.env.NODE_ENV !== "production" && /^http:\/\/localhost:\d+$/.test(origin);
}

app.addHook("onRequest", async (request, reply) => {
  const origin = request.headers.origin;
  if (origin && !isAllowedOrigin(origin)) {
    return reply.code(403).send({ error: "Origin not allowed" });
  }
});

await app.register(cors, {
  origin(origin, callback) {
    callback(null, isAllowedOrigin(origin));
  },
  credentials: true,
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
  allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
  exposedHeaders: ["X-Request-ID"],
  maxAge: 86400,
  strictPreflight: true,
});

app.get("/api/health", async () => ({ ok: true }));

app.post("/api/messages", async (request) => {
  return { ok: true, received: request.body };
});

await app.listen({ port: 8787, host: "0.0.0.0" });

Fastify에서는 plugin과 hook의 등록 순서가 중요합니다. 인증 hook이 CORS plugin보다 먼저 OPTIONS를 거부하면 브라우저는 실제 요청을 보내지 않습니다. Claude Code에 리뷰를 맡길 때는 header뿐 아니라 plugin 등록 순서도 보게 하세요.

Cloudflare Workers 설정

Cloudflare Workers는 표준 Fetch API를 직접 사용합니다. OPTIONS를 명시적으로 처리하고, 성공 응답과 에러 응답 모두에 CORS header를 붙입니다. origin에 따라 응답이 달라진다면 Vary: Origin도 추가하세요.

// src/index.ts
const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
]);

function getCorsHeaders(request: Request): HeadersInit | null {
  const origin = request.headers.get("Origin");
  if (!origin) return {};
  if (!allowedOrigins.has(origin)) return null;

  return {
    "Access-Control-Allow-Origin": origin,
    "Access-Control-Allow-Credentials": "true",
    "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Request-ID",
    "Access-Control-Max-Age": "86400",
    "Vary": "Origin",
  };
}

export default {
  async fetch(request: Request): Promise<Response> {
    const corsHeaders = getCorsHeaders(request);
    if (corsHeaders === null) {
      return Response.json({ error: "Origin not allowed" }, { status: 403 });
    }

    if (request.method === "OPTIONS") {
      return new Response(null, { status: 204, headers: corsHeaders });
    }

    const url = new URL(request.url);
    if (url.pathname === "/api/messages" && request.method === "POST") {
      const body = await request.json().catch(() => ({}));
      return Response.json({ ok: true, received: body }, { headers: corsHeaders });
    }

    return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
  },
};

Workers에서 흔한 실수는 정상 응답에만 header를 붙이는 것입니다. OPTIONS, 401, 403, 500에 CORS header가 없으면 DevTools에는 CORS 실패만 보이고 실제 애플리케이션 오류가 숨습니다.

Next.js Route Handler 설정

App Router의 app/api/.../route.ts는 Web 표준 RequestResponse를 사용합니다. Next.js 문서는 응답에 CORS header를 붙이는 예를 제공하지만, credentials를 쓰는 API라면 *가 아니라 allowlist를 사용하세요.

// app/api/messages/route.ts
const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
]);

function getCorsHeaders(request: Request): HeadersInit | null {
  const origin = request.headers.get("Origin");
  if (!origin) return {};
  if (!allowedOrigins.has(origin)) return null;

  return {
    "Access-Control-Allow-Origin": origin,
    "Access-Control-Allow-Credentials": "true",
    "Access-Control-Allow-Methods": "POST,OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Request-ID",
    "Access-Control-Max-Age": "86400",
    "Vary": "Origin",
  };
}

export async function OPTIONS(request: Request) {
  const headers = getCorsHeaders(request);
  if (headers === null) {
    return Response.json({ error: "Origin not allowed" }, { status: 403 });
  }
  return new Response(null, { status: 204, headers });
}

export async function POST(request: Request) {
  const headers = getCorsHeaders(request);
  if (headers === null) {
    return Response.json({ error: "Origin not allowed" }, { status: 403 });
  }

  const body = await request.json().catch(() => ({}));
  return Response.json({ ok: true, received: body }, { headers });
}

next.config.jsheaders()는 정적이고 공개적인 API header에 적합합니다. origin별로 판단해야 하는 인증 API는 Route Handler 안에서 처리하는 편이 더 명확합니다.

테스트 명령어

curl로 preflight와 실제 요청을 분리해서 확인하세요. Access-Control-Allow-Origin이 요청의 Origin과 정확히 일치하는지, Access-Control-Allow-Credentials: true가 허용 origin에만 반환되는지 봅니다.

curl -i -X OPTIONS http://localhost:8787/api/messages \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization"

curl -i -X POST http://localhost:8787/api/messages \
  -H "Origin: http://localhost:3000" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer dev-token" \
  --data '{"text":"hello"}'

curl -i -X OPTIONS http://localhost:8787/api/messages \
  -H "Origin: https://evil.example" \
  -H "Access-Control-Request-Method: POST"

브라우저의 credentials 확인은 다음처럼 할 수 있습니다. credentials: "include"를 쓰는 순간 wildcard CORS 응답은 브라우저에서 거부됩니다.

await fetch("http://localhost:8787/api/messages", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer dev-token",
  },
  body: JSON.stringify({ text: "hello" }),
});

실제 유스케이스

첫 번째는 SPA와 API를 다른 도메인에 배포하는 경우입니다. React 앱이 https://app.example.com, API가 https://api.example.com이라면 명시적인 allowlist가 필요합니다. 로그인 Cookie가 있다면 credentials, Cookie 속성, CSRF를 함께 확인하세요.

두 번째는 관리자 프론트엔드를 별도로 허용하는 경우입니다. https://admin.example.com을 allowlist에 추가하되, CORS를 관리자 권한 체크로 착각하면 안 됩니다. 권한 검사는 API 코드에서 수행해야 합니다.

세 번째는 Cloudflare Worker를 BFF나 가벼운 proxy로 쓰는 경우입니다. 브라우저는 Worker에 요청하고, Worker가 upstream API를 호출합니다. 브라우저로 돌아가는 Worker 응답에는 여전히 올바른 CORS header가 필요합니다.

네 번째는 공개 읽기 API입니다. Cookie도 Authorization도 비공개 데이터도 없다면 Access-Control-Allow-Origin: *가 허용될 수 있습니다. 나중에 인증을 추가할 가능성이 있다면 처음부터 allowlist로 설계하는 편이 안전합니다.

구체적인 함정

함정결과수정
*credentials: true를 함께 사용브라우저가 응답을 차단명시적인 origin 반환
https://app.example.com/ 등록trailing slash 때문에 불일치https://app.example.com만 저장
localhost만 허용포트가 다르면 실패http://localhost:3000처럼 포트까지 작성
OPTIONS에도 인증 요구preflight가 401/403에서 중단인증 전에 preflight 처리
에러 응답에 CORS 누락실제 오류가 DevTools에 안 보임4xx/5xx에도 header 추가
CDN이 origin별 header를 캐시다른 origin header가 섞임Vary: Origin 추가
CORS를 권한으로 착각비브라우저 클라이언트는 계속 호출인증, 인가, CSRF 별도 구현

MDN은 credentials가 포함된 CORS 요청에서 Access-Control-Allow-Origin: *를 사용할 수 없다고 설명합니다. Claude Code가 이 조합을 생성하면 버그로 보고 수정해야 합니다.

Claude Code 리뷰 요청 템플릿

Review this repository's CORS configuration.
Check:
- No Access-Control-Allow-Origin: * when credentials are enabled
- Allowlist uses exact scheme/host/port matching
- OPTIONS preflight runs before authentication middleware
- 4xx/5xx responses include the required CORS headers
- Vary: Origin is present when responses vary by origin
If changes are needed, propose the smallest safe diff.
Diagnose this CORS error by cause.
Browser error:
<paste the full DevTools Console message>

curl preflight:
<paste curl -i -X OPTIONS output>

Expected origin:
https://app.example.com

Read the relevant API files and return reproduction steps, root cause, fix, and tests.
Review the Express/Fastify/Next.js/Workers CORS implementation as a security reviewer.
Focus on:
- Whether request origins are blindly reflected
- Whether localhost remains enabled in production
- Whether Authorization is allowed without proper authorization checks
- Whether cookie flows mention SameSite=None; Secure and CSRF protection
- Whether test commands separate preflight and the real request
Group findings as Critical, Must fix, and Improvement.

공식 자료와 관련 글

기준 문서는 MDN CORS guide를 보세요. 구현은 Express cors middleware, @fastify/cors, Cloudflare Workers CORS examples, Next.js Route Handlers를 참고하면 됩니다. Claude Code workflow는 Claude Code commands가 도움이 됩니다.

API 개발 가이드, 웹 보안 헤더 가이드, Cloudflare Workers 가이드, 코드 리뷰 워크플로 체크리스트도 함께 읽으면 CORS만 따로 떨어진 설정으로 남지 않습니다.

다음 단계

예시 도메인을 자신의 도메인으로 바꾼 뒤 위 리뷰 템플릿을 저장소에 적용하고, Claude Code 보안 베스트 프랙티스로 Cookie, CSRF, 인가, 보안 헤더까지 같이 점검하세요. 고객 프로젝트나 내부 플랫폼에서는 이 체크리스트가 리뷰 산출물이 되어 유료 구현 지원이나 재사용 템플릿 도입 제안으로 이어지기 쉽습니다.

실제로 시도한 결과

Masa의 로컬 테스트에서는 Express와 Fastify 예제를 localhost:8787에서 실행했고, Origin: http://localhost:3000의 preflight와 POST는 성공했으며 https://evil.example은 403을 반환했습니다. 가장 놓치기 쉬운 부분은 에러 응답의 CORS header와 Workers의 명시적인 OPTIONS 처리였습니다. 먼저 allowlist를 구현하고 curl로 확인한 뒤 Claude Code에 wildcard와 credentials 조합, 필요한 Vary: Origin, production에 localhost가 남지 않는지를 확인시키는 흐름이 가장 안정적이었습니다.

#Claude Code #CORS #security #API #web development
무료

무료 PDF: Claude Code 치트시트

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

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

Masa

작성자 소개

Masa

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