웹 개발자의 신고식 🚨 CORS로부터 해방 되기

2023.10.05
13분
댓글

글을 시작하며

웹 개발자의 신입 신고식🚨이라고 할 정도로 CORS는 웹 개발자라면 한 번씩은 겪게 되는 문제입니다.

CORS 에러를 처음 접한 순간엔 백엔드와 프론트엔드 중 어떤 파트에서 이 에러를 해결해야 하는지 감이 안 잡히는 분들도 많으실 것 같습니다.

이번 포스팅을 통해서 CORS 에러란 무엇인지, 그리고 CORS로부터 해방되는 방법들에 대해서 배워보도록 하겠습니다.


CORS

CORS(Cross-Origin Resource Sharing)는 이름 그대로 출처(Origin)가 다른 자원들을 공유한다는 뜻입니다.

231006-042908

쉽게 말하자면 다른 출처의 자원에 접근해서 리소스를 사용한다는 의미입니다!

먼저 출처라는 개념에 대해서 알아보겠습니다.


출처 (Origin)

https://google.com:80/search?page=1

요 url은 저희가 매일 마주하는 구글의 url입니다.

위의 구성요소 중에서 Protocol(https://) + Host(google.com) + Port(80) 3가지가 같으면 동일한 출처(Origin)라고 합니다.


왜 다른 출처는 막아두나요?

우리는 웹에서 <img> <script> <frame> <video> 태그들을 사용하면서 새로운 자원들을 외부에서 가져올 수 있습니다.

만약 은행 홈페이지에 들어가던 중 악성 코드가 심어진 <script> 파일이 포함된 evil.com 페이지를 열게 된다면 어떤 일이 일어날까요?

231006-041732

안타깝게도 script 파일에 Delete/account 요청이 담겨있어서 페이지를 열자마자 유저의 계좌가 삭제되어버렸습니다..

이러한 예상치 못한 사고들을 막으며 보안을 강화하기 위해 다른 출처의 접근을 막는 정책이 등장하게 되었습니다.


동일 출처 정책 (Same-Origin Policy)

따라서 동일한 출처에서만 리소스를 공유할 수 있다는 정책을 사용하게 되었습니다.

여기서의 동일한 출처란 클라이언트와 서버가 같은 출처에 있다면 동일 출처이며 다른 서버에 있다면 다른 출처라고 취급합니다.

230929-232531

예를 들어, 위 사진에서 domain-a.com의 유저가 domain-b.com 서버에 요청하게 되면 호스트가 다르기 때문에 다른 출처에 요청한 상태인 것입니다.

도메인 외에도 같은 프로젝트 내에서 정의된 css 파일에 대한 요청은 동일 출처 요청이며
폰트의 경우 google과 같은 외부 사이트에서 리소스를 가져온다면 다른 출처 요청이라고 할 수 있습니다.

하지만 이렇게 엄격하게 차단해버리면 원하는 페이지를 만들 수 있게 될까요? 🤔
오픈된 인터넷 환경에서 다른 출처의 리소스를 가져와 사용하는 일은 흔한 일입니다.

따라서 몇 가지 예외 조항을 두고 다른 출처의 리소스를 허용하는 정책이 등장했습니다.
그것은 바로 CORS 정책을 지킨 리소스 요청입니다!


다른 출처 정책

사실 우리를 힘들게 했던 CORS의 시빨건 에러 메세지는 다른 출처의 리소스를 얻기 위한 해결 방안이었습니다.
동일 출처 정책(SOP)을 위반해도 CORS 정책을 지킨다면 다른 출처의 리소스도 불러올 수 있게 되는 것입니다!

그렇다면 어떻게 CORS 에러를 해결할 수 있을까요?

먼저 브라우저에서의 CORS 동작 과정에 대해서 살펴보겠습니다.

1. 클라이언트에서 HTTP 요청의 헤더에 Origin을 담아서 전달

웹은 서버에 요청을 보낼 때 HTTP 프로토콜을 이용합니다.
이 때 "여기서 왔어요"를 나타내는 Origin이라는 값을 요청 헤더에 담아서 보냅니다.


2. 서버는 응답 헤더에 Access-Control-Allow-Origin을 담아 클라이언트로 전달

서버는 요청을 받고나서 클라이언트에게 이 "이 url은 리소스에 접근할 수 있어요"Access-Control-Allow-Origin 필더에 담아서 응답합니다.


3. 클라이언트에서 응답을 비교해서 차단 여부를 결정

브라우저는 자신이 보냈던 요청의 Origin과 서버가 보낸 Access-Control-Allow-Origin 값을 비교해서 응답 사용 여부를 결정합니다.

만약 일치하지 않는다면 응답을 사용하지 않고 버리며 이러한 상황이 CORS 에러에 해당됩니다.


CORS 에러 해결법

결국 CORS 에러를 해결하기 위해서는 서버의 허용이 필요합니다.

만약 CORS 에러가 발생한다면 서버측에서 허용할 출처를 헤더의 Access-Control-Allow-Origin에 기재해서 응답하면 해결되는 것입니다.

그렇다면 클라이언트에서도 Origin 값을 변경하면 되지 않을까요?
좋은 시도였지만 Origin 값이 변경되면 브라우저에서 이를 감지하여 차단하기 때문에 가능하지 않습니다 🥲


🤔 정말 프론트에서 해결할 방법이 없나요?

저희도 백엔드 설정이 끝날 때까지 기다릴수만은 없죠..ㅎㅎ
프론트단에서도 아래의 방법을 통해 로컬 환경에서의 CORS 문제를 해결할 수 있습니다!

1. 크롬 확장 프로그램 이용

2. 프록시(Proxy) 이용

  • 프록시는 클라이언트와 서버 사이의 중간 대리점 역할을 합니다.
  • 서버에서 따로 설정을 안 해서 CORS 에러가 발생하는 것이라면, 모든 출처를 허용한 대리점을 통해 요청을 하면 되는 것이죠!
  • Vite의 경우 아래와 같이 proxy 설정을 해줄 수 있습니다.
// vite.config.json
export default defineConfig({
  plugins: [react(), svgr()],
  server: {
    proxy: {
      // 경로가 "/api" 로 시작하는 요청을 대상으로 proxy 설정
      '/api': {
        // 요청 서버 주소 설정
        target: 'http://www.google.com',
        // 요청 헤더 host 필드 값을 서버의 호스트 이름으로 변경
        changeOrigin: true,
        // 요청 경로에서 '/api' 제거
        rewrite: (path) => path.replace(/^\/api/, ''),
        // SSL 인증서 검증 무시
        secure: false,
        // WebSocket 프로토콜 사용
        ws: true,
      },
    },
  },
});

인증 데이터 요청 시의 CORS

위에서 살펴본 동작 흐름은 가장 기본적인 흐름을 설명한 것이고
사실 CORS는 세 가지의 시나리오에 따라서 동작 방식이 달라집니다.

아래의 지식들은 CORS를 당장 해결하는데 필요한 필수 지식은 아니지만, 쿠키나 토큰과 같은 인증 데이터를 요청해야 한다면 반드시 알아야 하는 개념입니다.

예비 요청 (Preflight Request)

브라우저는 먼저 예비 요청을 보내어 통신이 잘 되는지 확인한 후 본 요청을 보냅니다.

이러한 예비 요청을 보내는 것을 Preflight라고 부르며 HTTP 메서드가 OPTIONS라는 특징이 있습니다.


단순 요청 (Simple Request)

단순 요청은 예비 요청을 생략하고 서버에 본 요청을 보낸 후 서버로부터 받은 Access-Control-Allow-Origin를 비교해서 CORS 정책 위반 여부를 검사하는 방식입니다.

사실 대부분의 HTTP 요청은 예비 요청으로만 이루어집니다.
왜냐하면 API 요청은 대부분 text/xml이나 application/json으로 통신하기 때문에 단순 요청에서 요구하는 Content-Type과 맞지 않기 때문입니다.


인증된 요청 (Credentialed Request)

쿠키와 토큰과 관련된 CORS를 해결하고자 하신다면 이 개념에 주목해주시길 바라겠습니다!

인증된 요청은 서버에게 자격 인증 정보를(Credential) 실어서 요청할 때 사용됩니다.

자격 인증 정보란 세션 ID가 저장된 쿠키 혹은 Authorization 헤더에 설정하는 토큰 값들을 말합니다.


클라이언트에서 인증 정보 보내기

기본적으로 fetch와 같이 브라우저가 제공하는 요청 API들은 인증과 관련된 데이터를 요청 데이터에 담지 않도록 설정되어 있습니다.

따라서 credentials 옵션을 설정해주어야 인증 정보를 보낼 수 있습니다!

사용법은 메서드마다 살짝씩 다르기 때문에 예시 코드를 첨부하겠습니다.

fetch('https://example.com/users/login', {
  method: 'POST',
  credentials: 'include',
  body: JSON.stringify({
    userId: 1,
  }),
});
axios.post(
  'https://example.com/users/login',
  {
    userId: 1,
  },
  {
    withCredentials: true,
  },
);
$.ajax({
  ...
  xhrFields: {
    withCredentials: true,
  },
});

서버에서 헤더 설정하기

서버에서도 일반적인 CORS 요청과는 다르게 대응하는 것이 필요합니다.

  • Access-Control-Allow-Credentials 를 true로 설정
  • Access-Control-Allow-Origin 에 와일드카드("*") 사용 불가
  • Access-Control-Allow-Methods 에 와일드카드("*") 사용 불가
  • Access-Control-Allow-Headers 에 와일드카드("*") 사용 불가

인증 정보는 민감한 정보이기 때문에 출처를 정확히 기재해주어야 CORS 에러가 발생하지 않습니다.


글을 마치며

다소 긴 글이 되었지만 글을 작성하면서 CORS와 연관된 개념들과 해결 방법들에 대해서 배울 수 있었습니다.
CORS는 웹 개발 시에 반드시 마주하게 되는 개념이기 때문에 빨간 에러에 좌절하시기 보다는 그 원인과 해결 방법에 대해 알아갈 기회라고 생각하시는 것을 추천드립니다.. ㅎㅎ