CORS에 대해 알아보자
CORS 완전 정복
2025-03-02
서론
이전 면접에서 ‘CORS 에러를 해결하는 방법을 설명해보세요’라는 질문을 받은 적이 있습니다. 당시에는 간단히 서버에서 허용할 도메인을 지정하면 된다고 답했지만, 면접관의 추가 질문이 이어지면서 CORS에 대해 더 깊이 이해할 필요가 있음을 깨달았습니다.
개발자로 일하면서 API 요청을 보낼 때 예상치 못한 CORS 에러를 마주한 경험이 한두 번이 아닐 것입니다. ’분명 API가 잘 동작하는데, 왜 브라우저에서만 오류가 나는 걸까?’라는 의문이 들었던 적이 있다면, 이 글이 도움이 될 것입니다.
이번 글에서는 CORS의 개념부터 실제로 마주할 수 있는 문제 해결 방법까지 차근차근 정리해보겠습니다.
CORS의 개념
💡 [MDN] 교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 브라우저가 자신의 출처가 아닌 다른 어떤 출처(도메인, 스킴 혹은 포트)로부터 자원을 로딩하는 것을 허용하도록 서버가 허가 해주는 HTTP 헤더 기반 메커니즘입니다.
MDN에 정리된 내용처럼 CORS는 어떠한 오류의 종류가 아닌, HTTP 헤더 기반 메커니즘입니다. https://www.google.com
에서 제공되는 프론트엔드 JavaScript 코드가 fetch API를 사용하여 https://www.naver.com/data.json
에 요청하는 경우가 바로 교차 출처 요청의 예시입니다.
브라우저는 일단 보안 상의 이유로 스크립트에서 시작한 교차 출처 HTTP 요청을 제한합니다. 그리고 교차 출처 리소스를 호스팅하는 서버가 실제 요청을 허가할 것인지를 preflight request를 통해 확인합니다. 이때 preflight request의 응답에 올바른 CORS 헤더가 포함되어 있다면 그때 실제 요청을 허가합니다.
HTTP 요청 헤더
클라이언트가 교차 출처 리소스 공유 기능을 사용하기 위해 HTTP 요청을 발행할 때 사용할 수 있는 헤더들을 정리합니다.
- Origin: 클라이언트가 요청을 보낼 때, 현재 페이지의 출처 정보를 명시합니다. 서버는 이 헤더를 기반으로 요청이 허가된 출처에서 온 것인지 판단합니다.
- Access-Control-Request-Method: preflight 요청에서 서버에 허가받으려는 HTTP 메서드(e.g.,
GET
,POST
,DELETE
)를 지정합니다. - Access-Control-Request-Headers: 클라이언트가 preflight 요청에서 서버에 전달하려는 커스텀 헤더들을 나열합니다.
HTTP 응답 헤더
서버는 교차 출처 요청을 허용하기 위해 명세에 정의된 여러 응답 헤더들을 설정할 수 있습니다.
- Access-Control-Allow-Origin: 서버가 허용할 출처를 명시합니다. 특정 도메인(e.g.,
https://example.com
)이나 모든 출처를 허용하기 위해 를 설정할 수 있습니다. - Access-Control-Allow-Methods: 서버가 허용할 HTTP 메서드를 지정합니다. 예:
GET, POST, DELETE
- Access-Control-Allow-Headers: 클라이언트가 요청에서 사용할 수 있는 커스텀 헤더들을 명시합니다.
- Access-Control-Allow-Credentials: 자격 증명(쿠키, 인증 헤더 등)을 포함한 요청을 허용할지 여부를 결정합니다. 이 값이
true
로 설정되면 반드시 특정 출처를 명시해야 합니다. - Access-Control-Expose-Headers: 클라이언트가 응답에서 접근할 수 있는 헤더들을 명시합니다.
- Access-Control-Max-Age: preflight 요청의 응답을 캐싱할 수 있는 시간(초 단위)을 지정합니다.
CORS로 인한 문제를 해결하는 방법
서버에서 해결하기
-
허용 출처 설정
- 서버는 응답 헤더에
Access-Control-Allow-Origin
을 설정하여 허용할 출처를 명확히 지정해야 합니다. 특정 도메인만 허용하려면 해당 도메인을 명시하고, 모든 출처를 허용하려면*
을 사용할 수 있습니다.
예시:
Access-Control-Allow-Origin: https://frontend.example.com
모든 출처를 허용하는 경우:
Access-Control-Allow-Origin: *
단, 자격 증명을 포함한 요청에서는 반드시 특정 출처만 명시해야 합니다.
- 서버는 응답 헤더에
-
HTTP 메서드 허용
-
클라이언트가 사용할 수 있는 HTTP 메서드를 명시적으로 지정해야 합니다. 이를 위해
Access-Control-Allow-Methods
헤더를 응답에 추가합니다.예시:
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
-
-
헤더 허용
-
클라이언트가 요청에서 사용할 수 있는 커스텀 헤더들을 명시하기 위해
Access-Control-Allow-Headers
를 설정해야 합니다.예시:
Access-Control-Allow-Headers: Content-Type, Authorization
-
-
자격 증명 처리
-
쿠키나 인증 헤더를 포함한 요청을 허용하기 위해
Access-Control-Allow-Credentials
를true
로 설정합니다. 이 경우Access-Control-Allow-Origin
에는 특정 출처만 명시해야 하며, 는 사용할 수 없습니다.예시:
Access-Control-Allow-Credentials: true Access-Control-Allow-Origin: https://frontend.example.com
-
클라이언트에서 해결하기
Proxy 사용: 브라우저가 동일 출처 정책(Same-Origin Policy)으로 인해 외부 출처의 리소스를 요청할 수 없을 때, 같은 출처 내에 프록시 서버를 설정하여 문제를 해결할 수 있습니다. 프록시 서버는 클라이언트로부터 요청을 받아 외부 API로 전달하고, 응답을 다시 클라이언트로 반환하는 역할을 합니다. 이를 통해 브라우저는 동일 출처 정책을 위반하지 않고 요청을 수행할 수 있습니다.
프론트엔드 애플리케이션이 https://frontend.example.com
에서 실행 중이고, 외부 API가 https://api.external.com
에 있는 경우를 가정합니다. 이때 프록시 서버가 https://frontend.example.com/api-proxy
에 설정되어 있다면, 클라이언트는 다음과 같은 방식으로 API 요청을 할 수 있습니다.
// 클라이언트 코드
fetch('/api-proxy/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
프록시 서버는 내부적으로 외부 API에 요청을 전달합니다.
// Node.js Express 프록시 서버 예시
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
app.use('/api-proxy', createProxyMiddleware({
target: 'https://api.external.com',
changeOrigin: true,
pathRewrite: { '^/api-proxy': '' },
}));
app.listen(3000, () => console.log('Proxy server running on port 3000'));