아니 원래 토큰을 Context API를 사용해서 전역적으로 관리했었는데 이거 보니까
좀 개선의 여지가 있어서 개선한 기록이다.
zustand가 훨씬 쓰기 쉽고 Provider로 안감싸도되니까 뭐 여기저기서 쓰기도 편하고
그리고 기존 로직도 보니까 불필요하게 중복으로 저장하는부분도 있어서 새로 바꿧다.
Token 관리 로직 개선을 위한 상태 관리 라이브러리(zustand) 신규 도입
라이브러리 설치
yarn add zustand
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 };
변경 사항
간결하고 일관성 있는 처리가 되도록 개선하였습니다. 토큰을 중복해서 저장하지 않고, tokens 객체에 직접 저장하여 관리합니다. 또한 ref를 사용하지 않아 코드의 복잡성이 줄어들었습니다.
신규 아키텍처에서는 deleteTokensInKeyChain() 함수를 사용하여 Keychain에서 토큰을 명시적으로 삭제하고 있어, 토큰 삭제 과정이 더 명확합니다.