이번 포스팅은 "덕풀" 프로젝트를 진행하면서 애플리케이션 유저 상태와 인증 로직을 관리한 방법에 대해 이야기 해보고자 한다.
모달 전역 관리에 대해서 소개했던 지난 포스팅에서는 뷰와 더 밀접하고, 애플리케이션 전체 뷰 뿐만 아니라 서브 뷰에서도 사용이 용이하기 때문에 Context API를 사용했다고 소개했지만, 유저 상태 및 인증 로직은 애플리케이션 전체에서 단 하나만 존재해야하는 앱 상태 그 자체이고, View의 라이프 사이클과 무관하게 애플리케이션 자체의 라이프 사이클을 따라가는 영속적인 상태이기 때문에 전역관리 라이브러리인 Jotai를 사용하여 구현했다.
일단 위에서 언급했듯이 전역 상태 라이브러리를 통해서 유저 상태와 인증 로직을 관리한 이유에 대해 설명했으니, 들어가기에 앞서 전역 상태 라이브러리로 Jotai를 사용한 이유에 대해서 짚어보자.
전역 상태 관리 라이브러리로 Jotai를 채택한 이유
✅ react 친화적인가? - useAtom을 통해 react의 인터페이스와 유사한 방법으로 사용 가능
✅ 많은 보일러플레이트를 필요로 하지는 않는가? - action, reducer등의 정의 필요없이 데이터 보관의 기본단위로 atom을 이용하여 전역으로 관리 할 상태를 간단하게 생성
✅ 여러 확장 가능한 유틸함수를 제공하는가? - JWT를 사용하는 애플리케이션 특성상 로컬스토리지를 필요로 하는데 atomWithStorage와 같은 유틸함수를 제공
✅ 비동기 로직을 작성할 수 있는가? - 인증 로직을 모두 하나의 atom에서 관리하기 위해서 로그인, 로그아웃, 회원 탈퇴 및 토큰 재발급과 같은 비동기 작업을 수행해야하는데, Jotai는 비동기 로직을 사용 가능
✅ 토큰으로 부터 유저의 인증 상태를 끌어낼 수 있는가? - primitive atom을 통해서 파생된 derived atom을 생성 가능하기 때문에, 토큰을 primitive atom으로 관리하고, 로그인 유무, 유저 아이디와 같은 상태를 토큰으로부터 파생시켜 관리 가능
유저 토큰 관리
가장 먼저 derived atom을 끌어내기 위해서 로컬스토리지에 저장된 토큰 값을 통해서 primitive atom을 정의했다.
const tokenBaseAtom = atomWithStorage<string | null>(
TOKEN_KEY,
localStorage.getItem(TOKEN_KEY)
? JSON.parse(localStorage.getItem(TOKEN_KEY) as string)
: null,
);
Jotai에서 유틸로 제공하는 atomWithStorage를 통해 로컬스토리지에 토큰 값이 있다면 tokenBaseAtom에 토큰 값이 저장되고, 토큰 값이 없다면 null 값이 저장된다.
토큰 값으로 부터 파생되는 유저 상태
토큰 값으로부터 파생되는 derived atom에 대해서 살펴보기 전에 토큰을 업데이트하는 atom에 대해서 잠깐 설명하고자 한다.
위에서 모든 유저 상태의 원천이 되는 primitive atom인 tokenBaseAtom에 대해서 알아보았다, 아래에서 알아볼 derived atom 중 하나인 axios instance에서 액세스 토큰이 만료되는 경우 토큰 재발급을 통해 새로 발급받은 토큰을 tokenBaseAtom에 저장해야하는데, writable atom의 경우 해당 atom의 값을 변경하기 위해서는 외부에서 인자와 함께 업데이트 명령을 받은 경우에 대해서만 값을 변경할 수 있기 때문에, interceptor 내부에서 tokenBaseAtom의 값을 get, set 할 수 있는, 즉, tokenBaseAtom 값을 가져오거나, tokenBaseAtom에 새로운 토큰 값을 저장할 수 있는 atomWithUpdater를 구현했다.
export const atomWithUpdater = <Value, Args extends unknown[], Result>(
baseAtom: WritableAtom<Value, Args, Result>,
) =>
atom<readonly [Value, (...args: Args) => Result], Args, Result>(
(get, { setSelf }) => [get(baseAtom), setSelf],
(_get, set, ...args) => set(baseAtom, ...args),
);
export const updateTokenAtom = atomWithUpdater(tokenBaseAtom);
따라서 위와 같이 tokenBaseAtom을 atomWithUpdater의 인자로 넣어준다면
const [token, updateToken] = get(updateTokenAtom);
token 값과 해당 token 값을 변경할 수 있는 updateToken 함수를 사용할 수 있다.
자 이제 본론으로 돌아가보자, 토큰 값으로 부터 파생될 수 있는 atom으로는 다음과 같은 값이 존재할 수 있다.
1️⃣ 로그인 유무를 판단할 수 있는 atom
export const loginStatusAtom = atom<boolean>((get) => {
const [token, _] = get(updateTokenAtom);
return !!token;
});
2️⃣ 유저의 고유 아이디를 판단할 수 있는 atom
export const userUniqIdAtom = atom((get) => {
const [token, _] = get(updateTokenAtom);
if (token) {
const decodeToken = jwtDecode<JwtPayload>(token);
return decodeToken.userId;
}
return null;
});
3️⃣ 해당 유저의 토큰이 담긴 axios instance
export const defaultClientAtom = atom((get) => {
get(userUniqIdAtom);
const instance = axios.create({
baseURL: CONFIG.BASE_URL,
withCredentials: true,
});
instance.interceptors.request.use((config) => {
const [token, _] = get(updateTokenAtom);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
instance.interceptors.response.use(
...
);
return instance;
});
이렇게 각각 유저의 상태 값을 새로 정의하지 않아도, 토큰 값 하나로부터 여러가지 유저 상태를 판단할 수 있는 atom을 가질 수 있다.
인증 로직 관리
이로써 로컬스토리지에 저장된 토큰 값에 따라서 유저의 로그인 상태, 고유 아이디, axios instance를 관리할 수 있다는 것을 확인했다. 여기에 유저의 인증 로직까지 관리할 수 있다면, 사용자의 로그인 상태를 추적하고 관리하기 위한 상태와 그에 따른 비즈니스 로직이 하나의 파일에 캡슐화 되기 때문에 코드의 가독성을 향상 시킬 수 있고, 관련 기능에 대한 유지보수가 용이해질 것이다.
따라서 나는 추가적으로 인증과 관련된 로직인 로그인, 로그아웃 및 회원 탈퇴를 관리했다.
export const loginAtom = atom(null, async (get) => {
const client = get(defaultClientAtom);
const [_, updateToken] = get(updateTokenAtom);
const code = new URL(window.location.href).searchParams.get('code');
const { data } = await client.get(`/auth/kakao/callback?code=${code}`);
const token = data.data.accessToken;
updateToken(token);
});
export const logoutAtom = atom(null, async (get) => {
const client = get(ensuredAuthClientAtom);
await client.post(`/auth/logout`);
window.localStorage.removeItem(TOKEN_KEY);
if (CONFIG.ENV === 'development') {
window.location.href = `${CONFIG.LOCAL}`;
} else if (CONFIG.ENV === 'production') {
window.location.href = `${CONFIG.DOMAIN}`;
}
});
export const withdrawAtom = atom(null, async (get) => {
const client = get(ensuredAuthClientAtom);
await client.delete(`/auth`);
window.localStorage.removeItem(TOKEN_KEY);
if (CONFIG.ENV === 'development') {
window.location.href = `${CONFIG.LOCAL}`;
} else if (CONFIG.ENV === 'production') {
window.location.href = `${CONFIG.DOMAIN}`;
}
});
이렇게 애플리케이션 전역에서 유저의 로그인 상태와 인증 로직을 함께 관리하는 방법에 대해서 소개해봤다. 결과적으로 하나의 파일에 인증과 관련된 로직들이 모두 포함하여 모듈화를 진행했고, 가독성, 유지보수성, 확작성을 확보했다. instance에는 response의 에러 코드에 따른 에러처리 방법도 추가가 되어있는데, 이 부분에 대해서는 따로 Errorboundary와 react-query의 default 옵션을 사용하여 에러처리 중앙화를 구현한 것에 대해 포스팅하는 시간을 가져보도록 하겠다.
'개발' 카테고리의 다른 글
이미지 Lazy Loading을 통한 페이지 로딩 시간 감소 (0) | 2024.04.01 |
---|---|
애플리케이션 에러처리 중앙화 (0) | 2024.03.14 |
promise를 사용한 전역 모달 관리 (feat. contextAPI) (0) | 2024.03.05 |
[개발] Nextjs에서 SEO 최적화하기 (0) | 2023.06.02 |
[개발] Nextjs Link 컴포넌트와 <a> 태그 (feat. 이메일 링크걸기) (0) | 2023.05.28 |