로스트아크 api호출에 쓰일 수 있는 axios instance를 만들어보자
https://developer-lostark.game.onstove.com/
에서 제공되는 api를 호출하기 위한 axios instance를 만들어 호출해보는 방법을 소개하고자 합니다.
(사이트 들어가서 api토큰을 만들어줘야 합니다)
곧 출시할 Loado (로스트아크 게임 관련 정보제공 사이트) 버전2를 개발하면서 겪은 내용과 경험을 바탕으로 작성합니다.
여기에 제가 작성할 내용은 큰 틀에서 보면 다음과 같습니다.
- Lostark api에 통신할 axios instance랑 서비스를 만들기 (Axios) <- 주 내용
- 자신이 개발한 BE를 중심으로 lostark api도 호출할 수 있도록 하는 방법 (Axios, craco, proxy)
LostArk api에 통신할 axios instance 생성
npm i axios를 했다는 가정하에
import axios from "axios";
const BASE_URL = "https://developer-lostark.game.onstove.com";
const axiosInstance = axios.create({
baseURL: BASE_URL,
withCredentials: true,
timeout: 30000,
});
axios를 import해오고 axios.create안에 필요한 정보들을 넣으면 기본적인 axiosInstance는 생성이 됩니다.
이 상태 그대로 export해도 되지만 저희가 따로 처리할 로직들이 몇가지 있습니다
- api를 호출하기 위한 토큰값 담기
- api 호출 전/후 성공, 실패에 대한 처리
- 로스트아크 api는 기본적으로 1분에 100번의 요청만 받을 수 있기에 429 라는 요청 에러를 받을 경우 별도의 처리 해주기
(예를 들어 로스트아크 대표적인 편의기능 제공 사이트 중 하나인 icepeng에 보시면 아이템을 경매장에서 검색하다가 100개가 넘어가면 일정 시간동안 기다리고 다시 요청을 처리합니다)
위 2가지 방법은 interceptors를 이용해 구현해봅시다
const handleRequest = (config: any) => {
if (
config.url?.endsWith("markets/items") ||
config.url?.endsWith("auctions/items")
) {
Object.assign(config.headers, {
Authorization: `bearer ${process.env.REACT_APP_SMILEGATE_TOKEN}`,
});
}
return config;
};
const handleRequestError = (error: any) => {
return Promise.reject(error);
};
axiosInstance.interceptors.request.use(handleRequest, handleRequestError);
이렇게 request가 날라가기 전에 handleRequest를 통해 header에 추가적인 값을 넣어줄 수 있으며 혹시나 잘못 호출하고자 한다면 handleRequestError로 Promise.reject를 에러처리를 할 수 있습니다
response interceptor는 다음과 같이 구현해줍니다
const handleResponseSuccess = (response: any) => {
if (response.status === 200) {
return response.data;
} else if (response.status === 429) {
return Promise.reject(new Error("Request Limit"));
}
return response;
};
const handleResponseError = (error: any) => {
const { response } = error;
if (response.status === 429) {
return Promise.reject(new Error("Request Limit"));
}
if (response && response.data) {
return Promise.reject(response.data);
}
return Promise.reject(new Error("error"));
};
axiosInstance.interceptors.response.use(handleResponseSuccess, handleResponseError);
export default axiosInstance;
즉, 429라는 response code를 받을 경우 "Request Limit"라는 에러 메시지를 뱉어내게 하고 그 외의 케이스에선 서버에서 제공해주는 커스텀 에러나 단순히 "error"메시지를 뱉어내게끔 했습니다. 이후 axiosInstance를 export 해줍니다.
이렇게 하면 기본적인 axiosInstance를 만들 수 있으며 로직을 더 추가하거나 수정하고자 할 경우 interceptors에 들어가는 함수들을 변경해주면 됩니다.
이제 이를 호출할 기본적인 서비스를 만들어봅니다. 이 서비스에선 axios instance를 이용해 get, post요청을 날리고 429라는 "Too many requests" 에러를 받을 경우 1분 정도 기다렸다가 재호출하는 로직을 작성합니다. 재호출하는 횟수는 최대 2번으로 제한합니다.
import axiosInstance from "./AxiosInstance";
const RPS = 60 * 1020;
const MAX_RETCNT = 2;
class BaseService {
static requestMethod = {
get: axiosInstance.get,
post: axiosInstance.post,
};
static async request({
method = "get",
url,
data,
retryCnt = 0,
}: {
method: "get" | "post";
url: string;
data: any;
retryCnt?: number;
}) {
try {
const res = await this.requestMethod[method](url, data); // get이면 data를 제거해도 되지만 여기에선 그대로 냅둠
return res;
} catch (error: unknown) {
return this.handleError(error, method, url, data, retryCnt);
}
}
static async handleError(
error: unknown,
method: "get" | "post",
url: string,
data: any,
retryCnt: number
): Promise<any> {
if (error instanceof Error) {
const errMsg = error.message;
if (errMsg === "Request Limit" && MAX_RETCNT > retryCnt) {
await new Promise((res) => setTimeout(res, RPS));
const res = await this.request({
method,
url,
data,
retryCnt: retryCnt + 1,
});
return res;
} else if (errMsg === "Request Limit" && MAX_RETCNT <= retryCnt) {
// 에러 로그 생성/저장
const newErrorRecord = new ErrorLogModel({
message:
"came to Request Limit with MAX_RETCNT=${MAX_RETCNT} exceeded",
metadata: {
data,
method,
url,
},
});
await newErrorRecord.save();
}
}
return null;
}
}
export default BaseService;
소스가 조금 길어보이지만 차근차근 살펴보면
- BaseService 클래스의 request 함수를 호출할 경우 사전에 생성한 axiosInstance에 get 또는 post 요청을 날릴 수 있습니다.
에러가 발생하면 handleError함수로 이동합니다 - handleError함수는 request에서 에러 발생 시 실행되는 함수이며 error message로 "Request Limit"를 받을 경우 await new Promise를 통해 대략 1분 (RPS변수) 기다립니다. 이후 요청했던 request를 재호출하는 작업을 거치며 이를 최대 2회만 합니다.
2회 호출했는데도 에러가 발생하면 error log 테이블에 로그를 남기고 끝납니다. - Request Limit 에러가 아닐 경우 null만 리턴하니 별다른 작업을 하지 않습니다.
이를 실제로 쓰고자 한다면 다음과 같이 쓰면 됩니다
response는 다음과 같이 올겁니다
자신이 개발한 BE를 메인으로 하고 로스트아크 api를 우회적으로 호출하는 방법
Axios instance를 조금 바꿔줍니다
const BASE_URL = process.env.NODE_ENV === 'development' ? '' : '운영 BE 주소'
const axiosInstance = axios.create({
baseURL: BASE_URL,
withCredentials: true,
timeout: 30000,
});
제 프로젝트는 create react app 기반으로 만들어졌기에 craco.config.js에 proxy관련 로직을 넣어줍니다
const apiProxyTarget = 'https://developer-lostark.game.onstove.com';
module.exports = {
devServer: {
port: 8000,
proxy: [
{
context: ['/lostark/markets'],
target: apiProxyTarget,
changeOrigin: true,
pathRewrite: { '^/lostark/*': '' },
},
{
context: ['/lostark/auctions'],
target: apiProxyTarget,
changeOrigin: true,
pathRewrite: { '^/lostark/*': '' },
},
{
context: ['/api'],
target: 'http://localhost:8080',
},
],
}
}
코드가 조금 헷갈릴 수 있지만 자세히 살펴보면 다음과 같은 기능을 합니다
- http://localhost:8000/lostark/markets/items를 호출할 경우 "/lostark/markets"라는 문자열이 포함되어 있기에 localhost가 아닌 https://developer-lostark.game.onstove.com으로 호출하면서 "lostark"라는 문자열을 빈 문자열로 변경합니다
즉, http://localhost:8000/lostark/markets/items로 호출할 경우
https://developer-lostark.game.onstove.com/markets/items로 호출하게 변경됩니다
즉, 이렇게 될 경우 우리의 BE api endpoint엔 "/lostark/markets" 문자열이 있으면 안되겠죠 - "/lostark/auctions"라는 문자열이 포함되어 있을 경우 마찬가지로 변경됩니다
- 그 외에 http://localhost:8000/api 일 경우 http://localhost:8080/api로 변경되어 be에 호출됩니다 (이렇게 proxy작업을 하지 않을 경우 포트가 다르기 때문에 CORS에러가 발생할 겁니다, BE에 아무 작업을 하지 않았을 경우)