[WEB 개발 기초] CORS 에러와 해결 방법에 대해 알아보자.

이민선(Jasmine)·2023년 6월 7일
0

WEB 개발 기초

목록 보기
7/7
post-thumbnail

CORS 에러는 프론트엔드 개발자가 웹 개발을 하면서 자주 마주치게 되는 에러로 잘 알려져 있다. 숙련된 개발자라면 CORS에러가 나타나더라도 음 이 녀석 또 나타났군 잘 가라~ 하며 프슝띵똥톡딱따라락 해결이 가능하겠지만, 아직 숙련되지 않은 개발자라면 최악의 경우 에러 tracing의 갈피 자체를 못 잡거나, 원인과 해결방법을 머리로는 알더라도 해결하는 데 시간적 비용이 상당히 많이 소요될 수도 있다.

이번 포스팅에서는 개발자를 귀찮게 하는 존재로만 보였던 CORS 에러의 정체가 무엇인지, 그리고 해결 방법에는 어떤 것들이 있는지 공부해보자. CORS 에러를 이해하기 위해서는 SOP에 대한 이해가 선행되어야 하므로 먼저 살펴보도록 하자.

SOP가 무엇인가?

클라이언트의 출처와 서버의 출처가 서로 같은 경우에만 클라이언트가 스크립트를 통해 서버에게 리소스를 요청(fetch, axios 등)하는 것이 가능하게 하는 웹 보안 정책이 있는데, 이를 SOP(Same Origin Policy) 라고 한다. 만약 출처가 다르다면 (그리고 별도의 추가적 설정이 없다면) 클라이언트의 요청에도 불구하고 브라우저는 서버에서 들어온 응답을 가차없이 파기해버리고 CORS 에러를 내뿜는다.

클라이언트와 서버의 출처가 같아야 한다? 출처가 무엇을 말하는 걸까?

눈으로 먼저 확인해보자. 벨로그에서 포스팅을 하고 있는 zi금 이soon간에 개발자 도구를 열고 console창에

window.location.origin
또는
locaion.origin

을 입력하면 나오는 것이 현재 클라이언트의 출처이다. (port는 생략되어 있음)

이제 출처의 정의를 짚어보자면,
URL에서 scheme, host, port 이렇게 세 부분을 합쳐서 출처라고 한다.
scheme은 HTTP://, HTTPS:// 등 프로토콜을 의미하며,
port는 별도로 명시하지 않을 경우 HTTP일 경우에는 80, HTTPS일 경우에는 443이다.
host는 HTTP:// 또는 HTTPS:// 뒤에 나오는 도메인 이름이나 IP주소를 의미한다.

SOP는 왜 필요한 것인가?

만약 SOP가 없다면 어떨까? 우리는 이 세상에 악의적 의도를 가진 제 3자가 판 치고 있다는 사실을 기억해야 한다. 😈 <- 이렇게 생긴 제 3자가 웹사이트의 스크립트에 접근해서 악성 코드를 심은 후 원래 유저인 것처럼 위장해서 서버에 개인 정보를 요청한다면? 이를 XSS 공격이라고 한다.

이렇게 악의적인 의도를 가진 제 3자에게 서버가 불분명한 정체의 클라이언트의 확인되지도 않은 요청에 전부 답해줄 수 있다면? 우리는 웹 서비스를 믿고 사용할 수 없게 될 것이다. 보안과 웹 생태계의 보존과 직결되는 문제일 수 밖에 없다. 고로 브라우저가 서버의 응답을 파기하지 않고 유저에게 가져다줄 수 있을 최소한의 조건으로 SOP를 도입한 것으로 이해해볼 수 있다.

그래서 SOP만 주구장창 설명한 것 같은데 오늘의 주인공인 CORS에러와는 어떤 관계인 것인가?

CORS 에러가 무엇인가?


오늘의 블로그 짤 찾기를 하기 위해 pixabay.com에 들어갔다가, 문득 개발자 도구를 열고 지금 내 컴퓨터에 켜져 있는 서버인 localhost:3080을 fetch 함수에 인자로 전달하고 엔터를 눌러봤다. CORS 정책에 의해 pixabay.com에서 localhost:3080에 fetch하는 것이 막혀 있단다. SOP 정책에서 설명한 바와 같다. 둘 다 HTTPS로 스킴(혹은 프로토콜)은 같지만, 도메인과 포트 번호가 다르니 출처가 다른 것이다.

그런데 우리는 첫 문장만 읽고 에러 났다고 화내지 말고 뒷 부분을 잘 읽어봐야 한다. Access-Control-Allow-Origin 헤더가 나타나있지 않다고 한다. 그럼 뭔지 모르겠지만 헤더를 어찌저찌 잘 설정을 해주면 에러가 없어진다(뒤에서 자세히 설명할 것이다.)는 뜻인가? 그렇다. 한 마디로 CORS 에러는 SOP 정책으로 인해 교차 출처(cross-origin) 간에 리소스 공유가 어려워서 불편함을 겪는 👼🏻 <- 이렇게 생긴 선의의(?) 개발자들을 위해 예외적으로 두고 있는 정책이다. 만약 웹 보안 강화해보겠다고 출처가 다를 경우 브라우저가 서버에서 온 응답을 전부 파기해버린다면 API 호출할 때 얼마나 제약이 클 것일지 생각해봐야 한다.

그래서 이렇게 웹 보안을 위해 존재하지만 수많은 아가 개발자들을 당혹스럽게 만드는 CORS에러를 어떻게 하면 잠재우고 개발에 전념할 수 있을지 대표적인 방법들을 알아보자.

해결 방법

- Access-Control-Allow-Origin 응답 헤더 세팅

CORS에러를 해결하기 위해 가장 대표적으로 거론되는 방법이다. 서버가 클라이언트에게 응답 메시지를 보낼 때 header의 Access-Control-Allow-Origin에 클라이언트의 출처를 명시해주는 것이다.

const express = require('express');
const app = express();

app.use((req, res, next) => {
  // 허용하는 출처 (요청하는 니 출처가 이렇게 생겼다면 가능)
  res.setHeader('Access-Control-Allow-Origin', 'https://localhost:3000');
  // 허용하는 요청 메서드 (요청 시 이런 메서드 가능)
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  // 허용하는 요청 헤더 (요청할 때 이런 헤더 가능)
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

  next();
});

나는 CRA로 보통 localhost:3000에서 주로 개발을 하기 때문에 예시로 허용하는 출처를 저렇게 지정해보았다.
그런데 Access-Control-Allow-Origin에 *를 기재하면 서버는 '내가 가진 데이터, 소중하긴 하지만 모두에게 무료 나눔합니다'라고 하는 것과 다를 바 없어서 지양해야 한다. *는 어떤 클라이언트가 요청하든 브라우저가 응답을 파기하지 않고 전달해주기 때문에 보안 관념이 있는 개발자라면 내가 응답을 전달하고자 하는 클라이언트의 출처를 명확히 헤더에 기재하는 것이 바람직하다.

- webpack dev server proxy

webpack 개발 서버를 이용하여 CORS 정책을 우회하는 방법도 있다. webpack이라는 것이 번들링만 해주는 줄 알았는데, HMR(Hot Module Replacement), 클라이언트 요청 중개, 프록시 기능 등을 수행하는 개발 서버(로컬 서버)도 제공한다고 한다.

특히 오늘은 개발 서버가 클라이언트가 외부 서버에게 리소스 요청을 할 때, 클라이언트와 서버의 중간에서 proxy로서 중개해주는 역할을 한다는 점에 집중해보자. 먼저 CORS 정책을 어떻게 우회할 수 있는지 메커니즘을 살펴보려고 한다.

일반적으로는 클라이언트가 외부 서버에 요청을 직접 보내지만, webpack-dev-server는 중간에서 요청과 응답을 중개한다.

이 때 클라이언트의 출처와 외부 서버의 출처가 다르다면? webpack-dev-server가 있으면 요청과 응답이 가능해진다. 어떻게? 일반적으로 클라이언트와 webpack-dev-server의 출처는 동일하다. (예외적으로 다른 특수한 경우가 있긴 하다고 한다.) 이때 서버와 서버 사이에서는 CORS가 적용되지 않기 때문에 서버 간 출처가 달라도 리소스 공유가 가능하다. 따라서 webpack-dev-server가 대신 심부름해서 리소스를 받아오는 셈이다.

사용 방법

webpack-dev-server를 사용하여 CORS를 우회하는 방법은 비교적 간단하다.

package.json


package.json 마지막 부분에 프록시 서버가 요청을 전달해야 할 도착지인 외부 서버의 출처를 기재하면 된다.

한 가지만 더 하면 되는데, axios나 fetch 요청을 할 때 path 앞부분의 도메인을 지워야 한다.

BookService.js

export const getAllBooks = async () => {
  // fetch api 할 때 앞에 있었던 출처(http://localhost:3080)를 지워야 한다.
  const response = await fetch("/api/books");
  return await response.json();
};

export const createBook = async (data) => {
  // fetch api 할 때 앞에 있었던 출처(http://localhost:3080)를 지워야 한다.
  const response = await fetch("/api/book", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ book: data }),
  });
  return await response.json();
};

물론 webpack-dev-server도 CORS를 우회하기에 아주 간편한 proxy 방법이지만, 한계도 존재한다. webpack-dev-server는 단일 프록시 설정에 초점을 맞추고 있다. 단일 프록시? 그게 뭔가요? 아까 위에서 본 package.json 파일을 다시 보자.

proxy에 서버의 출처를 2개 이상 기재 가능한가? 하나 밖에 기재 못한다. 그럼 2개 이상의 서로 다른 서버에 api를 호출해야 할 때는 어떻게 해야 하나요? 그럴 줄 알고 라이브러리가 다 준비되어 있다. 이름 하여 에이치티티피프록시미들웨어.

- React Proxy (http-proxy-middleware 라이브러리)

이 라이브러리를 사용하면 다중 프록시 설정이 가능하다.
webpack-dev-server는 proxy 기능을 제공하는 개발 서버가 있는 구조라면, 이 라이브러리를 사용하면 프록시 서버 자체가 구축이 되는 것이라고 한다.

이하 CRA 이용 중이라 가정하고 설명하겠다.

사용 방법

우선 설치부터 하자구~

npm install http-proxy-middleware

설치가 끝나면 src 폴더 바로 하위에 setupProxy.js라는 파일을 생성해야 한다.

setupProxy.js

// 불러와 불러와 베이비
const { createProxyMiddleware } = require("http-proxy-middleware");

module.exports = function (app) {
  app.use(
    "/api1", // proxy가 필요한 path prameter
    createProxyMiddleware({
      target: "http://localhost:3080", // 외부 서버의 출처
      changeOrigin: true, // 프록시 서버가 서버의 출처와 동일하게 요청 헤더의 Access-Control-Allow-Origin을 변경해주게 하는 옵션
    })
  );

  // api를 호출해야 하는 교차 출처(cross-origin)의 서버 개수가 늘어나면 프록시를 밑에 또 설정해주면 된다. 아주 유연한 방법이다.
  app.use(
    "/api2",
    createProxyMiddleware({
      target: "http://localhost:3070",
      changeOrigin: true,
    })
  );
};

방금 설치한 http-proxy-middleware에서 createProxyMiddleware를 불러온다. 그리고 외부 서버의 출처, 즉 proxy의 도착지와 proxy가 필요한 path parameter를 각각 입력해주면 된다.

changeOrigin: true

이 부분은 프록시 서버가 클라이언트가 보낸 요청의 헤더에 있는 Access-Control-Allow-Origin을 외부 서버의 출처와 동일하게 바꾸는 옵션이다. 이렇게 하면 외부 서버는 동일한 출처로부터 들어온 요청인 것으로 인식하여 리소스를 공유해줄 수 있게 되고, 결과적으로 CORS를 우회할 수 있는 것이다.

여기까지 CORS 에러와 해결 방법에 대해 살펴보았다. 곧 있으면 팀 프로젝트를 시작하는데, 상황에 맞게 해결 방법을 잘 찾아나갈 수 있는 힘을 기른 포스팅 시간이었다. 아주 값지군! 그럼 이제 자러갑니다. 짜이찌앤~

참고:
https://yozm.wishket.com/magazine/detail/1225/

profile
기록에 진심인 개발자 🌿

0개의 댓글