구글에 CORS 에러라고 검색하면 브라우저 콘솔 창에서 띄우는 이런 식의 에러를 흔히 찾아볼 수 있다. 보면 CORS policy 때문에 fetch가 막혔다고 안내하는 것을 볼 수 있는데 이번 포스팅에서는 이것에 대해서 알아보려고 한다. 그러려면 우선 CORS가 등장하게 된 배경인 SOP에 대해서 먼저 설명해야 한다. SOP란 Same-Origin Policy로 우리말로 동일 출처 정책 정도로 번역할 수 있다. 이름에서 추측할 수 있듯이 같은 출처의 리소스만 공유가 가능하도록 하는 정책인데 여기서 출처라는 것은 프로토콜에 해당하는 scheme과 도메인 혹은 IP 주소인 hosts 그리고 포트까지를 의미한다.
즉 프로토콜, 호스트, 포트 이 중에서 하나만 다르더라도 동일 출처라고 보지 않는 것이다. 이런 정책이 생겨나게 된 이유는 잠재적으로 해로울 수 있는 문서를 분리함으로써 공격받을 수 있는 경로를 줄이기 위해서이다. SOP가 적용되어 있다면 애초에 다른 사이트와의 리소스 공유를 제한하기 때문에 로그인 정보와 같은 개인 정보가 타 사이트의 코드에 의해서 유출되는 것을 막을 수 있다. 이런 보안상의 이점 때문에 모든 브라우저에서 SOP를 기본적으로 사용하고 있다. 하지만 문제는 다른 출처의 리소스를 사용해야 할 일이 생각보다 많다는 것이다. 하나의 웹 사이트를 개발하기 위한 클라이언트와 서버부터가 출처가 다르기 때문이다.
이런 문제점을 해결하기 위해 사용되는 것이 바로 CORS이다. CORS는 Cross-Origin Resource Sharing의 줄임말로 교차 출처 리소스 공유 정도로 번역할 수 있다. MDN에서는
교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다.
라고 정의를 내리고 있다. 즉, CORS를 사용하면 SOP가 적용되어 있어도 다른 출처의 리소스에 접근할 권한을 얻을 수 있다는 것이다. CORS와 관련된 설정에서 클라이언트를 개발하는 프론트엔드 웹 개발자가 직접 코드를 작성할 일은 많지 않은 편이고 주로 서버측에서 응답 헤더를 설정해서 브라우저에게 제대로 응답해주고 인증 및 권한 검사를 수행해서 무단 접근을 방지해야하는 편이다. 그래서 간략하게 CORS의 동작 방식 정도를 알아보려고 한다.
CORS의 동작 방식은 크게 세 가지로 나눌 수 있다.
프리플라이트 요청이란 실제 요청을 보내기 전에 OPTIONS 메서드로 사전 요청을 보내 해당 출처 리소스에 접근 권한이 있는지부터 확인하는 것이다.
위의 사진에서 볼 수 있듯이 브라우저에서는 실제 요청 전에 프리플라이트 요청을 먼저 서버로 보내게 된다. 서버는 이 요청을 받고 허용이 되는지를 확인한 뒤에 출처, 메서드, 헤더 등의 CORS와 관련된 헤더를 포함해서 브라우저에 응답한다. 브라우저에서는 이 정보를 토대로 실제 요청이 가능한지 판단하고 가능하다면 실제 요청을 보내서 데이터를 받고 그렇지 않은 경우에는 위에서 보였던 것처럼 에러를 발생시키게 되는 것이다.
한편, 특정 조건을 만족하면 브라우저에서 사전 확인 작접인 프리플라이트 요청 없이 서버에 바로 요청을 보낼 수 있는데 이른 단순 요청이라고 한다. 우선 단순 요청이 가능하려면 3가지 조건을 충족해야한다.
- 메서드 종류는 GET, POST, HEAD
- 커스텀 헤더 없음
- 요청의 Content-Type 헤더는 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나일 것
프리플라이트 요청이 실제 요청을 보내기 전에 미리 권환을 확인해서 실제 요청을 처음부터 통째로 보냈다가 거부되는 것보다 리소스 측면에서 효율적이긴 하지만 프리플라이트 요청 또한 추가적인 네트워크 오버헤드가 발생되기 때문에 가능하다면 단순 요청을 활용하는 편이 좋다.
인증정보를 포함한 요청이란 말 그대로 요청 헤더에 인증 정보를 담아서 보내는 요청이다. 출처가 다를 경우에는 별도의 설정이 없다면 쿠키를 보낼 수 없으며 클라이언트 측에서도 요청 헤더에 withCredentials : true를 적어주어야 한다.
좋은 글 감사합니다!