토큰저장 로직 쪼끔 더 알기쉽게 바꾼기록

이승훈·2024년 6월 30일
0

시행착오

목록 보기
21/23
post-thumbnail

들어가기전에

아니 원래 토큰을 Context API를 사용해서 전역적으로 관리했었는데 이거 보니까
좀 개선의 여지가 있어서 개선한 기록이다.

zustand가 훨씬 쓰기 쉽고 Provider로 안감싸도되니까 뭐 여기저기서 쓰기도 편하고
그리고 기존 로직도 보니까 불필요하게 중복으로 저장하는부분도 있어서 새로 바꿧다.

개선 내용

Token 관리 로직 개선을 위한 상태 관리 라이브러리(zustand) 신규 도입

  1. 라이브러리 설치

    yarn add zustand
  2. store 생성 및 초기 상태 설정

    store 위치

    mobile-app/src/store/AuthToken

    store 초기 상태 설정

    기존 Context API 사용

    import AsyncStorage from '@react-native-async-storage/async-storage';
    import type { PropsWithChildren } from 'react';
    import { createContext, useCallback, useRef, useState } from 'react';
    
    import {
      deleteTokensInKeychain,
      getAccessTokenFromKeychain,
      getRefreshTokenFromKeychain,
      setAccessTokenToKeychain,
      setRefreshTokenToKeychain,
    } from '@/services/keychain';
    
    export interface AuthTokens {
      accessToken: string;
      refreshToken: string;
    }
    
    interface AuthTokenContextType {
      tokenState: AuthTokens;
      loadTokensFromKeychain: () => Promise<AuthTokens | null>;
      getAccessToken: () => string | null;
      getRefreshToken: () => string | null;
      setAccessToken: (accessToken: string) => void;
      setRefreshToken: (refreshToken: string) => void;
      deleteTokens: () => Promise<void>;
    }
    
    export const AuthTokenContext = createContext<AuthTokenContextType | null>(
      null,
    );
    
    const AuthTokenProvider = ({ children }: PropsWithChildren) => {
      const [tokenState, setTokenState] = useState<AuthTokens>({
        accessToken: '',
        refreshToken: '',
      });
      const accessTokenRef = useRef('');
      const refreshTokenRef = useRef('');
    
      const getAccessToken = useCallback(() => {
        if (!accessTokenRef.current) {
          console.log('NO_ACCESSTOKEN');
          return null;
        }
        return accessTokenRef.current;
      }, []);
    
      const getRefreshToken = useCallback(() => {
        if (!refreshTokenRef.current) {
          console.log('NO_REFRESHTOKEN');
          return null;
        }
        return refreshTokenRef.current;
      }, []);
    
      /**
       * Context와 저장소에 accessToken을 저장합니다.
       */
      const setAccessToken = useCallback((accessToken: string) => {
        if (!accessToken) {
          console.log('ERROR: 저장할 액세스 토큰이 없습니다');
          return;
        }
        accessTokenRef.current = accessToken;
        setTokenState((prev) => ({ ...prev, accessToken }));
        setAccessTokenToKeychain(accessToken);
      }, []);
    
      const setRefreshToken = useCallback(async (refreshToken: string) => {
        if (!refreshToken) {
          console.log('ERROR: 저장할 리프레시 토큰이 없습니다');
          return;
        }
        refreshTokenRef.current = refreshToken;
        setTokenState((prev) => ({ ...prev, refreshToken }));
        setRefreshTokenToKeychain(refreshToken);
      }, []);
    
      /** 앱 구동 시에 기기 저장소에서 access token을 세팅합니다. */
      const loadTokensFromKeychain = useCallback(async () => {
        const initAccessToken = await getAccessTokenFromKeychain();
        const initRefreshToken = await getRefreshTokenFromKeychain();
    
        if (!initAccessToken || !initRefreshToken) {
          deleteTokensInKeychain();
          if (initAccessToken) {
            //! PoC 유저의 AsyncStorage를 clear합니다.
            AsyncStorage.clear();
          }
          return null;
        }
    
        setAccessToken(initAccessToken);
        setRefreshToken(initRefreshToken);
    
        return {
          accessToken: initAccessToken,
          refreshToken: initRefreshToken,
        };
      }, [setAccessToken, setRefreshToken]);
    
      /**
       * 기기 저장소와 Context에서 토큰들을 제거합니다.
       */
      const deleteTokens = useCallback(async () => {
        await deleteTokensInKeychain();
      }, []);
    
      return (
        <AuthTokenContext.Provider
          value={{
            tokenState,
            loadTokensFromKeychain,
            getAccessToken,
            getRefreshToken,
            setAccessToken,
            setRefreshToken,
            deleteTokens,
          }}
        >
          {children}
        </AuthTokenContext.Provider>
      );
    };
    
    export default AuthTokenProvider;
    

    개선 후 zustand 사용

    import AsyncStorage from '@react-native-async-storage/async-storage';
    import { create } from 'zustand';
    
    import {
      deleteTokensInKeychain,
      getAccessTokenFromKeychain,
      getRefreshTokenFromKeychain,
      setAccessTokenToKeychain,
      setRefreshTokenToKeychain,
    } from '@/services/keychain';
    
    type State = {
      tokens: {
        accessToken: string;
        refreshToken: string;
      };
    };
    
    type Actions = {
      getAccessToken: () => string | null;
      getRefreshToken: () => string | null;
      setAccessToken: (accessToken: string) => void;
      setRefreshToken: (refreshToken: string) => void;
      loadTokensFromKeychain: () => Promise<void>;
      deleteTokens: () => Promise<void>;
    };
    
    const useAuthTokenStore = create<State & Actions>((set, get) => ({
      tokens: {
        accessToken: '',
        refreshToken: '',
      },
      getAccessToken: () => {
        const { accessToken } = get().tokens;
        return accessToken || null;
      },
      getRefreshToken: () => {
        const { refreshToken } = get().tokens;
        return refreshToken || null;
      },
      setAccessToken: (accessToken: string) => {
        if (!accessToken) {
          console.log('ERROR: 저장할 액세스 토큰이 없습니다');
          return;
        }
        set({ tokens: { ...get().tokens, accessToken } });
        setAccessTokenToKeychain(accessToken);
      },
      setRefreshToken: (refreshToken: string) => {
        if (!refreshToken) {
          console.log('ERROR: 저장할 리프레시 토큰이 없습니다');
          return;
        }
        set({ tokens: { ...get().tokens, refreshToken } });
        setRefreshTokenToKeychain(refreshToken);
      },
      loadTokensFromKeychain: async () => {
        const [accessToken, refreshToken] = await Promise.all([
          getAccessTokenFromKeychain(),
          getRefreshTokenFromKeychain(),
        ]);
    
        if (!accessToken || !refreshToken) {
          deleteTokensInKeychain();
          if (accessToken) {
            //! PoC 유저의 AsyncStorage를 clear합니다.
            AsyncStorage.clear();
          }
          return;
        }
    
        set({ tokens: { accessToken, refreshToken } });
      },
      deleteTokens: async () => {
        await deleteTokensInKeychain();
        set({ tokens: { accessToken: '', refreshToken: '' } });
      },
    }));
    
    export { useAuthTokenStore };
    
  3. 변경 사항

    1. 토큰 저장 방식:
      • 기존 아키텍처에서는 accessToken과 refreshToken을 별도의 ref (accessTokenRef, refreshTokenRef)와 tokenState에 중복해서 저장
      • 신규 아키텍처에서는 accessToken과 refreshToken을 zustand store의 tokens 객체에 저장하고, ref를 사용하지 않음
    2. 토큰 삭제 과정:
      • 기존 아키텍처에서는 deleteTokens() 함수를 호출하여 토큰을 삭제
      • 신규 아키텍처에서는 deleteTokensInKeyChain() 함수를 호출하여 Keychain에서 토큰을 삭제하고, tokens 객체의 값을 빈 문자열로 설정
    3. 에러 처리:
      • 기존 아키텍처에서는 에러 발생 시 logout() 함수를 호출하고, isAppInitialized 값을 true로 설정
      • 신규 아키텍처에서도 에러 발생 시 동일한 처리

    간결하고 일관성 있는 처리가 되도록 개선하였습니다. 토큰을 중복해서 저장하지 않고, tokens 객체에 직접 저장하여 관리합니다. 또한 ref를 사용하지 않아 코드의 복잡성이 줄어들었습니다.

    신규 아키텍처에서는 deleteTokensInKeyChain() 함수를 사용하여 Keychain에서 토큰을 명시적으로 삭제하고 있어, 토큰 삭제 과정이 더 명확합니다.

profile
Beyond the wall

0개의 댓글