(CS/컴퓨터공학) CORS 이해하기 - 동작 원리, 프리플라이트, 캐싱
✨ 개요
CORS 완전 정복 - 동작 원리, 프리플라이트(OPTIONS), 캐싱, 그리고 흔한 오해 12가지
CORS(Cross-Origin Resource Sharing)는 브라우저가 “다른 오리진의 리소스에 접근해도 되는지”를 서버 응답 헤더로 판단하는 메커니즘입니다.
핵심은 한 줄: 서버가 허용하면 OK, 안 하면 브라우저가 막는다. (클라이언트 코드로 “해제”할 수 없음)
—
1. 배경: SOP와 CORS
- SOP(Same-Origin Policy): 프로토콜·호스트·포트가 모두 같아야 JS가 응답 내용을 읽을 수 있음.
- CORS: SOP의 예외를 서버 응답 헤더로 선언하는 표준.
- 대상: 브라우저. (서버↔서버, cURL/Postman은 CORS에 걸리지 않음)
2 CORS의 요청/응답 흐름 (개념도)
[브라우저 JS] ---- cross-origin 요청 ----> [서버]
↑ |
| <--- CORS 응답 헤더(허용/거부) ----|
허용이면 JS가 응답 본문을 읽을 수 있음
- 브라우저는 응답에 CORS 허용 헤더가 없으면 네트워크는 성공이어도 JS에 “Blocked by CORS policy” 에러를 던집니다.
3 ‘단순 요청’ vs ‘프리플라이트(OPTIONS)’
3.1 단순(Simple) 요청
아래 모두 만족하면 사전 요청(OPTIONS) 없이 바로 보냅니다.
- 메서드:
GET,HEAD,POST - 헤더: CORS safelisted 헤더(
Accept,Accept-Language,Content-Language,Content-Type) - Content-Type 값:
text/plain,application/x-www-form-urlencoded,multipart/form-data - 본인 인증정보: 포함할 수 있으나(쿠키/Authorization) → 응답에서 별도 규칙 필요(§5)
3.2 프리플라이트(Preflight) 요청
- 위 조건을 벗어나면(예:
PUT/DELETE, 커스텀 헤더,application/json등) - 브라우저가 먼저
OPTIONS를 보내 서버 허용 여부를 확인하고, 허용 시 본 요청 전송.
프리플라이트 예
요청 (브라우저 → 서버)
OPTIONS /api/orders
Origin: https://shop.example
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization, X-Trace-Id
응답 (서버 → 브라우저)
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://shop.example
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Authorization, X-Trace-Id
Access-Control-Max-Age: 600
Vary: Origin
4. 핵심 응답 헤더 6종
| 헤더 | 의미/주의 |
|---|---|
| Access-Control-Allow-Origin | 허용 오리진. * 또는 특정 오리진(예: https://app.example). 자격 증명(쿠키/Authorization)과 함께라면 * 금지 |
| Access-Control-Allow-Credentials | true면 쿠키/Authorization 헤더를 포함한 요청 허용. 이때 Allow-Origin은 와일드카드 불가 |
| Access-Control-Expose-Headers | JS에서 읽을 수 있는 응답 헤더 화이트리스트 확장 |
| Access-Control-Allow-Methods | 프리플라이트 응답에서 허용 메서드 선언 |
| Access-Control-Allow-Headers | 프리플라이트 응답에서 허용 요청 헤더(커스텀/Authorization 등) |
| Access-Control-Max-Age | 프리플라이트 결과 캐싱 시간(초) |
Vary: Origin 을 꼭 넣어 CDN/프록시가 오리진별로 캐시 분리하도록 만드세요.
5. 인증(쿠키/토큰)과 CORS
브라우저 측(Fetch/Ajax)
// 쿠키/인증 포함
fetch("https://api.example.com/me", {
credentials: "include", // 또는 axios { withCredentials: true }
headers: { "Authorization": "Bearer <token>" }
});
서버응답
Access-Control-Allow-Origin: https://app.example.com # * 불가
Access-Control-Allow-Credentials: true
Vary: Origin
- Allow-Credentials: true + Allow-Origin: * → 사양 위반(차단).
- 보안상 필요 없으면 자격증명 없이 설계하는 게 단순하고 안전함.
6. 캐싱/성능 팁
- 프리플라이트 캐싱: Access-Control-Max-Age: 600 등으로 OPTIONS 트래픽 감소.
- CDN/Reverse Proxy가 앞단에 있으면 CORS 헤더가 최종 응답까지 전달되는지 확인.
- 멀티 오리진 허용 시 반사형(Reflect) 전략 사용: 요청 Origin 검사 후 동일 값으로 세팅 + Vary: Origin.
7. 환경별 설정 예시
7.1 Node/Express
import cors from "cors";
app.use(cors({
origin: (origin, cb) => {
const allow = ["https://app.example.com", "https://admin.example.com"];
cb(null, allow.includes(origin ?? "") ? origin : false);
},
credentials: true, // 쿠키/인증 허용
allowedHeaders: ["Content-Type", "Authorization", "X-Trace-Id"],
methods: ["GET","POST","PUT","DELETE","OPTIONS"],
maxAge: 600
}));
7.2 Spring boot
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET","POST","PUT","DELETE")
.allowedHeaders("Authorization","Content-Type","X-Trace-Id")
.allowCredentials(true)
.maxAge(600);
}
};
}
7.3 NGINX(리버스 프록시)
location /api/ {
if ($http_origin ~* ^https://(app|admin)\.example\.com$) {
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Vary Origin always;
}
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Methods "GET,POST,PUT,DELETE,OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization,Content-Type,X-Trace-Id" always;
add_header Access-Control-Max-Age 600 always;
return 204;
}
proxy_pass http://api_upstream;
}
8. 자주 겪는 문제와 해결
| 증상 | 원인/해결 |
|---|---|
| “Blocked by CORS policy” | 서버 응답에 허용 헤더가 없음/부족. 서버에서 설정해야 함(클라이언트 설정 X). |
| 프리플라이트만 4xx/5xx | OPTIONS 핸들러 누락/차단. 라우팅·WAF·리버스 프록시에서 OPTIONS 허용. |
| 쿠키가 안 간다 | credentials: 'include' + Allow-Credentials: true + 정확한 Allow-Origin 필요. * 불가. |
| CDN 캐시 이상 | Vary: Origin 빠짐 → 다른 오리진의 헤더가 섞여 전달. |
| 리다이렉트 후 에러 | 중간 응답/최종 응답 모두 CORS 헤더가 필요. |
no-cors로 보냈더니 응답이 빈 값 |
Opaque response: 보안상 본문 접근 불가(읽을 수 없음). CORS 허용으로 바꿔야 함. |
9. 오해와 진실 12가지
- “CORS는 보안 기능이다(서버 보호).” → ❌ 브라우저의 클라이언트 보호 장치. 서버 보호는 인증/인가/레이트리밋/WAF로.
- “클라이언트에서 CORS 끄면 된다.” → ❌ 개발자 도구 우회는 로컬만. 사용자 브라우저는 그대로. 정답은 서버 설정.
- “*로 열면 다 된다.” → ◑ 자격증명 필요하면 불가. 오리진을 구체적으로 써야 함.
- “프리플라이트는 느려서 무조건 나쁘다.” → ◑ Max-Age 캐싱+엔드포인트 통합으로 영향 최소화.
- “서버-서버 통신에도 CORS가 필요.” → ❌ 서버/봇/모바일 네이티브는 CORS에 안 걸림(브라우저 전용).
- “CORS 허용=보안 취약.” → ◑ 허용 대상 오리진을 제한하고 인증·레이트리밋을 같이 쓰면 안전.
- “JSON만 쓰면 단순 요청.” → ❌ application/json은 프리플라이트 발생.
- “모든 헤더를 노출한다.” → ❌ JS가 읽을 수 있는 헤더는 화이트리스트 + Expose-Headers로 확장.
- “리다이렉트면 자동으로 허용.” → ❌ 최종/중간 응답 모두 CORS 헤더 필요.
- “도메인만 맞으면 된다.” → ❌ 프로토콜/포트까지 포함해 오리진 비교.
- “프록시 쓰면 끝.” → ◑ 서버-사이드 프록시는 우회 수단이지만 보안/스케일/비용/캐시 계획이 필요.
- “개발/운영 동일 설정.” → ❌ 개발은 http://localhost:3000, 운영은 https://app.example.com 등 환경별 화이트리스트 관리.
10. 디버깅 체크리스트
- 네트워크 패널에서 요청/응답 헤더 확인 (특히 Origin, AC-Allow-*)
- 프리플라이트가 뜨는지(OPTIONS) → 응답 코드/헤더 점검
- 쿠키 필요 시 credentials 플래그/도메인/보안 어트리뷰트(Secure, SameSite) 확인
- CDN/프록시라면 Vary: Origin 존재 여부 확인