백엔드와 API 통신을 하다보면 고려해야할 사항과 의사결정 분기가 다양하다.
이러한 다양한 의사결정 분기 때문에, axios 모듈을 사용하다보면 몇 가지 불편함을 직면할 수 있다.
- 의사결정 분기가 많아짐에 따라 우후죽순으로 생겨나는 instance와 interceptor들.
- 복잡성 증가 및 관리 난이도 증가.
- 중앙화 되어있지 않아, 확장성이나 유지보수의 cost 증가.
- FE 입문자의 경우, Access token 관리에 어려움을 겪음.
- 토큰 만료 로직구현에 어려움.
- React-cookie와 같은 추가적인 의존성 모듈을 사용하게 됨.
이전 블로그 글에 작성했던 것처럼 이러한 불편함을 많은 사람들이 느끼고 있고, 간단하게 쓸 수 있는 커스텀 모듈을 제작해 npm에 배포해보고자 했다!
현재 Access Token을 cookie로 관리하는 과정에서, react-cookie라는 의존성 모듈을 사용하고 있다.
잠깐-
의존성 모듈을 왜 최소화 해야하는가?
대략적으로 알고는 있었지만, 이번 기회에 정확히 글로 정리해보고자 했다.
1. 유지보수 난이도 증가
의존성이 많아질수록, 해당 라이브러리나 패키지들의 업데이트를 따라가고 버젼을 관리하는 것이 어려워진다. 업데이트 과정에서 하위 패키지의 버젼 이슈가 발생할 경우 코드를 리팩터링하는 과정이 추가되어야한다.
2. 보안 이슈 발생 가능성 증가
특정 라이브러리의 보안 이슈가 발생하여 깃허브로부터 경고 메일을 받은 경험이 있을 것이다. 각각의 의존성 모듈은 잠재적인 보안 취약점을 가지고 있기 때문에, 의존성 모듈이 많을수록 보안 이슈가 발생할 가능성은 높아진다.
3. 번들 크기 증가
의존성 모듈이 많을수록 번들 크기 또한 증가한다. 웹 애플리케이션 초창기 로딩 속도나 빌드속도의 저하 원인이 된다.
4. 비효율적인 트리 쉐이킹
최신 빌드 도구들의 경우, 의존성 모듈에서 사용하지 않는 코드를 제거한다. 의존성 모듈이 트리 쉐이킹에 적합하지 않을 경우, 불필요한 코드가 번들에 포함되어 번들 크기에 악영향을 미칠 수 있다.
이유를 알았다면, Cookie를 담당하는 react-cookie 의존성 모듈을 제거해보자!
import { Cookies } from 'react-cookies';
export 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();
}
현재 Axios의 #cookie field에 주입되는 Cookies 객체는 react-cookie의 의존성 모듈이다. 가내수공업으로 직접 구현해 의존성을 제거하는 것이 우리의 목표! 🥳
이전에 react-cookie에서 사용하던 기능은 getter와 setter이다.
해당 부분을 직접 구현해보자!
설명이 길어져 Cookie 객체를 만드는 부분은 아래의 블로그 글로 정리해두었다! :)
export class Cookie {
get(name: getterNameProps): getterReturn {
const cookies = document.cookie.split('; ');
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie.split('=');
if (decodeURIComponent(cookieName) === name)
return decodeURIComponent(cookieValue);
}
return null;
}
set(
name: setterNameProps,
value: setterValueProps,
options: setterOptionProps = {}
): setterReturn {
let newCookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
if (options.path) newCookie += `path=${options.path};`;
if (options.domain) newCookie += `domain=${options.domain};`;
if (options.secure) newCookie += `secure;`;
if (options.sameSite) newCookie += `SameSite=${options.sameSite};`;
if (options.expires)
if (Object.prototype.toString.call(options.expires) === '[object Date]')
newCookie += `expires=${options.expires.toUTCString()};`;
document.cookie = newCookie;
}
}
Access Token이 만료되었을 때, RefreshToken으로 토큰을 재발급받고, API를 다시 요청하는 Middleware를 추가해보자!
우선, 백엔드 서버에서 만료된 AccessToken을 받았을 경우, 1002번 Code를 전달받는다 가정하자.
const TOKEN = Object.freeze({ EXPIRE: 1002, }); export const STATUS_CODE = Object.freeze({ TOKEN, });
문제가 발생할 경우, 이에 대한 처리를 가로챌 middleWare를 구현하기위해 axios의 interceptor를 사용한다.
response의 interceptor에 bind를 해준다. Class 객체에서 코드의 확장성과 유지보수를 위해 각 인자에 들어갈 부분들을 메서드로 분리해준 뒤, 바인딩을 해준다.
response 받는 statusCode가 1002번(expired accessToken)일 경우 이를 처리하기 위한 로직을 메서드 내부에 추가해준다. 원하는 responseError가 발생했을 때, 이를 핸들링하기 위한 메서드는 전부 해당 메서드 내부에서 관리하도록 한다.
위의 구조의 장점을 첨언하자면, MiddleWare의 수가 많아져 코드가 더러워질 때, key에 statusCode, value에 메서드를 넣은 객체를 이용해 시간복잡도 O(1)로 원하는 middleWare를 실행할 수 있다.
this.#MIDDLEWARE_MAP[status]
이제 토큰을 재발급받아보자
새로운 토큰을 정해진 API로 발급 받고, 정상적으로 발급이 된 경우, accessToken이 만료되어 실패한 요청을 보내기 위해, 새로운 config를 생성한다.
이후 인스턴스에 담아 return하여 실패한 API로 다시 요청을 보내도록 한다. :)
npm에 로그인 후 배포해주자!
오늘 아침 4시 반에 배포한 뒤, 자고 일어나보니 다운로드 수가 100명을 넘겼다! 신기방기!! 🤗
아주 재밌는 경험이었다!