CORS
CORS(Cross-Origin Resource Sharing)는 웹 브라우저가 한 출처(origin)에서 로드된 페이지가 다른 출처의 리소스에 접근할 때, 서버가 명시적으로 허용한 경우에만 응답을 전달하도록 제어하는 HTTP 헤더 기반 메커니즘입니다. 출처란 프로토콜, 호스트, 포트의 조합이며, 이 세 가지 중 하나라도 다르면 다른 출처로 간주됩니다.
▶아키텍처 다이어그램
🔄 프로세스 다이어그램점선 애니메이션은 데이터 또는 요청의 흐름 방향을 나타냅니다
브라우저에서 자바스크립트로 API를 호출하면 같은 서버에서는 잘 되던 요청이, API 서버 도메인이 달라지는 순간 브라우저가 응답을 차단합니다. 개발자 도구에 빨간 에러가 뜨고, 서버 로그에는 정상 응답이 찍혀 있는데 브라우저만 거부하는 상황이 벌어집니다. 이건 버그가 아니라 브라우저의 동일 출처 정책(Same-Origin Policy)이 의도적으로 막는 것입니다. 이 정책은 사용자가 로그인한 사이트의 쿠키와 세션을 다른 사이트의 스크립트가 함부로 사용하지 못하도록 보호합니다. 그런데 현대 웹에서는 프론트엔드와 API가 다른 도메인에 배포되거나, CDN에서 폰트를 가져오거나, 서드파티 서비스를 호출하는 일이 일상입니다. 동일 출처 정책만으로는 정당한 요청까지 막혀 버리는 문제가 생깁니다. CORS는 서버가 '이 출처에서 오는 요청은 괜찮다'고 브라우저에게 알려주는 표준 방식입니다.
웹 초기에 브라우저는 하나의 서버에서 HTML, CSS, 자바스크립트, 데이터까지 모두 받아 오는 구조가 일반적이었습니다. 동일 출처 정책은 이 환경에서 자연스러운 보안 경계였고, 사실상 '다른 서버의 리소스를 자바스크립트로 가져올 필요가 없다'는 전제 위에 있었습니다. 그런데 AJAX가 보편화되고, 프론트엔드가 독립 애플리케이션으로 발전하면서 이 전제가 무너졌습니다. API 서버를 분리하고, CDN에서 정적 파일을 서빙하고, 외부 서비스를 호출하는 구조가 표준이 됐습니다. JSONP 같은 우회 기법이 등장했지만 보안 허점이 있었고, 서버가 누구에게 응답을 허용할지 명시적으로 표현할 방법이 필요했습니다. 2014년 W3C가 CORS를 권고안으로 확정한 것은 동일 출처 정책의 보안은 유지하면서, 정당한 교차 출처 통신을 안전하게 열어 주기 위한 것이었습니다.
CORS의 핵심은 브라우저와 서버 사이의 HTTP 헤더 협상입니다. 브라우저는 교차 출처 요청을 보낼 때, 먼저 그 요청이 '단순 요청(Simple Request)' 조건을 충족하는지 확인합니다. GET이나 POST이면서 특수한 헤더가 없으면 바로 요청을 보내고, 응답의 Access-Control-Allow-Origin 헤더를 확인합니다. 이 헤더에 현재 출처가 포함되어 있으면 응답을 자바스크립트에 전달하고, 없으면 응답을 버립니다. 단순 요청 조건을 벗어나면 브라우저는 본 요청 전에 OPTIONS 메서드로 Preflight 요청을 먼저 보냅니다. '이 메서드, 이 헤더로 요청해도 되느냐'고 서버에 물어보는 것입니다. 서버가 Access-Control-Allow-Methods와 Access-Control-Allow-Headers로 허용 범위를 응답하면, 브라우저는 그제야 본 요청을 실행합니다. 쿠키나 Authorization 헤더 같은 자격 증명을 포함하려면 서버가 Access-Control-Allow-Credentials: true를 추가로 응답해야 하고, 이때 Allow-Origin에 와일드카드(*)를 쓸 수 없습니다. 특정 출처를 명시해야 합니다. 이 제약은 자격 증명이 포함된 요청은 더 엄격하게 통제하겠다는 설계 의도입니다.
CORS와 CSP는 둘 다 브라우저에서 동작하는 보안 메커니즘이라 혼동하기 쉽지만, 보호하는 방향이 다릅니다. CORS는 '다른 출처의 서버에 자바스크립트로 요청을 보낼 때, 서버가 이를 허용하는가'를 제어합니다. 보호 대상은 서버의 리소스이고, 정책 결정권은 응답하는 서버에 있습니다. CSP는 '이 페이지 안에서 어떤 출처의 스크립트, 이미지, 스타일을 로드할 수 있는가'를 제어합니다. 보호 대상은 페이지를 보는 사용자이고, 정책 결정권은 페이지를 내려주는 서버에 있습니다. 정리하면, CORS는 '나가는 요청'에 대해 상대 서버가 허락하는 구조이고, CSP는 '들어오는 리소스'에 대해 내 서버가 허용 범위를 미리 정해 두는 구조입니다. 프론트엔드에서 API를 호출할 때 막힌다면 CORS 문제일 가능성이 높고, 페이지에 삽입된 스크립트가 실행되지 않는다면 CSP 정책을 의심해야 합니다.
프론트엔드와 API 서버를 분리 배포하는 구조에서 CORS 설정은 배포 첫날부터 만나는 실무 과제입니다. 로컬 개발 환경에서 프론트엔드가 localhost:3000이고 API가 localhost:8080이면 이미 다른 출처이므로 CORS 설정 없이는 API 호출이 안 됩니다. 가장 흔한 실수는 개발 중 편의를 위해 Access-Control-Allow-Origin: *로 모든 출처를 열어 두는 것입니다. 개발 환경에서는 괜찮지만, 프로덕션에서 자격 증명이 포함된 요청까지 와일드카드로 열면 CSRF 공격에 노출될 수 있습니다. 프로덕션에서는 허용할 출처를 명시하고, Preflight 응답에 Access-Control-Max-Age를 설정해 불필요한 OPTIONS 요청 반복을 줄이는 것이 일반적입니다. CORS 에러가 발생했을 때 프론트엔드 코드를 아무리 수정해도 해결되지 않는 이유는, CORS 정책이 서버 응답 헤더에서 결정되기 때문입니다. 디버깅의 시작점은 브라우저 네트워크 탭에서 Preflight 응답 헤더를 확인하는 것입니다.