recoil-persist를 이용한 사용자 인증 관리

Intro

프로젝트를 하다보면 진행 상황을 저장하거나, 로그인 정보를 저장하는 등 브라우저를 껐다 키거나, 새로고침을 하더라도 데이터가 남아있어야 하는 상황이 자주 발생한다.

이럴 때는 Local StorageSession Storage를 활용해서 구현하는데 Local Storage는 새로고침 및 브라우저를 닫은 후에도 데이터가 유지되며, Session Storage는 새로고침 상황에서만 유지될 수 있게 해준다.

이번 글은 상태 관리 라이브러리인 recoil-persist를 사용해서 새로고침을 해도 로그인을 유지할 수 있도록 access-tokenrefresh-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);
  }
};

마무리

Recoilrecoil-persist를 사용하면 브라우저의 종료나, 새로고침 상황에서도 간편하게 토큰을 유지할 수 있다.

다만, 보안적인 측면에서 민감한 정보를 저장하는 경우, 스토리지 사용 시 주의해야 하며 가능하면 데이터를 암호화하여 저장하는 것이 좋다.

사용자 경험 향상의 목적이 있지만, 이제는 필수적인 기능이며 Recoil이 아니더라도, Redux, Zustand, Jotai 등 다양한 상태 관리 라이브러리를 활용해서도 구현할 수 있어야 한다.

Reference