CORS 완벽 이해

동일 출처 정책, 단순 요청과 프리플라이트 요청, OPTIONS 프리플라이트, Access-Control-* 헤더, 자격 증명, 자주 겪는 오류를 설명합니다.

CORS(Cross-Origin Resource Sharing)는 브라우저가 다른 출처(origin)의 리소스에 자바스크립트로 접근할 수 있도록 서버가 명시적으로 허용하는 표준 메커니즘입니다. 이걸 이해하려면 먼저 동일 출처 정책(Same-Origin Policy)을 알아야 합니다.

동일 출처 정책

출처스킴 + 호스트 + 포트의 조합입니다. 예를 들어 https://app.example.comhttps://api.example.com은 호스트가 달라 서로 다른 출처이고, http://https://도, 포트가 다른 것도 모두 다른 출처입니다. 브라우저는 보안을 위해 스크립트가 자신의 출처와 다른 출처의 응답을 읽는 것을 기본적으로 차단합니다. 이것이 동일 출처 정책이며, 사용자의 쿠키를 악용한 공격을 막는 핵심 방어선입니다.

CORS는 이 정책에 '구멍'을 내는 게 아니라, 서버가 특정 출처의 접근을 허용한다고 응답 헤더로 선언하게 해서 안전하게 교차 출처 통신을 여는 방식입니다. 중요한 점: CORS는 브라우저가 강제합니다. curl이나 서버 간 통신에는 CORS가 적용되지 않습니다.

단순 요청 vs 프리플라이트 요청

브라우저는 교차 출처 요청을 두 가지로 나눕니다. 단순 요청(simple request)GET/HEAD/POST 메서드에, 헤더가 제한된 안전한 집합(예: Content-Typetext/plain·application/x-www-form-urlencoded·multipart/form-data)일 때입니다. 이 경우 브라우저는 바로 요청을 보내고, 응답의 Access-Control-Allow-Origin 헤더를 확인해 스크립트가 읽어도 될지 판단합니다.

그 밖의 요청(예: Content-Type: application/json을 쓰거나, PUT/DELETE, Authorization 외 커스텀 헤더 사용)은 프리플라이트(preflight) 대상입니다. 실제 요청을 보내기 전에 브라우저가 먼저 OPTIONS 요청을 보내 '이런 요청을 보내도 되냐'고 서버에 묻습니다.

OPTIONS /v1/users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

서버가 이 프리플라이트에 허용 헤더로 응답하면 브라우저는 그제야 실제 PUT 요청을 보냅니다. Access-Control-Max-Age는 이 허용 결과를 캐시할 시간이라, 매 요청마다 프리플라이트가 반복되는 것을 줄여 줍니다.

자격 증명(credentials)

쿠키나 Authorization 헤더 같은 자격 증명을 교차 출처 요청에 포함하려면 양쪽 모두 동의해야 합니다. 클라이언트는 fetch(url, { credentials: 'include' })로 요청하고, 서버는 응답에 Access-Control-Allow-Credentials: true를 실어야 합니다. 중요한 제약: 자격 증명을 허용할 때는 Access-Control-Allow-Origin에 와일드카드 *를 쓸 수 없고 반드시 구체적인 출처를 지정해야 합니다.

자주 겪는 오류