프로젝트가 커지면 axios를 요청하는 코드도 많아진다.
문제는 이 코드가 굉장히 길고 반복되는 코드라는 것이다.
이에 대한 해결책인 instance와 interceptor를 잘 활용하면 이러한 문제를 어느정도 해결할 수 있다.
하지만 BE의 API가 많아질수록 instance와 interceptor도 한계를 갖는다.
API가 많아질수록 instance와 interceptor만으로는 감당하기 어려운 의사결정의 분기를 갖기 때문이다.
특히, react-query의 useMutation 같이 사용하게 될 경우 error에 대한 책임은 필연적으로 react-query에게 분산된다.
담당하는 에러의 분야는 다르겠지만 결국 useMutation의 onError의 코드를 정상적으로 실행시키려면 axios의 에러 핸들링 scope에서 throw enw Error를 생성해야 한다.
물론, Error의 핸들링의 분야를 Axios, React-query, React Errorboundary로 나눠 각 에러의 성격에 따른 에러 처리책임의 세분화는 충분히 좋은 전략이다.
하지만 FE의 입장에서 User의 interaction에 의해 발생하는 HTTP Protocol Error에 대한 처리는 re-direct와 Error-Message. 즉, View의 범위를 크게 벗어나지 않는다. 따라서, 에러 핸들링 책임은 react-query. 즉, View 컴포넌트에 위임하는 것이 좋다고 판단했다.
axios에서 제공하는 instance와 interceptor만으로 코드를 구현하는 방식은 아래에 제시하는 의사결정 분기에따라 instance를 직접 하드코딩 해야하고, 작성한 코드는 해당 프로젝트에 의존성이 발생해 프로젝트 간의 재사용성이 떨어진다고 판단해 고민을 시작하게 되었다.
다른 코드들을 참고하며 해결책을 모색해보았지만, 대부분 요청의 특징에 따라 인스턴스 생성 및 옵션을 반복적으로 작성하거나, 토큰이 필요 없는 요청이더라도 accessToken을 헤더에 담아둔 인스턴스를 재사용하는 방식이었다.
결국 해당 axios instance가 보내는 config는 어떻게 되는가?
라는 질문에 따라 작성해야하는 axios 요청코드가 크게 바뀐다는 것이다.
instance를 계속해서 생성하다 재사용성의 한계를 느끼고 하드코딩과 큰 차이가 없다고 생각하여 Axios 객체를 생성해 의사결정 분기를 객체에 녹여내기로 했다.
가장 먼저 생성자를 이용해 axios 객체를 생성할 때, 받는 유일한 매개변수는 isAuthReq. 즉, 요청의 인증 필요 여부이다.
default를 false로 설정하였으며, true를 넘겨줄 경우 헤더에 cookie값에 들어있는 accessToken을 instance 생성 시 config의 headers에 추가하는 로직을 구현하였다.
class Axios {
#instance;
#auth;
#cookie;
/**
* @param {boolean} isAuthReq
*/
constructor(isAuthReq = false) {
this.#instance = axios.create({
baseURL: `${process.env.REACT_APP_API_BASE_ROUTE}`,
});
this.#auth = isAuthReq;
this.#cookie = new Cookies();
this.#setInterceptor();
}
/* Incerceptor */
#setInterceptor() {
this.#instance.interceptors.request.use(
this.#reqMiddleWare.bind(this),
this.#reqOnError.bind(this)
);
this.#instance.interceptors.response.use(
this.#resMiddleWare.bind(this),
this.#resOnError.bind(this)
);
}
/* Req */
#reqMiddleWare(config) {
let newConfig = config;
if (this.#auth) newConfig = this.#setAuthReq(newConfig);
return newConfig;
}
#setAuthReq(config) {
const { headers } = config;
const newConfig = {
...config,
headers: {
...headers,
Authorization: `${this.#cookie.get(KEY.ACCESS_TOKEN)}`,
},
};
return newConfig;
}
}
res의 Cookie 처리, req의 Token 필요성에 대한 분기를 해결하면 경우의 수는 크게 줄어든다.
각 메서드에 대한 분기를 처리해준 뒤 아래의 경우를 해결해주면 대략적인 아웃라인은 끝나게 된다.
- GET요청에 대한
URL/URI
처리.- URI인 경우
params/query
를 처리
/**
* @param {string} endPoint
*/
get(endPoint) {
this.#instance({
method: METHOD.GET,
url: endPoint,
});
}
/**
* @param {string} endPoint
* @param {string} query
*/
getByQuery(endPoint, query) {
this.#instance({
method: METHOD.GET,
url: endPoint,
params: {
...query,
},
});
}
/**
* @param {string} endPoint
* @param {string} query
*/
getByParams(endPoint, params) {
this.#instance({
method: METHOD.GET,
url: `${endPoint}/${params}`,
});
}
/**
* @param {string} endPoint
* @param {object} data
*/
post(endPoint, data) {
this.#instance({
method: METHOD.POST,
url: `${endPoint}`,
data,
});
}
/**
* @param {string} endPoint
* @param {number} id
* @param {object} data
*/
put(endPoint, id, data) {
this.#instance({
method: METHOD.PUT,
url: `${endPoint}/${id}`,
data,
});
}
/**
* @param {string} endPoint
* @param {number} id
*/
delete(endPoint, id) {
this.#instance({
method: METHOD.DELETE,
url: `${endPoint}/${id}`,
});
}
}
res의 statusCode가 401(토큰 만료)일 경우, 자체적으로 Token 발급을 요청하는 코드를 추가해야 한다.
우선, 해당 코드를 보일러플레이트로 사용해 프로젝트를 진행해보고 코드를 개선해 나아가야겠다.