프로그래밍 공부를 하다보면 오픈 API에 요청보내고 응답받는 일을 굉장히 많이 하게 된다.
그러다보면 CORS 에러도 필연적으로 만나게 되는데, 대체 CORS란 무엇인가?
Cross-Origin Resource Sharing (CORS)
많은 웹 어플리케이션들이 이미지 파일이나 폰트, css 파일 같은 리소스들을 각각의 출처로부터 읽어온다. 만약에 웹 어플리케이션이 자기 자신이 속하지 않은 다른 도메인, 다른 프로토콜, 혹은 다른 포트에 있는 리소스를 요청하면 웹 어플리케이션은 cross-origin HTTP 요청을 실행한다. 만약에 프론트엔드 자바스크립트 코드가 https://domain-a.com에서 https://domain-b.com/data.json으로 json 데이터 요청을 보낸다면 이것이 바로 cross-origin HTTP 요청이 된다.
이 때 브라우저는 보안상의 이유로 스크립트 안에서 시작되는 cross-origin HTTP 요청들을 제한한다. 예를 들어서 XMLHttpRequest와 Fetch API는 동일 출처 원칙이 적용된다. 이 말은 웹 어플리케이션이 자기가 로드되었던 그 origin과 동일한 origin으로 리소스를 요청하는 것만 허용한다는 뜻이다. 다른 origin으로부터의 응답이 올바른 CORS header를 포함하지 않는다면 말이다.
즉..
다른 도메인의 img파일이나 css파일을 가져오는 것은 모두 가능하다. 그러나 로 감싸여진 script에서 생성된 Cross-site HTTP Request는 Same-origin policy를 적용받아 Cross-site HTTP Request가 제한된다.
동일 출처로 요청을 보내는 것(Same-origin requests)은 항상 허용되지만 다른 출처로의 요청(Cross-origin requests)은 보안상의 이유로 제한된다는 뜻이다. 그러나 AJAX가 널리 퍼지면서 안의 스크립트에서 생겨난 XMLHTTPRequest에서도 Cross-site HTTP Requests의 필요성이 매우 커졌다. 그러나 W3C에서 CORS라는 이름으로 새로운 표준을 내놓게되었다.
즉, 기존에는 보안상의 이유로 XMLHttpRequest가 자신과 동일한 도메인으로만 HTTP 요청을 보내도록 제한하였으나 웹 개발에서 다른 도메인으로의 요청이 꼭 필요하게 되었다. 그래서 XMLHttpRequest가 cross-domain을 요청할 수 있도록 CORS라는 것이 만들어졌다!
Preflight request
W3C 명세에 의하면 브라우저는 먼저 서버에 Preflight request(예비 요청)를 전송하여 실제 요청을 보내는 것이 안전한지 OPTIONS method로 확인한다. 그리고 서버로부터 유효하다는 응답을 받으면 그 다음 HTTP request 메소드와 함께 Actual request(본 요청)을 보낸다. 만약 유효하지 않다면 에러를 발생시키고 실제 요청은 서버로 전송하지 않는다.
이러한 예비 요청과 본 요청에 대한 서버의 응답은 프로그래머가 구분지어 처리하는 것은 아니다. 프로그래머가 Access-Control- 계열의 Response Header만 적절히 정해주면 Options 요청으로 오는 예비 요청과 GET, POST, PUT, DELETE 등으로 오는 본 요청의 처리는 서버가 알아서 처리한다.
Preflight request는 GET, HEAD, POST외 다른 방식으로도 요청을 보낼 수 있고 application/xml처럼 다른 Content-type으로 요청을 보낼 수도 있으며, 커스텀 헤더도 사용할 수 있다.
Simple Request
어떤 요청들은 CORS Preflight Request(사전 요청)을 발생시키지 않습니다. 보통 이런 요청들을 Simple Request라고 한다.
Simple Request의 경우에는 아래 3가지 조건이 모두 만족되는 경우를 말한다.
GET / HEAD / POST 중 한 가지 메소드를 사용해야 한다.
User agent에 의해 자동으로 설정되는 헤더(Connection, User-Agent 또는 Fetch spec에서 '금지된 헤더 이름(forbidden header name)'으로 정의된 이름들을 가진 헤더들)을 제외하고, Fetch spec에서 'CORS-safelisted request-header'라고 정의되어있고 수동 설정이 허용된 헤더들은 다음과 같다.
application / x-www-form-urlencoded
multipart/form-data
text/plain
Request with Credential
HTTP Cookie와 HTTP Authentication 정보를 인식할 수 있게 해주는 요청이다. 기본적으로 브라우저는 Non credential로 설정되어있기 때문에 credentials 전송을 위해선 설정을 해주어야 한다.
Simple Credential Request 요청 시에는 xhr.withCredentials = true를 지정해서 Credential 요청을 보낼 수 있고, 서버는 Response Header에 반드시 Access-Control-Allow-Credentials: true를 포함해야한다.
또한 서버는 credential 요청에 응답할 때 반드시 Access-Control-Allow-Origin 헤더의 값으로 "*"와 같은 와일드카드 대신 구체적인 도메인을 명시해야한다.
CORS 에러를 피하는 방법
MDN 문서를 읽어보면 다음과 같은 문장이 있다.
"Modern browsers use CORS in a APIs such as XMLHttpRequest or Fetch to mitigate the risks of cross-origin HTTP requests."
모던 브라우저들은 cross-origin HTTP 요청들의 위험성을 완화하기 위해 XMLHttpRequest 또는 Fetch와 같은 APIs에서 CORS를 사용한다.
즉, CORS는 브라우저가 사용하는 것이다. 그러므로 서버에서 서버로 보내는 요청은 CORS가 적용되지 않는다.
그러므로 프록시 서버를 추가로 만들어서 클라이언트에서 우리가 새로 만든 프록시 서버로 요청을 보내고 프록시 서버에서 원하는 타겟 서버에 요청을 보내면 브라우저가 개입되지 않았기 때문에 CORS 오류를 회피할 수 있다.