CORS 에러 해결하기 + Reverse Proxy server 구축 with Nginx

스윗포테이토·2022년 11월 4일
4

CORS

const response = await axios.get(`http://localhost:5000/test`);

위와 같이 React에서 외부 api로 요청을 보내는 경우, CORS 에러가 뜬다.
CORS는 Cross Origin Resource Sharing로, 서로 다른 출처 간에 자원을 공유하는 것을 말한다. 기본적으로 브라우져는 동일 출처 정책 (Same Origin Policy)을 따른다. 즉, 서로 다른 도메인에서 자원을 공유하는 것을 금지한다. 즉, 프론트엔드에서 Html, Css, JS 등 정적 파일을 받고, 백엔드에서 데이터를 받아오는 경우 동일 출처 정책을 위반하게 되고 CORS 에러가 뜬다. 이 에러를 해결하기 위해서는 프록시 서버를 구축해야 한다.

Proxy

Proxy란 요청을 중계하는 컴퓨터, 혹은 프로그램이다. 웹서핑을 하는 상황을 가정해보자. 이때 프록시 서버를 사용하면 브라우져와 서버 사이에서 요청을 중계해준다. 즉, 브라우져는 모든 요청과 응답을 프록시 서버를 통해서만 주고 받고, 프록시 서버가 브라우져를 대신해서 서버와 통신하는 것이다.

이 개념을 이용해서 CORS 에러를 해결할 수 있다. 즉, proxy 설정을 통해 프론트엔드 요청과 백엔드 요청을 프록시 서버로 받으면 브라우져는 동일 출처로 보고 CORS 에러를 띄우지 않는 것이다.

개발용 Proxy 설정

개발 단계(npm start)에서는 직접 서버를 새로 구축할 필요 없이 리액트 내에서 간단히 해결이 가능하다. 두 가지 방법이 있다.

package.json

package.json을 다음과 같이 수정해주면 된다.

  • package.json
    {
      "name": "client",
      "version": "0.1.0",
      "private": true,
      "dependencies": {
        ...
      },
      "scripts": {
        ...
      },
      "eslintConfig": {
        ...
      },
      "browserslist": {
        ...
      },
      "proxy": "http://127.0.0.1:5000" // 사용하는 백엔드 주소
    }
  • React.js
    이제 프록시 설정을 통해 도메인이 적용되기 때문에, 리액트에서는 도메인 이하 uri만 작성해서 요청을 보내면 된다.
    const response = await axios.get(`/test`);

백엔드 서버로의 요청 하나만 필요하다면 이 방법이 제일 간단하다. 좀 더 커스텀하고 싶으면 두번째 방법을 사용해야 한다.

http-proxy-middleware

http-proxy-middleware 패키지를 통해 좀 더 상세한 설정을 할 수 있다.
1. 패키지 설치

npm i http-proxy-middleware
  1. <Project>/src/setUpProxy.js 파일 작성
    다수의 api를 사용하는 경우 다음과 같이 uri를 통해 라우팅 하듯이 프록시를 설정해줄 수 있다.
const { createProxyMiddleware } = require("http-proxy-middleware");

module.exports = function (app) {
  app.use(
    "/api1",
    createProxyMiddleware({
      target: "http://<domain>:<portNum>",
      changeOrigin: true,
    })
  );
  app.use(
    "/api2",
    createProxyMiddleware({
      target: "http://<domain>:<portNum>",
      changeOrigin: true,
    })
  );
};


프론트엔드에서 /api1 이하 url로 요청을 보내면 App1에게 요청을 전달하고, /api2로 요청을 보내면 App2로 요청을 전달하게 되는 것이다.

  • React.js
    const response = await axios.get(`/api1/test`);
  • App1 - Express.js
    app.get(`/api1/test`, (res, req) => {
      res.send("App1 Received a GET request");
    });

여기서 App1에서는 모든 api에 대해 api1를 붙여줘야 한다는 게 조금 불편할 수 있다. 이 때는 setUpProxy.js에서 rewritePath 옵션을 통해 해결할 수 있다.

예를 들어,

const { createProxyMiddleware } = require("http-proxy-middleware");

module.exports = function (app) {
  app.use(
    "/api1",
    createProxyMiddleware({
      target: "http://app1:5000",
      changeOrigin: true,
      pathRewrite: {
        "^/api1": "", // '/api1'를 빈 문자열로 치환
      },
    })
  );
  app.use(
    "/api2",
    createProxyMiddleware({
      target: "http://app2:7000",
      changeOrigin: true,
      pathRewrite: {
        "^/api2": "", // '/api2'를 빈 문자열로 치환
      },
    })
  );
};

이렇게 경로를 치환해주면

  • React.js
    const response1 await axios.get(`/api1/test`); // http://app1:5000/test로 get 요청
    const response2 await axios.get(`/api2/test`); // http://app2:7000/test로 get 요청
  • App1 - Express.js
    app.get(`/test`, (res, req) => {
      res.send("App1 Received a GET request");
    });
  • App2 - Express.js
    app.get(`/test`, (res, req) => {
      res.send("App2 Received a GET request");
    });
    /api1, /api2 등을 생략하고 api를 만들 수 있다.

도커를 사용중이라면 도메인에 서비스 이름을 써주면 된다.

target: "http://backend:5000"

만일 연결이 되지 않는다면 도커 설정파일에서 network 설정을 해줘야 한다.

  • Docker-compose.yml

    version: "3"
    
    services:
      frontend:
        container_name: frontend
        ...
        networks:
          - app-tier
      backend:
        ...
        networks:
          - app-tier
    networks:
      app-tier:
        driver: bridge

배포용 Proxy 설정

위의 두 방법은 언제까지나 개발용 옵션이다. 배포를 할 때는 npm start가 아닌 외부의 웹서버를 활용하기 때문에, 따로 옵션을 설정해주어야 한다.
나는 웹서버로 Nginx를 사용중인데, reverse proxy를 지원해준다.

  • Nginx.conf

    upstream App1 {
        server app1:5000;
    }
    upstream App2 {
        server app2:7000;
    }
    
    server {
        listen 80;
        location /api1 {
            rewrite /api1/(.*) /$1 break; // `/api1` -> 빈 문자열
            proxy_pass http://App1;
        }
        location /api2 {
            rewrite /api2/(.*) /$1 break; // `/api2` -> 빈 문자열
            proxy_pass http://App2;
        }
        location / {
            root   /usr/share/nginx/html;
            index  index.html index.htm;
            try_files $uri $uri/ /index.html;
        }
    }

nginx에서도 기존 방식처럼 path를 재작성해주고 있기 때문에, 배포를 할 때 따로 소스코드나 Url을 수정하지 않고 그대로 사용할 수 있다.

profile
나의 삽질이 미래의 누군가를 구할 수 있다면...

0개의 댓글