2025-09-22 00:52
-
CORS는 다른 출처(Origin)의 리소스를 안전하게 요청하기 위한 브라우저-서버 간의 표준 합의 규칙이다.
-
웹의 핵심 보안 모델인 동일 출처 정책(SOP)을 유연하게 완화하여 최신 웹 애플리케이션 개발을 가능하게 한다.
-
CORS의 작동 방식은 HTTP 헤더를 통해 이루어지며, 단순 요청과 프리플라이트 요청 두 가지 시나리오를 이해하는 것이 중요하다.
개발자라면 반드시 알아야 할 CORS 완벽 정복 핸드북
웹 개발자라면 누구나 한 번쯤은 마주쳤을 붉은색 에러 메시지. “No ‘Access-Control-Allow-Origin’ header is present on the requested resource.” 이 메시지는 개발자를 당황하게 만들지만, 사실은 웹의 중요한 보안 체계가 잘 작동하고 있다는 신호다. 바로 CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유) 정책이다.
CORS는 에러가 아니라, 더 안전하고 개방적인 웹을 만들기 위한 약속이자 핸드셰이크다. 이 핸드북을 통해 CORS가 왜 탄생했는지, 어떤 구조로 작동하며, 어떻게 다루어야 하는지 완벽하게 이해하고 정복해 보자.
1. CORS는 왜 만들어졌는가: 모든 이야기의 시작, 동일 출처 정책(SOP)
CORS를 이해하려면 먼저 웹 브라우저의 가장 근본적인 보안 원칙인 **동일 출처 정책(Same-Origin Policy, SOP)**을 알아야 한다.
동일 출처 정책(SOP)이란?
SOP는 하나의 ‘출처(Origin)‘에서 로드된 문서나 스크립트가 다른 ‘출처’의 리소스와 상호작용하는 것을 제한하는 규칙이다. 여기서 ‘출처’는 다음 세 가지 요소의 조합으로 결정된다.
| 요소 | 예시 1 | 예시 2 | 비교 | 동일 출처 여부 |
|---|---|---|---|---|
| 프로토콜 | https:// | http:// | 다름 | X |
| 호스트(도메인) | example.com | example.com | 같음 | - |
| 포트 | :443 | :80 | 다름 | X |
예를 들어, 사용자가 https://mybank.com에 로그인한 상태라고 가정해 보자. 이때 다른 탭에서 악의적인 웹사이트 https://evil.com을 열었다. 만약 SOP가 없다면, evil.com의 스크립트가 mybank.com으로 “계좌 잔액 조회” 요청을 보내고 그 응답을 가로채 사용자 정보를 탈취할 수 있을 것이다.
SOP는 바로 이런 상황을 막기 위해 존재한다. 브라우저는 “너는 evil.com에서 왔으니, mybank.com의 정보에 함부로 접근할 수 없어!”라며 요청에 대한 응답을 차단한다. 이는 웹을 안전하게 만드는 매우 중요한 방어막이다.
SOP의 한계와 CORS의 등장
SOP는 강력한 보안을 제공하지만, 현대 웹 개발 환경에서는 걸림돌이 되기도 한다. 오늘날의 웹 서비스는 기능별로 서버를 분리(마이크로서비스 아키텍처)하거나, 외부 API를 활용하는 것이 일반적이다.
-
https://my-service.com(프론트엔드)에서https://api.my-service.com(백엔드 API)의 데이터를 요청 -
https://my-blog.com에서https://api.weather.com의 날씨 정보를 가져와 표시
이 모든 시나리오는 프로토콜, 호스트, 포트 중 하나라도 다르기 때문에 SOP에 의해 차단된다. 개발자들은 이 문제를 해결하기 위해 과거에는 JSONP와 같은 우회적인 방법을 사용했지만, 이는 보안에 취약하고 GET 요청만 가능하다는 한계가 있었다.
이러한 문제를 표준화되고 안전한 방식으로 해결하기 위해 등장한 것이 바로 CORS다. CORS는 서버와 브라우저가 서로 약속된 HTTP 헤더를 교환하여, **“이 출처는 내가 믿을 수 있으니 리소스 접근을 허용해 줘”**라고 알려주는 메커니즘이다. 즉, SOP라는 강력한 보안 원칙을 무력화하는 것이 아니라, 서버의 동의 하에 일부 출처에 대한 예외를 허용해 주는 합리적인 ‘방문증’ 시스템인 셈이다.
2. CORS의 작동 원리: HTTP 헤더를 통한 핸드셰이크
CORS의 핵심은 HTTP 헤더에 있다. 클라이언트(브라우저)와 서버는 특정 헤더를 통해 서로 통신하며 리소스 접근 권한을 확인한다. 이 과정은 크게 두 가지 시나리오로 나뉜다.
-
단순 요청 (Simple Request)
-
프리플라이트 요청 (Preflight Request)
시나리오 1: 단순 요청 (Simple Request)
단순 요청은 특정 조건을 만족할 때, 브라우저가 예비 요청 없이 바로 본 요청을 보내는 방식이다. 조건은 다음과 같다.
-
메서드:
GET,HEAD,POST중 하나여야 한다. -
헤더: 기본 헤더 외에
Accept,Accept-Language,Content-Language,Content-Type헤더만 허용된다. -
Content-Type 헤더:
application/x-www-form-urlencoded,multipart/form-data,text/plain값만 허용된다.
작동 흐름:
-
클라이언트 요청: 브라우저는 요청을 보낼 때 자동으로
Origin헤더에 현재 요청을 보내는 출처를 담아 보낸다.HTTP
GET /data HTTP/1.1 Host: api.example.com Origin: https://client.com -
서버 응답: 서버는 요청을 받고
Origin헤더를 확인한다. 만약 해당 출처를 허용하기로 했다면, 응답 헤더에Access-Control-Allow-Origin을 포함하여 보낸다.HTTP
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://client.com Content-Type: application/json { "message": "Data loaded successfully!" }만약 모든 출처를 허용하려면 와일드카드(*)를 사용할 수 있다.
Access-Control-Allow-Origin: *
-
브라우저 확인: 브라우저는 서버의 응답 헤더에
Access-Control-Allow-Origin이 있는지, 그리고 그 값이 요청의Origin과 일치하는지 확인한다. 일치하면 요청한 스크립트로 응답 데이터를 전달하고, 일치하지 않으면 CORS 에러를 발생시키고 데이터를 폐기한다.
시나리오 2: 프리플라이트 요청 (Preflight Request)
단순 요청의 조건을 벗어나는 요청은 모두 프리플라이트 요청의 대상이 된다. 예를 들어 PUT, DELETE 메서드를 사용하거나, Content-Type이 application/json이거나, 커스텀 헤더(X-Auth-Token)를 포함하는 경우다.
프리플라이트(Preflight, 사전 비행)라는 이름처럼, 브라우저는 본 요청을 보내기 전에 **“내가 이런 요청을 보내도 괜찮을까?”**라고 서버에 확인하는 예비 요청을 먼저 보낸다. 이 예비 요청은 OPTIONS 메서드를 사용한다.
작동 흐름:
-
클라이언트 예비 요청 (OPTIONS): 브라우저는 본 요청(예:
PUT)에 대한 정보를 담아OPTIONS메서드로 예비 요청을 보낸다.-
Access-Control-Request-Method: 본 요청에서 사용할 메서드 (PUT). -
Access-Control-Request-Headers: 본 요청에서 사용할 커스텀 헤더 (Content-Type,X-Auth-Token).
HTTP
OPTIONS /resource/123 HTTP/1.1 Host: api.example.com Origin: https://client.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: Content-Type, X-Auth-Token -
-
서버 예비 응답: 서버는 이
OPTIONS요청을 받고, 앞으로 들어올 본 요청을 허용할지 여부를 응답 헤더에 담아 전달한다. 이 응답에는 실제 데이터가 포함되지 않는다.-
Access-Control-Allow-Origin: 접근을 허용하는 출처. -
Access-Control-Allow-Methods: 허용하는 메서드 목록. -
Access-Control-Allow-Headers: 허용하는 헤더 목록. -
Access-Control-Max-Age: 이 예비 요청의 결과를 브라우저가 캐시할 시간(초). 이 시간 동안은 동일한 요청에 대해 프리플라이트 요청을 생략한다.
HTTP
HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://client.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, X-Auth-Token Access-Control-Max-Age: 86400 -
-
브라우저 확인 및 본 요청: 브라우저는 예비 응답을 확인하고, 자신이 보내려던 본 요청이 허용 범위에 포함되는지 검사한다. 허용된다면, 그제서야 원래 보내려던 **본 요청(
PUT)**을 서버로 보낸다. 이후 과정은 단순 요청과 동일하다. 만약 허용되지 않는다면, CORS 에러를 발생시키고 본 요청은 보내지 않는다.
3. 심화 내용: 인증 정보와 주요 헤더 정리
인증 정보를 포함한 요청 (Credentialed Requests)
쿠키, HTTP 인증, TLS 클라이언트 인증서와 같은 인증 정보(Credentials)는 보안상의 이유로 CORS 요청에 기본적으로 포함되지 않는다. 만약 cross-origin 요청에 인증 정보를 포함해야 한다면 클라이언트와 서버 양쪽 모두에서 추가 설정이 필요하다.
-
클라이언트 설정:
fetchAPI나XMLHttpRequest사용 시, credentials 옵션을include로 설정해야 한다.JavaScript
fetch('https://api.example.com/data', { credentials: 'include' // 'same-origin', 'include', 'omit'(default) }); -
서버 설정: 서버는 응답 헤더에
Access-Control-Allow-Credentials: true를 반드시 포함해야 한다.
매우 중요한 규칙: 만약 Access-Control-Allow-Credentials 헤더를 true로 설정한다면, Access-Control-Allow-Origin 헤더에는 와일드카드(*)를 사용할 수 없다. 반드시 https://client.com과 같이 명확한 출처를 명시해야 한다.
CORS 주요 헤더 치트 시트
| 헤더 | 구분 | 설명 |
|---|---|---|
Origin | 요청 | 요청이 시작된 출처를 나타낸다. |
Access-Control-Allow-Origin | 응답 | 서버가 리소스 접근을 허용하는 출처를 명시한다. |
Access-Control-Allow-Credentials | 응답 | 요청에 인증 정보(쿠키 등) 포함을 허용할지 여부를 나타낸다. |
Access-Control-Request-Method | 프리플라이트 요청 | 실제 요청에서 사용할 HTTP 메서드를 나타낸다. |
Access-Control-Request-Headers | 프리플라이트 요청 | 실제 요청에서 사용할 커스텀 헤더를 나타낸다. |
Access-Control-Allow-Methods | 프리플라이트 응답 | 서버가 허용하는 HTTP 메서드의 목록을 나타낸다. |
Access-Control-Allow-Headers | 프리플라이트 응답 | 서버가 허용하는 헤더의 목록을 나타낸다. |
Access-Control-Max-Age | 프리플라이트 응답 | 프리플라이트 응답 결과를 캐시할 수 있는 최대 시간(초)을 나타낸다. |
4. CORS 문제 해결 및 실전 적용
브라우저 개발자 도구의 콘솔에서 CORS 관련 에러를 마주쳤다면, 다음을 기억하라. “CORS 에러는 클라이언트(브라우저)의 문제가 아니라, 서버의 응답 설정 문제다.”
-
에러 메시지: “No ‘Access-Control-Allow-Origin’ header is present…”
-
원인: 서버가 응답에
Access-Control-Allow-Origin헤더를 포함하지 않았다. -
해결: 서버 측 코드에서 요청의
Origin을 확인하고, 허용된 출처라면 해당 헤더를 응답에 추가해야 한다.
대부분의 백엔드 프레임워크는 CORS 설정을 쉽게 할 수 있는 라이브러리나 미들웨어를 제공한다. 예를 들어, Node.js Express에서는 cors 미들웨어를 사용하여 간단하게 해결할 수 있다.
JavaScript
const express = require('express');
const cors = require('cors');
const app = express();
// 모든 출처에서의 요청을 허용
// app.use(cors());
// 특정 출처만 허용하고, 인증 정보 포함을 허용하는 경우
const corsOptions = {
origin: 'https://client.com',
credentials: true
};
app.use(cors(corsOptions));
app.get('/data', (req, res) => {
res.json({ message: 'CORS is configured!' });
});
app.listen(3001, () => console.log('Server is running'));
결론: CORS는 장벽이 아닌 소통의 규칙
CORS는 개발 초기 단계에서 번거로운 장벽처럼 느껴질 수 있다. 하지만 그 본질은 무질서한 웹 환경에서 안전하게 데이터를 교환하기 위한 최소한의 약속이자 소통의 규칙이다.
CORS의 동작 원리를 이해하는 것은 단순히 에러를 해결하는 것을 넘어, 웹의 보안 모델과 브라우저의 작동 방식을 더 깊이 이해하는 과정이다. 이제 붉은색 CORS 에러 메시지를 마주하더라도 당황하지 말자. 그것은 브라우저가 우리의 애플리케이션을 묵묵히 지켜주고 있다는 든든한 신호이며, 우리는 서버에 올바른 ‘방문증’을 발급해 주기만 하면 된다.