---
id: web-cors-practical-guide
title: CORS 실전 가이드
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [web, cors, security, vibe-coding]
tech_stack: { language: "Any backend", applicable_to: ["Web", "API"] }
applied_in: []
aliases: [preflight, OPTIONS, Access-Control-Allow-Origin]
---

# CORS 실전 가이드

> CORS 는 **브라우저의 안전장치**. 서버는 "누가 호출해도 되는지" 알리고, 브라우저는 그 정책을 강제. 핵심은 (1) preflight 가 언제 일어나는지, (2) credentials 가 들어갈 때 정책이 더 까다로워진다는 것.

## 📖 핵심 개념
- **Same-origin** (scheme + host + port 일치) = CORS 적용 안 됨
- **Cross-origin** = 응답 헤더 (`Access-Control-Allow-*`) 필요
- **Simple request** (GET/HEAD/POST + 표준 헤더만) = preflight 없이 바로
- **Preflight** = OPTIONS 요청으로 사전 체크 (커스텀 헤더, PUT/DELETE 등)
- **Credentials** (cookie, Authorization) = `withCredentials: true` 또는 `credentials: 'include'`

## 💻 코드 패턴

### 서버 — 안전한 CORS (Express 예시)
```ts
import cors from 'cors';
const allowed = ['https://app.example.com', 'https://admin.example.com'];

app.use(cors({
  origin: (origin, cb) => {
    if (!origin) return cb(null, true); // server-to-server / curl
    if (allowed.includes(origin)) return cb(null, true);
    cb(new Error('Not allowed by CORS'));
  },
  credentials: true,                       // cookie 허용
  methods: ['GET','POST','PUT','PATCH','DELETE'],
  allowedHeaders: ['Content-Type','Authorization','X-Request-Id'],
  exposedHeaders: ['X-Request-Id','ETag'],
  maxAge: 86400,                           // preflight 캐시 1일
}));
```

### 클라이언트 — credentials 포함
```ts
fetch('https://api.example.com/me', {
  credentials: 'include', // cookie 보내기
  headers: { 'Authorization': `Bearer ${token}` }
});
```

## 🤔 의사결정 기준
| 상황 | 설정 |
|---|---|
| 공개 API (인증 없음, public CDN) | `Access-Control-Allow-Origin: *` |
| 인증 사용 (cookie/Authorization) | 명시적 origin 화이트리스트 + `credentials: true` |
| 여러 클라이언트 도메인 | 화이트리스트 함수로 동적 검사 |
| dev 환경 | 환경변수로 `localhost:*` 추가 |
| WebSocket | CORS 안 적용. 별도 origin 검증. |
| 서버-서버 통신 | CORS 무관 (브라우저 기능) |

## ❌ 안티패턴
- **`Allow-Origin: *` + credentials 사용**: 브라우저가 거부. 명시적 origin 필수.
- **`Allow-Origin` 을 요청의 `Origin` 헤더 그대로 echo**: 화이트리스트 없이 echo = 모든 origin 허용. 사고.
- **OPTIONS 핸들러 누락**: preflight 가 401/404 → 진짜 요청 못 감.
- **maxAge 0 또는 미설정**: 모든 요청마다 preflight. latency 부하.
- **에러 응답에 CORS 헤더 누락**: 브라우저가 응답 못 읽어 디버깅 지옥. 모든 응답 (성공/실패) 에 일관 적용.
- **`X-` 커스텀 헤더 추가했는데 allowedHeaders 안 넣음**: preflight 실패.
- **응답에 CORS 헤더 두 번** (서버 + 리버스 프록시 둘 다): 브라우저가 거부. 한 곳에서만.

## 🤖 LLM 활용 힌트
- "credentials 포함 요청이면 `*` 금지, 명시 origin" 강조.
- Express/Fastify/Nginx 등 어디서 CORS 셋팅하는지 명확히 — 한 곳에서만.

## 🔗 관련 문서
- [[Web_HTTP_Cache_Headers]]
- [[Web_JWT_Patterns]]
