React 알못이 쓴 글입니다 :) 잘못된 내용 있으면 댓글 부탁드려요
React 의 데이터 흐름은 State & Props 를 사용해 단방향으로 이루어져야 한다는 규칙이 있다.
이러한 데이터 흐름은 컴포넌트 간의 관계를 명확히 정의하기 때문에 유지보수성을 향상시킨다. 하지만 데이터를 전달하는 과정에서 거쳐야 하는 컴포넌트가 너무 많은 상황이라면 유지보수가 어려울 뿐만 아니라 성능이 저하될 우려가 존재한다.
이러한 과정을 prop drilling 이라 한다.
이렇듯 여러 페이지에 걸쳐 공통으로 사용하는 경우가 있기 때문에 상태 관리 라이브러리를 사용한다.
프로젝트를 진행하며 로그인한 사용자의 정보를 전역 상태로 관리하기 위해 상태 관리 라이브러리를 사용하고자 했다. 상태 관리 라이브러리는 Redux, Recoil, Zustand, Context API 등이 존재한다.
Zustand 사용 이유
나는 Zustand를 사용하였다. Redux와 유사하며 설정이 간단하고 사용하기가 쉽다.
Redux와 Zustand 는 Flux 아키텍처 모델을 기반으로 하는 중앙 집중식 상태 관리 솔루션이다. 데이터가 단방향으로 흐르며 Action이 발생하면, Dispatcher에서 이를 해석한 후 Store에 저장한 정보를 갱신하고, 그 데이터가 View로 전달된다.
이러한 유형은 상태를 한 곳에 모아 관리하고 싶을 때 적합하다.
Redux와 비교했을 때, Zustand는 보일러플레이트 코드가 적어 초기 설정이 간단하고, 코드의 양이 적어 사용하기가 쉽다. 따라서 단순하고 효율적인 상태 관리가 필요한 프로젝트에 적합한 선택이다.
새로고침 시 상태가 초기화되는 문제
Zustand는 메모리에 상태를 저장한다. 따라서 페이지를 새로고침하면 메모리가 초기화되고, Zustand 상태도 초기화 된다. 이를 해결하기 위해 Zustand의 미들웨어인 persist를 이용하여 로컬 스토리리지와 같은 저장소에 데이터를 저장하여 유지할 수 있도록 해준다.
사용자의 상태를 정의하는 TypeScript 인터페이스를 만든 후, create 함수를 이용하요 Zustand store를 생성한다. 여기서 persist 미들웨어를 사용하여 상태를 로컬 스토리지에 저장한다.
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export interface UserState {
name: string;
profileImg: string;
email: string;
role: string;
setUser: (userData: Partial<UserState>) => void;
clearUser: () => void;
}
const useUserStore = create<UserState>()(
persist(
(set) => ({
name: '',
profileImg: '',
email: '',
role: '',
setUser: (userData: Partial<UserState>) => set((state) => ({ ...state, ...userData })),
clearUser: () => set({ name: '', profileImg: '', email: '', role: '' }),
}),
{
name: 'user-storage',
}
)
);
export { useUserStore };
export default useUserStore;
커스텀 훅을 작성하여 어플리케이션이 로드될 때, 실행되도록 하였다. 위에선 사용자의 상태만 저장했지만 다른 팀원이 작성한 토큰 상태 저장 또한 함께 저장한다.
// useInitializeAuth.tsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import useAuthStore from '@store/useAuthStore';
import useUserStore from '@store/useUserStore';
const useInitializeAuth = () => {
const nav = useNavigate();
const { setTokens, clearTokens } = useAuthStore((state) => ({
setTokens: state.setTokens,
clearTokens: state.clearTokens,
}));
const { setUser, clearUser } = useUserStore((state) => ({
setUser: state.setUser,
clearUser: state.clearUser,
}));
useEffect(() => {
const queryParams = new URLSearchParams(window.location.search);
const code = queryParams.get('tempToken');
if (code) {
fetch(`요청 API 경로`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${code}`,
},
})
.then((response) => response.json())
.then((data) => {
const { 'Authorization': accessToken, 'Authorization-Refresh': refreshToken } = data;
setTokens(accessToken, refreshToken);
fetch('요청 API 경로', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
})
.then((response) => response.json())
.then((userData) => {
setUser(userData);
nav('/');
})
.catch((error) => {
clearTokens();
clearUser();
});
})
.catch((error) => {
clearTokens();
clearUser();
});
}
}, [nav, setTokens, clearTokens, setUser, clearUser]);
};
export default useInitializeAuth;
작성을 마치며
문제를 해결하는 과정에서 단순히 로컬 스토리지를 직접 사용하는 경우와 Zustand의 persist를 사용하는 경우의 차이가 무엇이지 라는 궁금증이 들었다. 두 방법 모두 상태를 브라우저의 로컬 스토리지에 저장하는 방식이기 때문이다.
결론은 Zustand의 persist 미들웨어를 사용하면 상태 저장과 복원을 자동으로 처리해 주므로, 직접 로컬 스토리지를 관리할 필요가 없다. 상태를 로컬 스토리지에 저장하고 복원하는 로직이 Zustand 내부에서 관리되기 때문에 코드가 간결해지고 유지보수가 쉬워진다.