웹개발을 진행하면서 가장 처음 애를 먹었던 오류가 바로 CORS였고, 현재까지도 프로젝트를 진행하다보면 빈번하게 CORS를 마주친다. 처음에 CORS를 마주쳤을 때는 이해하기도 어려웠고 원하는 대로 API요청이 이루어지지 않아 스트레스를 많이 받았는데, 지금 CORS 에러를 마주하면 전보다는 버벅이지 않는 모습을 보면서 아 그래도 점점 성장하고 있구나를 느낀다. 따라서 이번 포스팅에는 CORS 에러에 대해서 한 번 다뤄보도록 하겠다.
CORS 란?
CORS는 Cross-Origin-Resource Sharing 으로 교차 출처 리소스 공유라는 뜻으로 해석할 수 있다. 여기서 말하는 교차 출처라는 것은 CORS를 다루는 많은 포스팅에서 다른 출처라는 뜻으로 해석하여 설명한다. 그렇다면 CORS는 다른 출처와의 리소스 공유를 뜻한다고 할 수 있겠다.
출처(Origin)란?
다른 출처와의 리소스 공유라는 말에서 출처가 뜻하는 것은 어떤 것인지 알아보도록 하자. 출처(Origin)라는 것은 위에 볼 수 있는 주소창에서 나타내는 URL을 통해 확인해볼 수 있다. 현재 URL이 "https://www.randomdomain.com:3000/user?query=page=1#origin" 이라고 가정해보자. 구성요소에 따라 분리를 해보자면
다음과 같이 분리할 수 있다. 여기서 출처(Origin)는 프로토콜 + 호스트 + 포트 이 세가지를 합친 것을 의미한다. 그러나 대부분 URL을 확인해보면 포트번호가 명시되어있지 않은 것을 볼 수 있는데, 이러한 경우는 프로토콜마다 기본적으로 정해져있는 포트 번호를 따른다는 것이다. RFC 2616 문서를 확인해보면 기본 포트번호가 주어지지 않거나 비어있다면 80번으로 가정한다고 명시되어있다. 이렇게 포트번호가 주어지지 않는 경우를 제외하고 포트 번호가 주어진다면, 동일 출처를 만족하기 위해서는 프로토콜, 호스트와 함께 포트번호까지 일치해야한다.
SOP
출처(Origin)에 대해서는 어느 정도 이해가 되었다면, 또 다른 정책인 SOP에 대해서도 알아보자.
SOP는 Same-Origin-Policy로 동일 출처 정책을 의미한다. 위에서 설명한 CORS와는 뗄 수 없는 정책으로, 말 그대로 같은 출처만 리소스 공유가 가능한 정책이다. 따라서 다른 출처로 리소스 공유를 요청하면 SOP 정책을 위반하는 것이고, CORS는 다른 출처로 리소스 공유가 가능하게끔 SOP를 위반하지 않도록 풀어주는 정책이라고 이해할 수 있다.
CORS가 적용되는 구간
Postman으로 API 요청을 보냈을 때는 정상적으로 동작하는데, 로컬에서는 CORS 에러가 나는 상황을 본적이 있을 것이다. CORS가 서버에서 적용되는 정책이라면 서버에서도 에러 로그가 남아야하는데, 서버 쪽에서는 리소스 요청에 대한 정상적인 응답을 한다. 이 이유는 CORS 정책이 적용되는 구간은 브라우저이기 때문이다. 따라서 서버는 리소스 요청에 대한 응답을 해주고 브라우저는 해당 응답을 분석하여 출처가 동일 하지 않으면 차단하는 방식이다. 사진 출처
브라우저의 CORS 동작 원리
브라우저에 구현된 정책인 CORS는 어떻게 이루어지는지에 대해서도 알아보도록 하자.
기본 동작은 다음과 같은 순서로 이루어진다.
1. 클라이언트가 HTTP 프로토콜 요청 헤더에 Origin(출처)를 담아 전달한다.
2. 서버가 응답 헤더에 리소스 공유가 허용된 Origin(출처)를 Access-Control-Allow-Origin에 담아 클라이언트에 전달한다.
3. 응답을 받은 브라우저가 요청을 보냈던 Origin(출처)와 응답 헤더에 담긴 Access-Control-Allow-Origin을 비교한다.
4. 응답이 유효하지 않으면 해당 응답을 버린다. (CORS 에러가 발생한다.)
위의 과정을 보면 굉장히 간단하게 보일 수 있지만, CORS 동작 원리는 세가지의 시나리오로 나뉘어진다.
1. 단순 요청 (Simple Request)
단순 요청은 위에서 설명한 기본 동작 그대로 수행되는 요청으로, 바로 밑에서 설명할 예비 요청없이 바로 서버로 요청하여 그에 대한 응답으로 서버가 Access-Control-Allow-Origin을 헤더에 담아 보내주게 되면 브라우저에서 CORS 정책 위반 여부를 검사한다. 단순 요청을 하기 위해서는 몇가지의 조건이 필요한데, 대부분의 요청은 해당 조건을 만족하기 쉽지 않기 때문에 단순 요청이 아닌 예비 요청 방식이 사용된다. 만족해야 하는 조건은 다음과 같다.
1. 요청 메소드가 GET, POST, HEAD 중 하나여야 한다.
2. Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width 헤더만 사용해야 한다.
3. Content-Type 헤더가 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나여야 한다.
2. 예비 요청 (Preflight Request)
예비 요청(Preflight Request)은 대부분의 요청 방식에 해당하고, 예비 요청을 통해 서버와 통신 상태를 확인하여 본 요청을 보내기 전에 브라우저 단에서 안전한 요청인지 확인하는 역할을 수행한다. 예비 요청의 HTTP 메소드는 OPTIONS라는 메소드가 사용된다. 예비 요청의 동작 과정을 한 번 살펴보자.
1. fetch API를 통해 리소스를 요청한다.
2. 브라우저는 서버에게 예비 요청(Preflight Request)을 먼저 보낸다.
3. 서버는 예비 요청에 대한 응답으로 응답헤더에 어떤 것들을 금지하고 허용하는지 정보를 담아 브라우저에게 보낸다.
4. 브라우저는 요청 헤더에 담긴 정보와 응답 헤더에 담긴 정보를 비교하여 요청이 안전하다고 확인되면 본 요청을 보낸다.
5. 서버가 본 요청에 대한 응답을 하면 브라우저는 응답 데이터를 넘겨준다.
위와 같은 과정으로 예비 요청이 동작하게 되는데 실제 개발자 도구에서 예비 요청 방식에서 요청 헤더와 응답 헤더의 값을 확인해보도록 하자.
예비 요청의 HTTP 메소드인 것을 확인할 수 있다. 다음으로는 요청 헤더와 응답 헤더를 확인해보자.
위에서 알아본 예비 요청의 동작 과정에서 2번에 해당 하는 과정으로, access-control-request-headers 헤더에 실제 요청에 사용할 헤더, access-control-request-method 실제 요청에 사용할 메소드를, origin 헤더의 자신의 출처를 넣어 보낸다.
위에서 알아본 예비 요청의 동작 과정에서 3번에 해당 하는 과정으로, access-control-allow-headers 헤더에 허용되는 헤더들의 목록을, access-control-allow-methods 헤더에 허용되는 메소드의 목록을, access-control-allow-origin 헤더에 허용되는 출처의 목록을 담아 브라우저로 보낸다.
따라서 위의 예시에서 요청 헤더와 응답 헤더에 명시되어 있는 출처를 확인해보면 둘이 같은 것을 확인할 수 있고, CORS 정책을 통과하여 리소스 공유가 가능하다.
3. 인증된 요청 (Credentialed Request)
인증된 요청(Credentialed Request)은 인증 정보를 담아 서버에 요청을 하는 경우에 사용되는 방식이다. 기본적으로 브라우저에서 제공하는 요청 API들은 인증 정보를 요청 데이터에 담지 못하게 되어있는데, 쿠키, Authorization 헤더에 설정하는 토큰과 같은 인증 정보를 보내기 위해서는 요청 시에 별도의 설정이 필요하기 때문에 인증된 요청 방식을 사용한다. ajax 요청에는 다양한 도구가 사용될 수 있는데, 인증된 요청 방식을 사용하기 위해서 XMLHttpRequest, ajax, axios를 사용하는 경우에는 withCredential 옵션을 true로 설정하고, fetch API를 사용하는 경우에는 credentials 옵션을 include로 설정해야 한다. 이렇게 별도의 옵션 설정을 해주지 않는 한 인증 정보는 서버로 넘어가지 않는다.
추가적으로 이렇게 클라이언트 단에서 인증 정보를 요청에 포함시켰을 경우에는 서버 단에서도 다르게 처리를 해주어야 한다. 응답 헤더에서 access-control-allow-headers, access-control-allow-methods, access-control-allow-origin 설정에 와일드 카드(*)를 사용할 수 없고, access-control-allow-credentials 항목을 true로 설정해주어야 한다. 그림 출처
처음에 CORS를 접했을 때 프론트 측에서 처리해야 하는지, 백엔드 쪽에서 처리해야하는지 허둥지둥대고 감을 잡지 못했을 때가 있었다. 프론트에서 프록시를 사용하는게 맞는 방법인지, 백엔드에서 처리를 해야 맞는 방법인지, 백엔드에서 처리해야 한다면 와일드 카드를 써야하는지, 와일드 카드를 썼는데도 계속해서 CORS 에러가 나는데 이건 어떻게 처리해야 하는지 등... 오늘은 이렇게 포스팅을 통해 CORS에 대해 정리해봤는데 나도 다시 한 번 개념을 잡는데 유익한 시간이었고, 이 글을 보는 다른 개발자 분들에게도 도움이 되었으면 하는 바람이다.
참고
https://evan-moon.github.io/2020/05/21/about-cors/
'개발 지식 정리' 카테고리의 다른 글
주소창에 URL을 입력하면 무슨 일이 일어날까? (2) | 2023.09.04 |
---|---|
nextjs에서 recoil 세팅하기 (0) | 2023.05.06 |
외부 이미지 가져오기 (feat. nextjs) (0) | 2023.04.30 |
[NextJS] per-page Layout을 사용한 공통 레이아웃 적용 (0) | 2023.03.14 |
issue와 pr 연결하기(feat. Github Projects) (0) | 2023.03.08 |