CORS란 무엇인가?
개요
Trip-Together 프로젝트를 진행하면서 처음으로 맞딱뜨린 에러였던 CORS 에러를 해결하기 위해 공부한 내용을 정리한 내용이다
- CORS는 무엇인지?
- 어떤 역할을 하는지?
- CORS가 필요한 이유는?
- 구체적인 사용 예시
순으로 내용이 전개될 예정이다
CORS는 무엇인가?
CORS(Cross-Origin Resource Sharing)는 웹 페이지가 자신과 다른 출처(origin)의 리소스에 접근할 수 있도록 서버와 브라우저 간의 데이터 교환을 안전하게 허용하는 보안 메커니즘이다.
웹 보안에서는 동일 출처 정책(Same-Origin Policy, SOP)을 통해 웹 페이지가 같은 출처(origin)의 리소스만 요청할 수 있도록 제한한다 이러한 보안 정책 덕분에 악의적인 웹사이트가 사용자의 데이터를 무단으로 가져가는 것을 방지할 수 있다
그러나 실제 웹 개발에서는 프론트엔드와 백엔드가 서로 다른 출처에서 동작하는 경우가 많아 같은 출처 정책이 불편할 수 있다. 이럴 때 CORS를 활용하면 특정 조건에서 교차 출처 요청을 허용할 수 있다
CORS가 필요한 이유
웹 개발에서 프론트엔드와 백엔드가 서로 다른 출처(origin)를 가질 경우, 브라우저의 동일 출처 정책 때문에 API 요청이 차단된다. 이러한 문제를 해결하고 안전하게 교차 출처 요청을 허용하기 위해 CORS가 필요하다.
예제 상황
- 프론트엔드가 http://localhost:3000에서 실행됨
- 백엔드 API는 http://localhost:5000에서 제공됨
위 상황에서 다음과 같은 요청을 보낼 때
fetch("http://localhost:5000/data")
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("Error:", error));
브라우저는 보안 정책(SOP)에 의거하여 요청을 차단하고, CORS 오류가 발생한다
이런 상황에서, 백엔드 서버에서 CORS를 설정하면 프론트엔드가 백엔드 API에 접근할 수 있다
CORS의 동작 방식
CORS는 서버가 응답할 때 특정 HTTP 헤더를 추가하여 교차 출처 요청을 허용하는 방식으로 동작한다
즉, 브라우저는 요청을 보내기 전에 서버가 해당 출처의 접근을 허용하는지 부터 확인한다
CORS의 요청-응답 프로세스
표준 인터넷 통신에서, 브라우저는 애플리케이션 서버에 HTTP 요청을 보내고, HTTP 응답으로 데이터를 수신하여 웹 페이지에 표시한다
이 과정에서 현재 사용자의 브라우저에서 실행 중인 웹사이트의 출처를 현재 오리진(Current Origin), 요청 대상 출처를 크로스 오리진(Cross Origin)이라고 한다
CORS가 적용되는 크로스 오리진 요청의 전체 프로세스는 다음과 같다
1) 브라우저가 요청을 보냄
브라우저는 HTTP 요청을 보낼 때, 현재 오리진의 프로토콜, 호스트, 포트 정보를 포함한 Origin 헤더를 추가한다
// 예시 Request
GET /data HTTP/1.1
Host: api.example.com
Origin: https://news.example.com
2) 서버가 요청을 검사하고 응답함
서버는 요청의 Origin 헤더를 확인하고, 허용된 출처인지 검토한다
허용된 출처일 경우, 응답 헤더에 Access-Control-Allow-Origin을 포함하여 브라우저가 요청을 허용할지 판단할 수 있도록 한다
// 예시 Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://news.example.com
3) 브라우저가 응답을 확인
브라우저는 응답의 Access-Control-Allow-Origin 값을 확인하고, 현재 출처가 허용된 출처인지 검사한다
요청한 출처가 Access-Control-Allow-Origin에 포함되어 잇다면 요청을 정상적으로 처리하고 데이터를 반환한다
만약 포함되지 않았다면, CORS 오류를 발생시키며 요청을 차단한다
4) 서버에서 크로스 오리진 요청을 허용하지 않는 경우
만약 서버가 CORS를 허용하지 않는다면 응답에서 Access-Control-Allow-Origin 헤더를 포함하지 않는다
이 경우, 브라우저는 보안 정책에 따라 다음과 같은 CORS 오류를 출력하며 요청을 차단한다
Access to fetch at 'https://partner-api.com/data' from origin 'https://news.example.com' has been blocked by CORS policy.
실제 패킷 확인해보기
1) 브라우저가 요청을 보냄
사용자가 웹 페이지에서 https://lh3.googleusercontent.com/...에 있는 이미지를 요청하면, 브라우저는 해당 URL이 현재 웹사이트와 다른 출처에 속하는지 확인한 후 HTTP 요청을 보낸다
// 요청 헤더
GET /a/ACg8ocl7p2E6vG1Cw... HTTP/1.1
Host: lh3.googleusercontent.com
Referer: (없음) → no-referrer 정책 적용
User-Agent: Mozilla/5.0 ...
브라우저는 현재 오리진 정보를 Origin 헤더에 포함해야 하지만, 이 요청에서는 no-referrer로 인해 Origin 헤더가 보이지 않음
즉, 요청이 보안 정책에 의해 최소한의 정보만 포함되도록 설정되어 있다
2) 서버가 요청을 검사하고 응답함
Googleusercontent의 서버(https://lh3.googleusercontent.com/...)는 브라우저가 보낸 요청을 확인하고, CORS 정책을 적용하여 응답을 반환한다
// 응답 헤더
Access-Control-Allow-Origin: * ← 모든 오리진에서 요청 가능
Access-Control-Expose-Headers: Content-Length ← 브라우저가 이 응답 헤더에 접근 가능
Cross-Origin-Resource-Policy: cross-origin ← 다른 도메인에서도 리소스를 사용 가능
Timing-Allow-Origin: * ← 모든 출처에서 성능 데이터 확인 가능
각 응답 헤더의 동작 방식은 다음과 같다
- Access-Control-Allow-Origin: *
모든 출처에서 이 리소스에 접근할 수 있도록 허용한다
특정 도메인만 허용하려면 * 대신 도메인명을 지정해야 한다. (Access-Control-Allow-Origin: https://example.com)
하지만, * 설정 시 인증 정보(쿠키, 인증 헤더 등)는 포함할 수 없다 - Access-Control-Expose-Headers: Content-Length
기본적으로 브라우저는 CORS 요청에서 일부 응답 헤더만 접근할 수 있다
하지만 이 설정을 추가하면, 클라이언트(JavaScript 코드)가 Content-Length 값을 읽을 수 있도록 허용한다
예를 들어, fetch()로 요청할 때 response.headers.get("Content-Length")를 사용할 수 있다 - Cross-Origin-Resource-Policy: cross-origin
웹 리소스 보호를 위한 기본 정책(Same-Origin Policy)을 우회하여, 다른 도메인에서도 이 리소스를 자유롭게 요청하고 사용할 수 있도록 허용한다
예를 들어, trip-together.com에서 이 이미지를 <img> 태그로 불러와도 차단되지 않는다 - Timing-Allow-Origin: *
다른 출처의 웹사이트에서도 이 서버의 성능 데이터를 확인할 수 있도록 허용한다
예를 들어, Performance API를 이용해 리소스 로딩 속도를 측정할 수 있다
특정 도메인에서만 성능 데이터를 확인하도록 하려면 Timing-Allow-Origin: https://example.com처럼 설정해야 한다
3) 브라우저가 응답을 확인 후 처리함
브라우저는 서버의 응답을 받고, CORS 정책에 맞게 요청을 허용할지 차단할지 결정한다
- 서버가 Access-Control-Allow-Origin: *(와일드카드)을 설정했으므로, 브라우저는 응답을 허용
- 브라우저(chrome)는 Access-Control-Expose-Headers를 확인하여, Content-Length 등의 특정 헤더 값을 클라이언트(triptogether.com)에서 접근 가능하도록 노출
- 최종적으로 이미지를 화면에 표시하거나, 해당 리소스를 가져와 추가적인 처리를 수행할 수 있음
CORS가 사용되는 상황들
지금까지 CORS가 프론트엔드-백엔드 API 요청에서 필요하다는 점을 살펴봤지만, 실제로는 다양한 환경에서 사용된다
1) 웹 폰트
웹사이트에서 Google Fonts 같은 외부 폰트를 사용하면 CORS 요청이 발생한다
폰트 제공 서버는 Access-Control-Allow-Origin: *을 설정하여, 다양한 웹사이트에서 폰트를 로드할 수 있도록 허용해야 한다
Access-Control-Allow-Origin: * Content-Type: font/woff2
만약 해당 헤더가 없다면, 웹사이트에서 폰트가 로드되지 않는 문제가 발생할 수 있다
2) 클라우드 스토리지 (AWS S3, Google Cloud Storage)
프론트엔드에서 AWS S3, Google Cloud Storage(GCS) 같은 클라우드 스토리지에서 이미지나 파일을 직접 가져올 때 CORS 설정이 필요하다
만약 CORS가 설정되지 않았다면, 프론트엔드에서 해당 파일을 불러올 수 없다
// AWS S3에서 CORS 설정 예제:
<CORSConfiguration>
<CORSRule>
<AllowedOrigin>https://example.com</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
3) CDN(Content Delivery Network) 사용 시
Cloudflare, Akamai, Fastly 같은 CDN을 이용해 정적 리소스를 배포할 때, CORS가 설정되지 않으면 외부 웹사이트에서 CDN 리소스를 가져올 수 없다
따라서 CDN 제공업체는 Access-Control-Allow-Origin: * 또는 특정 오리진만 허용하는 설정을 제공해야 한다
4) OAuth 인증 과정 (예: 구글 로그인, 깃허브 로그인)
프론트엔드에서 OAuth 기반 인증(구글, 페이스북, 깃허브 로그인)을 구현할 때, CORS가 중요한 역할을 한다.
예를 들어, 프론트엔드에서 백엔드에 OAuth 토큰을 요청하는 경우, 서버에서 Access-Control-Allow-Credentials: true를 설정해야 한다
그렇지 않으면, 브라우저에서 인증 정보가 포함된 요청을 차단하게 된다
CORS 베스트 프랙티스
CORS는 잘못 설정하면 보안 취약점을 초래할 수 있기 때문에, 다음과 같은 베스트 프랙티스를 따르자
1) 적절한 액세스 목록 정의
API를 공개적으로 사용할 목적이 아니라면, 허용할 도메인을 명확히 지정해야 한다
Access-Control-Allow-Origin: https://example.com
2) null 오리진 허용 방지
일부 브라우저는 파일 요청(file://) 또는 localhost 요청에서 Origin: null 값을 전송할 수 있다
하지만, 서버에서 Access-Control-Allow-Origin: null을 허용하면, 공격자가 null 오리진을 이용해 무단 요청을 보낼 수 있다
따라서 null 오리진을 허용하지 않도록 해야 한다
3) Access-Control-Allow-Credentials 설정 주의
인증이 필요한 요청(credentials: "include")에서는 Access-Control-Allow-Origin: *을 사용하면 안 된다
이 때, 보안적으로 안전하게 설정하려면, 특정 도메인과 함께 사용해야 함.
// 잘못된 예시
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
위와 같이 설정하면 어떤 사이트에서도 인증된 요청을 보낼 수 있어 보안 취약점이 발생한다
// 올바른 예시
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
4) Access-Control-Allow-Methods를 제한적으로 사용
불필요한 HTTP 메서드를 허용하지 않도록 설정해야 한다. 필요한 메서드만 열어놓자
Access-Control-Allow-Methods: GET, POST
마무리
Trip-Together 프로젝트를 진행하며, 처음으로 CORS 오류를 경험하고 이를 해결하기 위해 학습하면서 많은 것을 배울 수 있었다
배운 점
- CORS는 단순한 에러가 아니라, 웹 보안을 위한 중요한 정책이라는 점
- 서버와 프론트엔드 개발자가 CORS 정책을 사전에 명확히 논의해야 한다는 것 (불필요한 오버헤드 방지)
향후 고려할 점
- 운영 환경과 개발 환경에서의 CORS 정책을 다르게 설정하는 것이 중요하다.
(예: 개발에서는 localhost를 허용하지만, 운영에서는 특정 도메인만 허용) - 사용자 인증이 포함된 요청에서는 CORS 설정을 더욱 신중하게 다뤄야 함 (Access-Control-Allow-Credentials 주의)
프로젝트에서 얻은 인사이트
CORS는 개발 과정에서 한 번쯤 마주치게 되는 에러지만, 그 본질을 이해하지 못하면 단순히 "CORS 문제니까 백엔드에서 해결해줘"라고만 생각할 수 있을 것 같다
하지만 이번 경험을 통해, 이 오류가 단순한 코드상의 문제가 아니라 보안 정책의 일환이라는 점을 명확히 이해하게 되었다
앞으로 프로젝트를 진행할 때는 CORS 정책을 먼저 명확히 정의하고, 사전에 논의하는 것만으로도 개발 속도를 더 높일 수 있을 것이라 생각한다
CORS 문제를 겪으며 웹 보안과 브라우저 정책에 대해 한 걸음 더 깊이 이해할 수 있었던 좋은 경험이었다