알라딘 Open API CORS 이슈 해결

알라딘 open API를 사용해서 도서 검색 기능을 구현하다가 CORS 이슈를 맞닥뜨렸다. 해결과정과 함께 공부한 내용을 간단하게 정리해보려고 한다.

일단 상황은 이랬다. 도서 검색을 위해 알라딘 서버로 요청을 보내면 No 'Access-Control-Allow-Origin' header is present on the requested resource. 에러가 발생하는 것이다.

cors

Access to XMLHttpRequest at [알라딘 open api] from origin 'http://localhost:3010' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

자바스크립트는 보안상의 이유로 클라이언트와 서버의 출처가 같은 경우에만 (=같은 도메인 주소를 가진 경우에만) ajax를 통해 데이터를 요청/응답 할 수 있다. 이러한 보안 규정을 SOP (Same-Origin Policy, 동일 출처 정책)라고 한다.

SOP에 의한 제약을 해결하기 위해 등장한 것이 바로 CORS다.

CORS는 Cross-Origin Resource Sharing의 약자로 도메인이 다르더라도 데이터를 공유할 수 있게 해주는 메커니즘이다. 웹 어플리케이션은 다른 도메인(출처)로 데이터를 요청할때 cross-origin HTTP 요청을 실행하게된다.

서버단에서는 cross-origin HTTP 요청을 처리하기 위해 아래와 같은 추가적인 HTTP header를 사용한다. 클라이언트와 서버의 도메인 다를 경우 클라이언트에게 서버측 응답에 접근할 수 있는 권한을 부여하는 것이다.

Access-Control-Allow-Origin
Access-Control-Allow-Method
Access-Control-Max-Age
Access-Control-Allow-Headers

결국 클라이언트의 도메인(localhost:3010)과 알라딘 open api 서버의 도메인이 달랐기 때문에 SOP에 의해 요청이 거부된 것이고, 알라딘 서버측에서 특정 도메인 혹은 모든 도메인을 허용하도록 CORS 설정을 해주지 않는 이상 클라이언트단에서 해결할 수 있는 방법을 찾아야 했다.


1. JSONP

알라딘 api 사용시 CORS 이슈를 피하기위해 JSONP를 사용했다는 블로그 글을 참고해서 JSONP를 써보기로 했다.

JSONP? 생소하다.

JSONP란

JSON with Padding 의 약자로 2009년 CORS가 나오기 전까지 다른 도메인에 비동기 요청을 하기 위해 사용되던 방법이다. HTML의 script 요소로 요청되는 호출에는 보안상 정책이 적용되지 않는점을 이용, SOP를 피해서 다른 도메인에 비동기 요청을 보내고 응답을 받는 것이다.

JSONP를 사용해서 데이터 통신을 하려면

HTML 파일내에 있는 <script> 태그의 src 속성에 요청 url을 입력하고

<script src="http://www.aladin.co.kr"></script>

응답받은 데이터를 담을 callback 함수를 만들어서 window 객체의 프로퍼티로 할당해두면 된다.

window.myCallback = function(data) {
	console.log(data)
}

그러면 서버측에서는 응답 JSON을 클라이언트가 정의해둔 callback 함수로 감싸서 보내준다.

myCallback({ data: 'hello world!' })

JSONP 방식으로 사용하려면 두 가지 조건을 충족해야 한다.

  1. 서버에서 JSON 형태로 데이터를 응답해줘야 한다.
  2. 위 script 태그로 받아온 JSON 데이터를 브라우저 어딘가에 저장해야 한다.

JSONP는 보안상 이슈로 더이상 사용되지 않는 방법이라 그런지 구글링을 해도 구현 방법을 찾기 쉽지 않았다. 직접 구현하는것보다 현재 프로젝트 환경(리액트 + 타입스크립트)에서 사용해볼만한 라이브러리를 찾아서 사용해보는게 났다고 판단했다.

여러 라이브러리를 사용해보고 (axios-jsonp-pro, fetch-jsonp) JSONP 요청/응답을 처리하는 커스텀 훅도 찾아서 적용해 보았지만 매번 CORB (Cross-Origin Read Blocking) 에러가 발생했다.

Cross-Origin Read Blocking (CORB) blocked cross-origin response [알라딘 open api] with MIME type application/json. See https://www.chromestatus.com/feature/5629709824032768 for more details.

CORB란 CORS와 마찬가지로 웹사이트의 보안을 위한 기능으로 <script> 또는 <img> 태그 안에서 출처가 다른 도메인(cross-origin)으로 요청을 보내고 응답을 받은 경우 해당 응답을 차단하는 기능이다.

응답의 형식(Content-Type)이 HTML 또는 XML 또는 JSON인 경우 해당 응답은 CORB에 의해 빈 응답으로 대체된다.

CORB에 대한 자세한 내용은 여기서 확인 가능

결론적으로 알라딘 api 응답 포맷은 XML 또는 JSON이기 때문에 CORB를 피해갈 수 없었다.


2. Next.js 서버를 proxy 서버로 사용

두번째로 시도한 방법은 Next.js 서버를 custom해서 proxy server로 사용하는 방법이다.

기본적으로 CORS는 브라우저-서버간에 발생하는 이슈이고 서버끼리 통신할때는 발생하지 않는다는 점에 착안했다.

의도했던 플로우는 클라이언트에서 알라딘 서버로 요청을 보내면 Next.js가 해당 요청을 가로채서 대신 알라딘 서버에 요청을 보내고, 다시 알라딘 서버로부터 오는 응답을 받아서 클라이언트측에 보내주는 것이었다.

Next.js를 proxy 서버로 사용할 경우 도서 검색 플로우는 다음과 같이 나타낼 수 있겠다.

next.js proxy server

서버 리소스가 추가적으로 필요하다는 단점은 있지만 CORS가 발생하지 않을 것이기 때문에 시도해볼만 하다고 판단했고.

하지만 Next.js 문서, Next.js github 예시, 블로그 글 등을 참고해보니 내가 원하는 동작을 구현하는데 적절한 방법이 아닌 것 같았다.

Next.js 서버를 통한 proxying은 localhost 같은 base URL을 제외한 나머지 path (query 부분)을 기준으로 외부 api 서버로 라우팅을 해주는 것에 그쳤기 때문이다.


3. express 서버 활용

결국 백엔드 서버를 최소한으로 구성해놓고 백엔드 서버를 거쳐서 요청/응답이 이루어지도록 했다.

express proxy

백엔드 express 서버에 요청을 보내면 백엔드 서버가 알라딘 서버에 요청을 보내고 응답을 받아서 클라이언트에 보내준다.

구현한 코드는 다음과 같다.


React

const getResults = useCallback(async () => {
  try {
    const res = await axios.get(
      `${process.env.NEXT_PUBLIC_DEV_API}/search/book?searchQuery=${searchQuery}`
    );
    console.log(res);
  } catch (err) {
    console.log(err);
  }
}, [searchQuery]);

express 서버

const express = require("express");
const cors = require("cors");
const morgan = require("morgan");
const axios = require("axios");

const app = express();

app.use(morgan("dev"));
app.use(cors({ origin: true, credentials: true })); // cors 허용

app.use(express.json());

app.get("/search/book", async (req, res) => {
  // 클라이언트가 보낸 쿼리값을 받아서
  const { searchQuery } = req.query;
  const url = `http://www.aladin.co.kr/~${encodeURIComponent(searchQuery)}`;

  try {
    // 알라딘 서버에 검색 요청
    const {
      data: { item: result },
    } = await axios.get(url);
    // 응답을 받으면 클라이언트에게 전달
    if (result) res.status(200).send(result);
  } catch (err) {
    console.log(err);
  }
});

app.listen(3020, () => {
  console.log("Listening to port 3020...");
});

크롬 콘솔창의 출력 내용이다. 데이터 받아오기 성공!

data


문제를 해결하려고 반나절 이상 매달린 것 같다. open api를 사용하는데 이런식의 CORS 이슈라니..
그래도 덕분에 SOP, CORS, JSONP에 대해 공부하고 정리할 수 있는 기회였다.
백엔드 서버를 거쳐서 요청을 보내는게 최선의 방법인지는 잘 모르겠지만 문제도 해결 했고.
아, 나중에 proxy에 대해서 제대로 알아봐야겠다. forward proxy, reverse proxy 같은 키워드들이 있던데.



Refecrence


@Reese
Sin Prosa Sin Pausa