최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday

티스토리 뷰

목차

1. CORS 에러

2. Proxy서버로 우회하는 방법

3. express 로 프록시 서버 만들기

4. React에 적용해보기

5. package.json 을 사용한 프록시 설정

6. CRA의 setupProxy.js 를 사용하기


1. CORS 에러

이미지 클릭시, 크게보기

많은분들이 익히 알고있겠지만 빠르게 그 원인에 대해서만 정리하면 다음과 같다

 

웹 어플리케이션에서 다른 도메인 또는 포트로 부터 리소스를 요청할 때 발생하는 보안 메커니즘인데, 웹 어플리케이션은

보안 상의 이유로 스크립트에서 동일 출처 정책(Same Origin Policy)을 따르게 되어있다.

동일 출처 정책은 웹 페이지가 다른 도메인, 포트, 프로토콜로 부터 리소스를 요청할 때 제한을 두는 보안 메커니즘이다.

 

CORS는 클라이언트에서 발생한다. 따라서 웹 브라우저는 보안 상의 이유로 다른 출처로의 AJAX 요청에 대해서는 동일 출처 정책을 적용하고, 서버는 요청에 대해 적절한 CORS 헤더를 반환해야 한다.

 

이때 서버가 CORS 헤더를 반환하지 않거나 잘못된 헤더를 반환하는 경우, 웹브라우저는 CORS에러를 발생시키는 것이다.

 

CORS 에러가 나는 주요 상황은 다음과 같다.

 

- 다른 도메인에서 자원에 대한 ajax 요청을 보낼 때

- XML Http Request 또는 Fetch API 를 사용하여 다른 도메인으로 리소스 요청

- 특정 HTTP 메서드(예를 들어, PUT, DELETE)나 특정 커스텀 헤더를 사용하여 요청

 

CORS 에러를 해결하기 위해서는 서버 측에서 CORS 정책을 설정해야 한다.

서버는 요청에 대한 CORS 헤더를 반환하여 브라우저가 리소스에 접근할 수 있도록 허용한다.

 

- Access-Control-Allow-Origin

- Access-Control-Allow-Methods

- Access-Control-Allow-Headers 

 

등이 포함될 수 있다.

 

2. Proxy 서버 우회하는 방법

이미지 클릭시, 크게보기

3. express 로 프록시 서버 만들기

프록시 서버를 직접 만들어서 localhost 에서 사용해보자 

 

3.1) 필요한 모듈 설치

- cors, express, express-http-proxy, morgan을 한번에 설치해보자

// install devDependencies
$ npm i --D cors express express-http-proxy morgan
or
$ yarn add -D cors express express-http-proxy morgan

3.2) 프록시 서버 스크립트를 작성

- proxy.js 혹은 타입스크립의 경우 proxy.ts

/** 
 * Simple proxy server using express.
 * @description cors를 우회하기 위한 간단한 프록시 서버
 * react에서 로컬서버 구동을 위한 프록시는 setupProxy.js 에서 설정을 하지만
 * bundle된 static 파일에 대한 cors를 테스트 하기위한 목적으로 사용됨.
 * @setting 
 * - js
 * '.js' and '.mjs' files are interpreted as ES modules.
 * package.json: must add the top-level { "type": "module" }
 * package.json: "scripts" in "proxy": "node proxy.js",
 * $ npm run proxy
 * - typescript
 * If written in 'typescript' and used when registering a command...
 * package.json: "scripts" in "start:proxy": "ts-node ./proxy.ts",
 * $ npm run start:proxy
 * @see package.json -type https://nodejs.org/api/packages.html#type
 * @see ts-node -install https://www.npmjs.com/package/ts-node
 * @example
 * // When used as an asynchronous function
 * async function apiRequest(path = '') {
    try {
        const response = await fetch(`http://localhost:9090/api/my/server${path}`);
        return response.json();
      } catch (error) {
        throw new Error(`request error: ${error.status}`);
      }
      return null;
    }
 */
import cors from 'cors';
import express from 'express';
import proxy from 'express-http-proxy';
import morgan from 'morgan';

const app = express();
const url = 'https://www.my.proxy.server.com'; // 요청을 보낼 서버 도메인
const port = 9090; // 프록시 서버 사용 포트

app.use(cors());
app.use(morgan('tiny'));
app.use('/', proxy(url));

app.listen(port, () => {
  console.log(`👉 Listening on ${port}, 📮 Proxy [ ${url} ]`);
});

3.3) 프록시 서버 구동하기

- 먼저 위의 `import` 구분을 사용하기 위해서 package.json 의 `type` 부분에 `module` 형식으로 지정해 줘야 한다.

 

- nodejs 서버에서 프록시 서버를 직접 구동하려면 `$ node proxy.js` 명령어로 직접 실행해도 된다.

 

- 그러나 package.json 의 scripts 부분에 CLI 를 미리 입력해두고 사용할 수 있으므로 몇가지 스크립트를 추가하겠다

 

- "scripts": { "proxy": "node proxy.js" }

   `$ npm run proxy` 명령어로 프록시를 구동시킬 수 있다

 

- "scripts": { "start:proxy": "ts-node ./proxy.ts" }

   만약 타입스크립로 작성하고 구동하고 싶은 경우, `ts-node` 를 설치하고

   proxy.ts 로 작성된 파일은 `$ npm run start:proxy` 명령어로 프록시를 구동시킬 수 있다

 

- "scripts": { "serve": "http-server ./index.html -p 8080 -c-1" }

   테스트로 프록시 서버를 사용해 볼 클라이언트 페이지인 index.html 정적파일을 로컬서버에 구동해보려면 사용한다

   `http-server` 모듈을 설치하여 `$ npm run serve` 명령어로 index.html 파일을 로컬서버로 구동시킬 수 있다.

 

- 최종 완성된 package.json 모습은 아래와 같다

{
  "name": "proxy",
  "version": "0.1.0",
  "description": "Sample implementation of proxy",
  "type": "module",
  "dependencies": {
    "cors": "^2.8.4",
    "express": "^4.16.3",
    "express-http-proxy": "^1.2.0",
    "morgan": "^1.9.0"
  },
  "devDependencies": {},
  "scripts": {
    "proxy": "node proxy.js",
    "start:proxy": "ts-node proxy.ts",
    "serve": "http-server ./index.html -p 8080 -c-1"
  },
  "author": "serpiko",
  "license": "MIT"
 }

3.4) 클라이언트 측 소스코드에서 프록시를 사용하기

- 프록시 서버를 먼저 띄운다 (3.3 참고)

- 기존 API 요청을 예를 들어, `https://www.my.proxy.server.com` 와 같이 했다면

   이제는 `http://localhost:9090` 으로 변경해야 한다

- `http://localhost:9090`로 요청하면, 실제로는 https://www.my.proxy.server.com 으로 요청이된다

- 예를 들면 아래와 같이 api 를 요청할 때 경로를 변경해 주어야 한다는 것이다

...
const response = await fetch(`http://localhost:9090/api/my/request`);
// 실제로는 https://www.myproxyserver.com/api/my/request 로 요청하게 된다
response.json();
...

 

4. React에 적용해 보기

4.1) https://github.com/serpiko-git/react-proxy-tutorial 에 예제를 구성하였으니 clone을 하려면 다음과 같다

git clone [REPO_URL] [DIR]

$ git clone https://github.com/serpiko-git/react-proxy-tutorial.git

 4.2) 만약 git 저장소가 아닌, 직접 수동으로 개발환경을 구성하려면 다음과 같다

- CRA 설치

$ npx create-react-app proxy-tutorial

// 설치 이후 동작 시켜보자
$ cd proxy-tutorial
$ npm start

- express 모듈 설치

$ npm i --D cors express express-http-proxy morgan

4.3) 여기서 부터는 이해를 위해서 반드시 확인해야 하는 내용이다.

        폴더 구조는 다음과 같다

/src
├── App.js // 리액트 앱
└── proxy.js // 프록시

 

4.4) src/proxy.js 작성

import cors from 'cors';
import express from 'express';
import proxy from 'express-http-proxy';
import morgan from 'morgan';

const app = express();
const url = 'https://reqres.in'; // 요청을 보낼 서버 도메인
const port = 9090; // 프록시 서버 사용 포트

app.use(cors());
app.use(morgan('tiny'));
app.use('/', proxy(url));

app.listen(port, () => {
  console.log(`👉 Listening on ${port}, 📮 Proxy [ ${url} ]`);
});

4.5) react와 express 구동을 위한 package.json 작성

 

- `import` 구분을 사용하기 위해서 package.json 의 `type` 부분에 `module` 형식으로 지정해 줘야 한다.

- script 에는 클라이언트를 3001 번 포트로 구동하기 위한 cli와 proxy를 구동하기 위한 cli 를 설정해 둔다.

{
  ...
  "type": "module",
  "scripts": {
    "start": "PORT=3001 react-scripts start",
    "proxy": "node src/proxy.js"
  },
  ...
}

4.6) 프록시 구동

$ npm run proxy

express의 morgan 모듈을 설치했기 때문에 아래와 같이 구동 로그를 터미널에서 바로 알 수 있다.

결과화면)

위 결과와 같다면 이제 로컬서버의 9090 포트로 https://reqres.in 요청을 프록시해 줄 서버를 띄운것이다.

 

4.7) 리액트에서 프록시 사용

- 먼저 `https://reqres.in/api/users/2` 경로로 api 를 요청하는 버튼과

   `http://localhost:9090/api/users/2` 경로로 api 를 요청하는 버튼. 이렇게 2개의 버튼을 만들었다

- 각각의 버튼을 클릭 했을 때, api 요청에 대한 결과값을 화면에 바로 출력하는 부분까지 넣어놓았다

import { useCallback, useMemo, useState } from 'react';

function App() {
  const originUrl = useMemo(()=>'https://reqres.in/api/users/2', []);
  const proxyUrl = useMemo(()=>'http://localhost:9090/api/users/2', []);

  const [originResult, setOriginResult] = useState('');
  const [proxyResult, setProxyResult] = useState('');

  const getFetch = useCallback((url) => {
  return fetch(url)
    .then((response) => response.json())
    .then((data) => {
      return data;
    })
    .catch((error) => {
      throw new Error(error);
    });
  },[]);

  const onClickHandler = async (e) => {
    const { name } = e.target;
    if( name === "origin") {
      const result = await getFetch(originUrl);
      setOriginResult(JSON.stringify(result.data, null, 2));
    }else{
      const result = await getFetch(proxyUrl);
      setProxyResult(JSON.stringify(result.data, null, 2));
    }
  }
  return (
    <div className="App">
      <header className="App-header">
        <div style={{"border": "2px solid red", width:"50%"}}>
          <button name='origin' type='button' onClick={onClickHandler}>send to `{originUrl}`</button>
          <pre>{originResult && originResult}</pre>
        </div>
        <hr />
        <div style={{"border": "2px solid blue", width:"50%"}}>
          <button name='proxy' type='button' onClick={onClickHandler}>send to `{proxyUrl}`</button>
          <pre>{proxyResult && proxyResult}</pre>
        </div>
      </header>
    </div>
  );
}

export default App;

4.8) 리액트 구동하기

- 4.5)에서 "start": "PORT=3001 react-scripts start", 로 설정해두었기 때문에 3001번 포트로 구동됨을 유의한다

$ npm run start

결과화면)

4.9) 이제 버튼을 클릭해보면서 개발자 도구를 열어 네트워크를 확인해보자

- 첫번째 버튼 클릭

   첫번째 버튼의 경우 `https://reqres.in/api/users/2` 으로 요청이 되어있어서 정상 요청 되고 또 응답도 정상이다

 - 그러나 만약 reqres.in 서버에서 CORS 에 대한 허용이 없었다면 분명 아래와 같은 CORS에러를 만났을 것이다

4.10) 두번째 버튼 클릭

- 두번째 버튼의 경우도 정상적으로 요청되고 응답도 정상이다

- 그러나 위의 요청은 클라이언트가 아니라 프록시에서 보낸 요청이다

- 결과적으로 프록시 서버를 구축하면, 브라우저의 CORS 정책에 대한 에러를 회피할 수 있는

   가장 강력하고 직접적인 방법이라 할 수 있다.

5. `package.json`을 사용한 프록시 설정

5.1) 단일 프록시 설정 (도메인이 한 개인 경우)

{
  "name": "react",
  "version": "0.1.0",
  "dependencies": {...},
  "scripts": {...},
  "proxy": "https://www.myproxyserver.com"
}

5.2) 클라이언트 소스 코드에서 요청 시

...
const response = await fetch(`/api/my/request`);
// 실제로는 https://www.myproxyserver.com/api/my/request 로 요청하게 된다
response.json();
...

 

5.3) 프록시 여러개 설정 (도메인이 여러개인 경우)

{
  "name": "react",
  "version": "0.1.0",
  "dependencies": {...},
  "scripts": {...},
  "proxy": {
    "api": {
      "target": "https:www.myproxyserver.com"
    },
    "token": {
      "target": "http://localhost:9090"
    }
  }
}

5.4) 클라이언트 소스 코드에서 요청 시

await fetch(`/api/my/request`);
// https:www.myproxyserver.com/api/my/request

await fetch(`/token/tokenstring`);
// http://localhost:9090/token/tokenstring

 

6. CRA의 setupProxy.js

- 리액트의 CRA의 경우 http-proxy-middleware 모듈을 사용한 createProxyMiddleware 객체를 이용하여

  프록시를 사용할 수 있도록 기능이 내장되어 있다.

- 파일이 위치 할 곳은 `src/setupProxy.js` 이다

 

6.1) src/setupProxy.js

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

const apiProxyConfig = {
  API: {
    target: process.env.REACT_APP_API_DOMAIN || 'https://api/v1/domain.com',
    changeOrigin: true,
  },
  DOMAIN: {
    target: process.env.REACT_APP_DOMAIN || 'https://www.my.domain.com',
    changeOrigin: true,
  },
};

module.exports = function (app) {
  app.use('/api', createProxyMiddleware(apiProxyConfig.API));
  app.use('/domain', createProxyMiddleware(apiProxyConfig.DOMAIN));
};

6.2) .env.development

- 위의 (6.1) 에서 setupProxy.js 에서는 환경변수도사용하고 있는데, 리액트에서 환경변수를 사용하려면 반드시 접두어

   `REACT_APP_...` 으로 시작되어야 한다. 접두어를 사용하면 아래와 같다.

REACT_APP_DOMAIN=https://www.my.domain.com/
REACT_APP_API_DOMAIN=https://api/v1/my/server.com

6.3) 위의 (6.1)에서 설정된 내용을 클라이언트의 소스코드에서 사용

await fetch(`/api/my/request`);
// https://www.my.domain.com/api/my/request

await fetch(`/domain/querystring`);
// https://api/v1/my/server.com/my/domain

 

댓글