제가 처음 프론트엔드 개발을 배우고 첫 프로젝트를 진행했을 때 가장 저를 힘들게 했던 주범은 자바스크립트도, 리액트도 아닌 CORS였습니다. 이번 기회에 CORS에 대해 제대로 알아보려고 합니다.
출처란, uri에서 스킴(프로토콜), 호스트, 포트번호를 의미합니다. 즉, 서버를 찾아가기 위한 최소한의 정보들을 합친 것입니다.
참고로, 브라우저에서는 window
객체의 location
객체가 가진 origin
이라는 프로퍼티를 통해 현재 출처가 어떤지 알아볼 수도 있습니다.
console.log(location.origin);
브라우저는 클라이언트로서 서버와 요청과 응답을 통하여 데이터를 주고 받습니다. 그런데 브라우저의 정책 중에 출처가 다른 서버와 데이터를 주고받을 수 없다는 정책이 있습니다. 이 정책을 동일 출처 정책, SOP(Same Origin Policy)라고 합니다. 이러한 정책이 만들어진 이유는 출처가 다른 컴퓨터들끼리 아무런 제약 없이 데이터를 요청과 응답을 주고 받을 수 있게 하면 보안 문제가 발생하기 쉽기 때문입니다.
그렇다면 오늘의 주제인 CORS는 무엇일까요? 바로 앞서 알아본 SOP 정책에 예외를 두는 매커니즘입니다. 출처가 다르더라도 합의된 규칙을 따른다면 리소스 공유를 허용하겠다는 것입니다. CORS는 cross-origin resource sharing의 약자로, 교차 출처 자원 공유로 해석됩니다. 동일 출처라는 말이 더 직관적이니 앞으로는 동일 출처라고 하겠습니다.
CORS는 서로 다른 출처를 가진 리소스를 안전하게 사용할 수 있게 해주는 장치입니다. 요즘같은 오픈 웹 시대에 진짜로 동일 출처끼리만 자원을 공유하도록 하면 웹의 이점을 누릴 수 없게 되기 때문에 CORS라는 예외를 두어 자원공유를 안전하게 만든 정책이라고 할 수 있겠네요.
CORS에러는 브라우저에서 다른 origin을 가진 서버로 요청을 보내는데 CORS 기준을 충족시키지 않았을 때 발생합니다.
여기서 말하는 기준은 생각보다 단순한데, 서버에서 미리 자원 공유를 허락할 출처들을 명시해두면 됩니다.
브라우저가 서버에 데이터를 요청할 때 Origin이라는 헤더를 추가합니다. 여기에 요청하는 쪽의 scheme, 도메인, 포트넘버가 담기게 됩니다. 출처를 명시해주는 것이죠.
서버는 이 요청을 받고 데이터를 응답에 실어 보냅니다. 이 때 응답헤더에 Access-Control-Allow-Origin이라는 정보를 실어 보냅니다. 여기에 서버가 허용하는 출처들을 명시해둔 것입니다.
브라우저는 이 응답헤더의 값을 읽어 요청 헤더에 보낸 origin이 포함되어있다면 안전한 요청으로 간주하고 응답 데이터를 받아오게 되고, 없으면 CORS에러를 뿜게 됩니다.
사실 위에서 알아본 CORS 요청 검증 과정은 Simple Request라는 요청의 종류입니다. PUT, DELETE메서드와 같이 추가 검증이 필요한 조금은 위험해 보이는 요청들은 요청을 보내기 전에 브라우저에서 서버에게 미리 Preflight라는 것을 보냅니다. 예비 요청이라고 생각하면 쉬운데, preflight에서 먼저 CORS 검증이 이루어 집니다. 이 때 simple request 검증방식보다는 조금 더 빡빡한 기준들을 충족시켜야 안전한 요청으로 판단하게 됩니다. Preflight를 보내는 요청은 simple request와 달리 요청을 보내는 것 또한 허락을 받아야 합니다. 그래서 본 요청 전에 예비 요청을 보내는 것입니다. 이 과정을 그림으로 간단히 나타내면 다음과 같습니다.
가장 정석적인 방법은 서버에서 Access-Control-Allow-Origin을 세팅해주는 것입니다. 백엔드를 구성하는 프레임워크들은 대부분 이 응답 헤더 설정을 쉽고 빠르게 해주는 미들웨어들을 많이 제공하기 때문에 귀찮더라도 안전을 위해 믿을만한 출처를 명시해주는 것이 좋습니다.
다른 방법은 CORS 요청은 브라우저에서만 검증한다는 사실을 이용하여 리버스 프록시를 두는 것입니다. 서버끼리 통신할 때는 SOP니 CORS니 아무런 제약이 없습니다. 이러한 사실을 이용하여 브라우저를 속이는 방법이 리버스 프록시를 사용하는 방법입니다.
우선 다음과 같은 설정을 로컬에서 해놓는다면 브라우저는 ‘/api’로 가는 요청에 대해 localhost로 보내는 요청으로 알고 있지만, 사실은 evan.com, 즉 다른 출처로 요청을 프록시 서버가 보내주는 것입니다.
이 방법은 CORS 정책을 우회하는 방법입니다. 아래와 같은 세팅은 프로덕션 환경에서 달라지는 경우가 있으니 주의하도록 합시다.
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.evan.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
}
}
}
참고자료