(CS/컴퓨터공학) CORS 이해하기 - 동작 원리, 프리플라이트, 캐싱

✨ 개요

CORS 완전 정복 - 동작 원리, 프리플라이트(OPTIONS), 캐싱, 그리고 흔한 오해 12가지

CORS(Cross-Origin Resource Sharing)는 브라우저가 “다른 오리진의 리소스에 접근해도 되는지”를 서버 응답 헤더로 판단하는 메커니즘입니다.
핵심은 한 줄: 서버가 허용하면 OK, 안 하면 브라우저가 막는다. (클라이언트 코드로 “해제”할 수 없음) —

1. 배경: SOP와 CORS


2 CORS의 요청/응답 흐름 (개념도)

[브라우저 JS] ---- cross-origin 요청 ----> [서버]
↑ |
| <--- CORS 응답 헤더(허용/거부) ----|
허용이면 JS가 응답 본문을 읽을 수 있음

3 ‘단순 요청’ vs ‘프리플라이트(OPTIONS)’

3.1 단순(Simple) 요청

아래 모두 만족하면 사전 요청(OPTIONS) 없이 바로 보냅니다.

3.2 프리플라이트(Preflight) 요청

프리플라이트 예

요청 (브라우저 → 서버)

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

6. 캐싱/성능 팁


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가지


10. 디버깅 체크리스트



Related Posts