새해가 밝았다.. 회고를 작성하고 싶지만 최근 한 작업에 대한 기록을 남겨야겠다 싶을때 남기고싶기에 먼저 작성한다.. 최근 프로젝트에 OAuth 인증방식을 도입하기 위해 기존 인증 소스를 바꿔야 한다는 소리가 들렸다😵💫
사실 예전부터 프로젝트 인증방식에 대해서는 의문이 많았다.. (엑세스토큰 유효시간이 무려 24시간이다) 하지만 드디어 바꾼다고 하니 좋았다. FE 입장에서는 간단한 작업이라고 생각했다. 왜냐면 api 요청 함수만 살짝 손봐주면 될거라고 생각했으니까.
하지만 우리의 프로젝트 역시 예상대로 흘러가주지 않는다 api요청 함수가 무려 15개였다.. 🥹
export const get = () => {}
export const get_v2 = () => {}
export const get_v3 = () => {}
export const new_get = () => {}
export const post = () => {}
export const post_v2 = () => {}
export const new_post = () => {}
// ...
한숨이 저절로나왔다. 살펴보니 헤더 Content-Type
이 뭔지에 따라, request 형태가 body가 아닌 JSON으로 파싱 후 encodeURI를 한다던지 등등 방식에 따라서 전부 함수가 만들어져있었고 거기다 method별로 전부 제각각이었다.. 아니왜대체..
아무튼! 15개를 모두 바꿔야 한다는 소리다. 머리가아팠다 새로운 함수를 만들어서 일괄 적용하자니 그거 나름대로 골치아프다. 뭘 크게 변경하기도 싫었고 그냥 지금 사용중인 템플릿 그대로 해당 기능만 추가하고싶었다. 고민끝에 인증관련 로직을 수행하는 팩토리 함수를 만들어서 15개 전부에 적용시키는 방법이었다. 테스트도 팩토리 함수만 하면 되고 나머지 15개의 함수내부 소스역시 건들필요가 없으니 해당 방법으로 진행했다.
export const checkExpireHttpRequestFactory = (() => {
let isPending = false;
let subscriber = [];
return async (httpRequest) => {
try {
const result = await httpRequest();
return result;
} catch (err) {
const { status, errMsg } = getAxiosErr(err);
// 토큰이 없을때
if (!Token.get('access') || !Token.get('refresh')) {
redirectLoginPage();
}
// refresh 만료시
else if (status === 401 && errMsg?.includes('클라이언트가 인증이 안되서')) {
alert('인증이 만료되었습니다.');
Token.clearAll();
redirectLoginPage();
}
}
};
})();
먼저 예외처리에 refresh가 만료되었을때를 처리했고 발생 가능성은 없으나 localStorage.getItem()
의 string | null
타입가드를 위해서 토큰이 없을때를 처리했다. 물론 ! 어설션
을 사용할 수 있지만 혹시모르니 이와같이 처리했다.
이제 access를 다시 받아온 다음 해당요청 그대로 다시 요청을 보내야한다. 그렇기 때문에 해당 요청을 저장할 subscriber
가 필요했고 api함수 호출시마다 초기화되면 안되기때문에 클로저
로 유지되도록 했다.
export const checkExpireHttpRequestFactory = (() => {
let isPending = false;
let subscriber = [];
const getRequestPromise = (httpRequest) => new Promise((resolve) => {
subscriber.push(() => {
resolve(httpRequest());
});
});
return async (httpRequest) => {
try {
const result = await httpRequest();
return result;
} catch (err) {
if (checkExpireErr(err)) {
if (!isPending) {
isPending = true;
await getAccessToken();
isPending = false;
subscriber.forEach((resolveRequest) => resolveRequest());
subscriber = [];
}
return getRequestPromise(httpRequest);
} else {
throw err;
}
}
};
})();
엑세스 토큰을 받아오는 중이라면 getRequestPromise
함수를 실행해 subscriber
내부에 요청을 resolve
하는 함수를 넣어놓고 Promise
를 return한다. 이후 엑세스 토큰을 받아왔다면 subscriber를 순회해 resolve
함수를 실행시켜서 다시 요청을 발생시킨 뒤에 subscriber
를 초기화시킨다.
그럼 받는쪽에선 자연적으로 새로운 access로 await될거니 요청그대로 잘 실행된다.
export const get = (...param: Parameters<typeof get>) => {
return checkExpireHttpRequestFactory(() => GET(...param));
}
사용은 위와같이 했다. 기존 api함수는 upper케이스로 변경하고. 팩토리를 적용시킨 함수를 원래 함수의 이름으로 변경시켜서 그대로 사용했다. 사실 결과물을 보니 데이터타입에 자유로운 Javascript 여서 쉽게 작성한 로직이다 싶다..🥹