Intro
프로젝트를 하다보면 진행 상황을 저장하거나, 로그인 정보를 저장하는 등 브라우저를 껐다 키거나, 새로고침을 하더라도 데이터가 남아있어야 하는 상황이 자주 발생한다.
이럴 때는 Local Storage나 Session Storage를 활용해서 구현하는데 Local Storage는 새로고침 및 브라우저를 닫은 후에도 데이터가 유지되며, Session Storage는 새로고침 상황에서만 유지될 수 있게 해준다.
이번 글은 상태 관리 라이브러리인 recoil-persist를 사용해서 새로고침을 해도 로그인을 유지할 수 있도록 access-token과 refresh-token을 관리해보겠다.
1. Recoil과 recoil-persist
1.1 recoil-persist란
recoil-persist는 Recoil 상태를 로컬 스토리지나 세션 스토리지에 저장하여, 상태를 유지할 수 있게 해주는 라이브러리다.
1.2 라이브러리 설치
npm install recoil recoil-persist
1.3 기본 설정
- key: 저장소에 저장될 key 값
- storage: 저장소 선택 (localStorage or sessionStorage)
import { atom } from 'recoil'; import { recoilPersist } from 'recoil-persist'; useAuthStore.ts; // recoil-persist 설정 const { persistAtom } = recoilPersist({ key: 'recoil-persist', storage: sessionStorage, });
기본 설정 이후 로컬 혹은 세션 스토리지에 저장하고 싶은 atom에 effects_UNSTABLE: [petsistAtom] 을 추가해준다.
effects_UNSTABLE은 Recoil에서 제공하는 기능으로, atom의 사이드 이펙트를 정의할 때 사용한다.
여기서는 recoil-persist의 persistAtom을 추가하여 해당 Atom의 상태를 지속적으로 저장하고 복원할 수 있게 해준다.
export const accessTokenState = atom<string | null>({ key: 'accessToken', default: null, effects_UNSTABLE: [persistAtom], }); export const refreshTokenState = atom<string | null>({ key: 'refreshToken', default: null, effects_UNSTABLE: [persistAtom], });
2. 보안 고려사항
Local Storage나 Session Storage에 access-token이나 refresh-token과 같은 민감한 데이터를 저장하는 경우, 클라이언트 측에서 쉽게 접근 가능하고, 이로 인해 토큰을 탈취할 위험이 있기 때문에 보안상 주의하여야한다.
이를 방지하기 위해 다음과 같은 방법을 사용할 수 있다:
- JWT(JSON Web Token)를 이용한 암호화
- access-token의 수명을 짧게 설정하고, refresh-token으로 갱신
- HTTPS 사용
- HttpOnly, Secure 쿠키 사용
- 엄격한 CORS 설정
- SameSite 쿠키 설정
3. 토큰 관리 함수
사용자의 토큰을 관리하는 함수들은 다음과 같으며, 일반적으로 토큰을 관리하는 함수와 큰 차이가 없다.
코드들은 세션 스토리지에 토큰을 저장하는 기준이다.
3.1 토큰 만료 함수
recoil-persist의 key로 저장했던 값만 삭제해주면 된다.
export const expireToken = () => { sessionStorage.removeItem('recoil-persist'); };
3.2 액세스 토큰 관리 함수
export const handleAccessToken = (): string | null => { const recoilPersistData = sessionStorage.getItem('recoil-persist'); if (recoilPersistData) { const parsedData = JSON.parse(recoilPersistData); if (parsedData && parsedData.accessToken) { return parsedData.accessToken; } } return null; };
3.3 리프레시 토큰 관리 함수
export const handleRefreshToken = (): string | null => { const recoilPersistData = sessionStorage.getItem('recoil-persist'); if (recoilPersistData) { const parsedData = JSON.parse(recoilPersistData); return parsedData.refreshToken; } return null; };
4. 활용 예시
토큰 관리 함수들을 사용하여, 사용자의 로그인, 로그아웃, 토큰 갱신 기능을 구현할 수 있다.
import { useRecoilState } from 'recoil'; import { accessTokenState, expireToken, handleAccessToken, handleRefreshToken, refreshTokenState, } from './authStore'; // 로그인 함수 예시 const login = async (username: string, password: string) => { // API 호출 로직 const { accessToken, refreshToken } = await loginAPI(username, password); // 토큰 저장 setAccessToken(accessToken); setRefreshToken(refreshToken); }; // 로그아웃 함수 예시 const logout = () => { expireToken(); // 추가적인 로그아웃 로직 }; // 토큰 갱신 함수 예시 const refreshTokens = async () => { const currentRefreshToken = handleRefreshToken(); if (currentRefreshToken) { // API를 통한 토큰 갱신 로직 const { newAccessToken, newRefreshToken } = await refreshTokenAPI(currentRefreshToken); setAccessToken(newAccessToken); setRefreshToken(newRefreshToken); } };
마무리
Recoil과 recoil-persist를 사용하면 브라우저의 종료나, 새로고침 상황에서도 간편하게 토큰을 유지할 수 있다.
다만, 보안적인 측면에서 민감한 정보를 저장하는 경우, 스토리지 사용 시 주의해야 하며 가능하면 데이터를 암호화하여 저장하는 것이 좋다.
사용자 경험 향상의 목적이 있지만, 이제는 필수적인 기능이며 Recoil이 아니더라도, Redux, Zustand, Jotai 등 다양한 상태 관리 라이브러리를 활용해서도 구현할 수 있어야 한다.