본문 바로가기

Development/Web

Open API 사용 시 발생하는 CORS 이슈, 원인과 해결 방법 정리

들어가며

오랜만에 Frontend를 연습할 겸 공공데이터포털 사이트에서 오픈 API를 사용해보았다. 그런데, CORS 이슈가 발생하는 탓에 결국에는 Node.js(express)로 Proxy 서버를 구축하여 개발하였다.

🔗 소스코드 : https://github.com/choewy/medical-maps-api-proxy-ssr

CORS

CORS(Cross-Origin Resource Sharing)을 해석해보면 교차 출처 리소스 공유라고 하는데, 도대체 이게 무슨 말인지 알 수 없었다. 폭풍 구글링하고, 구글링한 정보를 취합해서 간단하게 설명하자면 다음과 같다.


📌 API 개념 요약

API가 무엇인지 생각하면 CORS를 이해하는데 도움이 될 것이라고 생각한다. API를 간략히 설명하자면 클라이언트와 서버가 소통하는 방식이라고 할 수 있다. 이를테면, 클라이언트(localhost:3000)가 서버(api.com:8080/data)에게 data를 요청하면 서버는 응답으로 클라이언트에게 데이터를 전달해준다.


📌 Origin의 의미

CORS에서 의미하는 출처는 서버와 클라이언트의 URL이라고 할 수 있다. 즉, 위의 예시에서 localhost:3000는 클라이언트의 출처이고 api.com:8080은 서버의 출처가 되는 셈이다(조금 더 자세히 설명하자면, 출처에 해당하는 URL은 {protocol}://{host}:{port}를 의미한다).


이와 같이 서로 다른 출처 간에 어떠한 리소스를 전달하는 방식을 제어하는 체제가 CORS이며, 정책을 위반하여 서로 다른 출처 간 리소스를 공유하려고 시도하면 보안 상의 이유로 브라우저가 이를 차단한다. 예를 들어, Frontend에서 주로 사용하는 axios, fetch, XMLHttpRequest API는 동일 출처 정책을 따르는데, 이와 같은 API를 사용하여 다른 출처에게 리소스를 요청하는 경우 CORS가 발생한다.

해결 방법

그렇다면 어떻게 CORS 이슈를 해결할 수 있을까? 다시 말하자면, 완벽한 '해결'이라고 하기 보다는 CORS 정책을 우회한다고 하는 것이 맞을 지도 모르겠다. 위에서 CORS 정책을 위반 시 보안 상의 이유로 브라우저가 차단한다고 언급하였다. 그렇다면 브라우저를 통해서 리소스를 공유하는게 아니라면, 이러한 문제를 쉽게 해결할 수 있을 것이다(즉, 서버로 API를 요청할 수 있도록 해야한다는 의미). 검색한 자료에 따르면 이를 해결할 수 있는 방법으로는 크게 4가지 정도 있는 듯 하다.

Backend : Proxy Server 이용 또는 구축

이 방법은 내가 CORS에 대한 개략적인 개념정도만 이해하고 바로 서버를 구현하였던 방법이다.

(본문 중에서) ... 오랜만에 Frontend를 연습할 겸 공공데이터포털 사이트에서 오픈 API를 사용해보았다. 그런데, CORS 이슈가 발생하는 결국에는 Node.js(express)로 Proxy 서버를 구축하여 개발하였다.

'use strict';

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

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(`${__dirname}/src/public`));

const apiUrl = "공공데이터 API 요청 URL";
const serviceKey = "API 서비스 Key";

const getAPI = async (params) => {
    const {pageNo, numOfRows} = params;

    let queryString = `serviceKey=${serviceKey}`;
    queryString += `&pageNo=${pageNo}`;
    queryString += `&numOfRows=${numOfRows}`;

    try {
        return await axios.get(`${url}?${queryString}`);
    } catch (err) {
        console.error("API 요청 중 오류 발생");
        return { success: false };
    };
};

app.get('/api', async (req, res) => {
    const { pageNo, numOfRows } = req.query;
    getAPI({ pageNo, numOfRows })
        .then((response) => {
            const { data: { response: { body: { items: { item } } } } } = response;
            res.json({ success: true, rows: item });
        })
        .catch((err) => {
            console.error(error);
            res.json({ success: false, error: err });
        });
});

즉, 클라이언트는 서버에게 API를 요청하면, 서버는 공공데이터 API 데이터를 공공데이터 서버로부터 데이터를 받아오고, 데이터를 클라이언트에게 응답으로 보내주는 방식이다.

Backend : Header의 Access-Control-Allow-Origin 설정

express 서버가 localhost:5000으로 실행되고 있고, React의 로컬 서버는 localhost:3000으로 실행되고 있다고 가정해보자. 이와 같은 경우에도 마찬가지로 서로 다른 출처 간 리소스 공유 정책을 위반하게 되는데, 아래와 같이 서버에서 응답헤더에 React의 로컬 서버 도메인을 허용해주면 문제를 해결할 수 있다.

'use strict';

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

const domain = "localhost:3000";
app.get('/api', (req, res) => {
    res.header("Access-Control-Allow-Origin", domain);
    res.send(data);
});

때로는 도메인 대신에 *을 입력하기도 하는데, 이는 모든 출처에서 오는 요청을 허용한다는 뜻이다. 즉, 알 수 없는 출처에서의 모든 요청을 허용하므로 *은 사용하지 않도록 하고, 되도록 허용하고자 하는 도메인을 직접 입력하는 것이 좋다.

Backend : CORS 미들웨어 사용

위의 방법과 마찬가지인데, cors 미들웨어를 사용하면 더 간단하게 Header의 Access-Control-Allow-Origin을 설정할 수 있다.

'use strict';

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

const domain = "localhost:3000";
const options = { origin: domain };

app.use(cors(options));

이때 app.use(cors())로 미들웨어를 사용하도록 하는 경우에는 위에서의 *와 같이 모든 출처에서 오는 요청을 허용하게 되므로 이 또한 지양하도록 한다.

Frontend : http-proxy-middleware 라이브러리 사용

서버에서 CORS를 해결하지 못하는 상황이 발생(공공 API를 사용하는 등)할 수도 있는데, 이와 같은 경우에는 클라이언트에서 직접 설정해주어야 한다. 이를 해결하기 위한 대표적인 라이브러리가 바로 http-proxy-middleware인데, React로 예를 들어보자면 다음과 같다.

/* ./src/setupProxy.js */

'use strict';

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

module.exports = function (app) {
    app.use(
        createProxyMiddleware(['/api1', '/api2'], {
            target: "http://localhost:5000",
            changeOrigin: true
        })
    );
};

(본문 중에서) ... express 서버가 localhost:5000으로 실행되고 있고, React의 로컬 서버는 localhost:3000으로 실행되고 있다고 가정해보자.

['/api1', '/api2']는 요청할 경로이고, target은 프록시 서버 주소(위의 예시에서 express 서버의 주소)이며, changeOrigin은 target에 해당하는 서버의 Header를 변경해주는 옵션이다. 즉, localhost:3000/api1에서 localhost:5000/api1로 API를 요청할 때, 요청 출처인 localhost:3000을 localhost:5000으로 바꾸어 웹 브라우저를 속이는 방식이라고 할 수 있다.

마치며

이렇게 CORS에 대한 간단한 개념부터 해결 방법까지 정리해보았다. 매번 코딩하면서 느끼는 거지만, 어제 작성한 코드를 오늘 보면 정말 마음에 들지 않을 때가 있다. 분명 어제 코드를 작성하고 검토하면서 해당 코드가 만족하여 커밋까지 했는데 말이다(인간의 욕심은 끝이 없어서 그런가...). 아마 위의 GitHub 링크에 남겨놓은 코드도 조만간 뜯어고칠 거 같다.