한 사이트에서 주소가 다른 서버로 요청을 보낼 때 CORS 에러를 한 번쯤은 본 적 있을 것이다.
그렇다면 CORS 에러는 왜 생기며 우리를 괴롭히는(?) 걸까?
CORS를 이해하기 전, 먼저 SOP에 대해 알아야 한다.
1. SOP (Same Origin Policy)
SOP는 말 그대로 동일 출처 정책, 즉 동일한 출처(Origin) 끼리만 API 등의 데이터 접근이 가능하도록 막는 정책을 말한다.
다르게 말하면 다른 출처의 리소스를 사용하는 것에 제한하는 보안 방식이다.
출처(Origin)란 어떤 걸 말하는 걸까?

URL은 위의 사진과 같이 Protocol, Host, Port, Path, Query-String, Fragment .. 등으로 이루어져있다.
이 중 출처는 Protocot, Host, Port 까지를 말하며, 3가지가 모두 같아야 같은 출처라고 할 수 있다.
왜 동일 출처에서만 리소스를 공유하도록 제안할 필요가 있을까?
많은 사람들은 인증 토큰을 활용해 여러 페이지에서 활동하며 정보를 브라우저의 쿠키에 저장한다.
이 덕분에 로그아웃하지 않아도 동일한 사이트에 다시 접속하면 로그인 상태를 유지할 수 있다.
로그인 상태가 유지된다는 것은 사용자가 API 요청을 보낼 때,
해당 요청이 신뢰할 수 있는 사용자로부터 왔음을 증명하기 위해 브라우저에 저장된 인증 정보(쿠키, 토큰 등)가 함께 전달된다는 의미다.
이 과정만으로는 문제가 없지만, 해커들이 사용자가 악성 HTML, CSS, JS를 포함한 웹사이트에 접속하도록 유도하면 상황이 달라진다.
사용자가 이러한 악성 웹사이트를 방문하면, 브라우저는 자동으로 해당 사이트의 JS 코드를 다운로드하여 실행하게 된다.
이 경우, 악성 JS 코드가 사용자 브라우저에 저장된 개인정보를 조회하는 요청을 보내면서 인증 정보(토큰)까지 함께 전송해 정보를 탈취할 위험이 생긴다. 이렇게 탈취된 정보는 사용자의 계정이나 데이터를 악용하는 데 사용될 수 있다.
따라서 브라우저는 이러한 공격을 방지하기 위해 기본적으로 SOP을 적용하여, 악의적인 사이트가 다른 출처의 리소스에 접근하는 것을 차단한다.
모든 브라우저는 SOP 정책을 가지고 있기 때문에 다른 출처에서의 요청을 막고 있는 것은 CORS이 아니라 SOP이라고 할 수 있다.
CORS는 SOP 때문에 막힌 요청을 풀 수 있는 방법을 말한다.
2. CORS (Cross Origin Resource Sharing)
Cross Origin은 Same-Origin(동일 출처)의 반대 개념인 다른 출처를 말하며, CORS는 다른 출처 간에 리소스(주고 받는 데이터)를 공유할 수 있도록 하는 걸 말한다.
즉 서로 다른 출처끼리 정보 요청과 반환이 가능하도록 하는 게 CORS이다.
CORS는 브라우저에서 발생하기 때문에
Postman으로 요청을 보내거나 백엔드에서 HTTP로 요청을 보내면 CORS 오류가 나지 않지만,
실제 웹사이트에 GET 요청을 보내게 되면 CORS 오류가 뜨게 되는 것이다.
웹 생태계가 다양해지면서 여러 서비스들 간에 자유롭게 데이터를 주고 받을 필요가 있는데
이를 SOP로 브라우저가 다른 사이트 간의 요청을 막고 있으니까
합의된 출처들 간에 합법적으로 허용해주기 위해 어떤 조건을 충족시키면 리소스 공유가 되도록 만들어진 매커니즘이 CORS이다.
어떤 조건을 충족시키면 될까?
요청을 받는 백엔드에서 이걸 허락할 다른 출처들을 미리 명시하면,
지정한 사이트에서는 서버로 얼마든지 HTTP 요청들을 보낼 수 있게 된다.
3. CORS 동작 방식
CORS 동작 방식은 Simple Request와 Preflight Request, Credential Request 3가지로 나뉜다.
3.1 Simple Request
Simple Request는 Preflight 요청없이 바로 요청을 날리며, 다음의 조건을 모두 만족해야 한다.
- GET, POST, HEAD 메서드
- Content-Type
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- 헤더는 Accept, Accept-Language, Content-Language, Content-Type만 허용
동작 방식은 다음과 같다.
- 브라우저는 서로 다른 출처 간 요청을 보낼 때, 요청 header에 Origin이라는 값을 추가한다.
- 헤더(Header)는 데이터를 전송할 때 추가되는 보충 정보이며, 요청을 받는 서버는 이 헤더를 통해 요청한 클라이언트의 IP 주소, 사용할 프로토콜, 요청 옵션 등을 확인할 수 있다. Origin 헤더에는 요청하는 측의 scheme(프로토콜), 도메인, 포트가 포함된다.
- Scheme(프로토콜)은 요청할 자원에 접근하는 방식을 나타낸다. http, https, ftp 등이 있다.
- 요청을 받은 서버는 응답 헤더에 Access-Control-Allow-Origin 값을 포함하여 응답한다.
- 브라우저는 Origin 헤더의 출처 값이 서버 응답의 Access-Control-Allow-Origin 값과 일치하면 요청을 안전한 것으로 간주하고 응답 데이터를 받아온다. 만약 일치하지 않으면 CORS 오류를 발생시킨다.
- 토큰 등 사용자 식별 정보가 포함된 요청의 경우, 요청을 보내는 측에서는 요청 옵션의 credentials 항목을 true로 설정해야 한다.
- 서버에서는 요청을 보낸 출처(웹페이지 주소)를 정확히 명시한 후, Access-Control-Allow-Credentials 항목을 true로 설정해야 한다.

3.2 Preflight Request
아래 3가지의 경우에는 보안상의 이유로 사전에 허용 여부를 검증해야 하기 때문에 Prefilght 요청이 필요하다.
- PUT, DELETE, PATCH 등 서버의 데이터를 변경할 수 있는 HTTP 메서드를 사용한 경우
- Authorization, X-Custom-Header, Content-Type: application/json 등 허용되지 않은 헤더를 포함한 경우
- credentials: true 등 Credentials 설정이 필요한 경우
동작 방식은 다음과 같다.
- 본 요청을 보내기 전에 Preflight 요청(OPTIONS 메서드)을 먼저 보내서 요청이 안전한지 확인한 후, 서버의 허가를 받아야 본 요청을 전송할 수 있다.
- Preflight 요청이 성공하면 브라우저는 본 요청을 정상적으로 실행한다. 만약 서버에서 허용하지 않으면 본 요청은 차단된다.

3.3 Credentialed Request
Credential Request는 인증 관련 헤더를 포함할 때 사용하는 요청이며, 이를 사용하면 로그인 절차를 간소화하고 사용자 경험(UX)을 향상시킬 수 있다.
Credential Request를 사용하려면 아래의 조건을 만족해야 한다.
- 클라이언트에서 요청 시 credentials: include 설정
- 서버에서 응답 시 Access-Control-Allow-Credentials: true 설정
웹 브라우저에서 지원하는 주요 Credential 유형은 다음과 같다.
- PasswordCredential: 아이디/비밀번호 기반 로그인 지원
- FederatedCredential: Google, Facebook 등 소셜 로그인 지원
동작 방식은 다음과 같다.
- 사용자가 웹사이트에서 로그인할 때, 브라우저는 navigator.credentials.get() API를 통해 저장된 인증 정보를 자동으로 요청할 수 있다.
- 요청을 받은 브라우저는 저장된 PasswordCredential 또는 FederatedCredential을 확인하고, 해당 정보를 반환한다.
- 서버는 받은 인증 정보를 활용하여 사용자를 로그인 처리한다.
- 사용자가 자동 로그인을 원하지 않거나 인증 정보가 만료된 경우, 브라우저는 사용자가 직접 로그인하도록 안내한다.
- 만약 credentials 요청이 실패하면, 브라우저는 UI를 제공하여 사용자가 다시 인증을 요청하거나, 서버와 별도의 로그인 절차를 진행할 수 있도록 한다.
4. CORS 에러 해결하는 방법
4.1 서버에서 해결하는 방법
- 서버에서 CORS 헤더(Access-Control-Allow-Origin) 추가
- 서버가 응답할 때, 브라우저가 허용할 수 있도록 CORS 관련 헤더를 추가해야 한다.
- 서버의 응답 헤더에 아래와 같이 설정하면 특정 출처에서 요청을 허용할 수 있다.
const allowedOrigins = ["https://example1.com", "https://example2.com"];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
}
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Allow-Credentials", "true");
next();
});
- Access-Control-Allow-Credentials 설정 (토큰, 쿠키 사용 시 필수)
- 인증 정보(토큰, 쿠키)를 포함한 요청에서는 credentials: true를 설정해야 한다.
- 서버에서 응답 시, 출처를 *(와일드카드)로 설정하면 credentials을 허용할 수 없다.
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
- Preflight 요청(OPTIONS 메서드) 처리
- PUT, DELETE, PATCH 등의 요청이 있을 경우 Preflight 요청(OPTIONS 메서드) 을 처리해야 한다.
app.options("*", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Allow-Credentials", "true");
res.sendStatus(204);
});
4.2 클라이언트에서 해결하는 방법
- 프록시 서버 사용 (package.json 수정)
- React, Vue 등 프론트엔드 개발 시, package.json에 프록시 설정을 추가한다.
- 이후 요청을 http://localhost:3000/api/data로 보내면 자동으로 https://api.example.com/api/data로 프록시 처리된다.
"proxy": "https://api.example.com"
- Nginx 프록시 설정
- 아래의 설정을 적용하면 https://backend.example.com에서 온 응답이 CORS 정책을 따르도록 설정됨
server {
listen 80;
server_name example.com;
location /api/ {
proxy_pass https://backend.example.com/;
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE";
add_header Access-Control-Allow-Headers "Authorization, Content-Type";
}
}
참고
https://www.youtube.com/watch?v=bW31xiNB8Nc&t=5s
'Web' 카테고리의 다른 글
[Web] 로그인했다고? 너 누군데? 세션과 JWT에 대해 (1) | 2025.03.27 |
---|---|
[Web] SPA? MPA? CSR? SSR? SSG? (1) | 2025.03.22 |
[Web] Webpack이란 무엇일까? (1) | 2024.12.14 |
[Web] 모듈 번들러, 한 페이지로 끝내기 (2) | 2024.12.13 |