React

[Zustand] Zustand로 상태관리하기 (next.js14, typescript)

연신내고독한늑대 2024. 8. 14. 20:00

Zustand란?

React 애플리케이션에서 상태 관리를 위해 사용되는 경량 상태 관리 라이브러리입니다. 

이 라이브러리는 간단하고 직관적인 API로, 작은 애플리케이션부터 복잡한 애플리케이션까지 손쉽게 상태 관리를 할 수 있도록 도와줍니다. Zustand는 단일 스토어 모델을 따르며, 리덕스(Redux)의 보일러플레이트 코드 없이도 쉽게 사용할 수 있습니다.

 

상태 관리 라이브러리란?

상태 관리 라이브러리는 웹 애플리케이션에서 컴포넌트 간의 상태를 효율적으로 관리하고 공유하기 위해 사용되는 도구입니다. 현대 웹 애플리케이션은 사용자와의 상호작용이 많아지고, UI가 복잡해지면서 상태(state)를 효과적으로 관리하는 것이 중요해졌습니다.

상태란 애플리케이션의 특정 시점에서의 데이터의 모습을 의미하며, UI 요소들이 서로 다른 상태를 기반으로 동작하게 됩니다. 예를 들어, 사용자가 로그인했는지 여부, 쇼핑 카트에 담긴 상품 목록, 현재 페이지의 UI 상태 등이 모두 상태에 해당합니다.

React와 같은 프론트엔드 라이브러리에서는 상태를 컴포넌트 내부에서 관리할 수 있지만, 애플리케이션이 커지고 복잡해지면 상태를 효율적으로 관리하기 어려워질 수 있습니다. 특히, 여러 컴포넌트에서 상태를 공유해야 하거나, 전역적으로 상태를 관리할 필요가 있을 때는 더욱 그렇습니다.

이런 문제를 해결하기 위해 상태 관리 라이브러리가 등장하게 되었으며, 이들 라이브러리는 전역적으로 상태를 관리하고, 다양한 컴포넌트 간에 상태를 쉽게 공유할 수 있도록 도와줍니다.

상태 관리 라이브러리의 필요성

  1. 상태의 중앙 집중화: 애플리케이션 전역에서 상태를 중앙에서 관리하고, 모든 컴포넌트가 이 상태에 접근할 수 있게 합니다. 이를 통해 코드의 일관성을 유지하고, 버그 발생 가능성을 줄일 수 있습니다.
  2. 상태의 예측 가능성: 상태 관리 라이브러리를 사용하면 상태의 변화를 추적하고 예측할 수 있습니다. 특히 Redux와 같은 라이브러리는 상태의 변경이 언제 어디서 발생했는지 명확하게 알 수 있게 해줍니다.
  3. 코드 구조의 개선: 상태 관리 라이브러리를 사용하면 컴포넌트 내부의 복잡한 상태 관리 로직을 분리하여 코드의 가독성과 유지보수성을 높일 수 있습니다.
  4. 서로 다른 컴포넌트 간의 상태 공유: 상태를 중앙에서 관리하기 때문에, 애플리케이션 내의 다양한 컴포넌트들이 쉽게 상태를 공유하고 사용할 수 있습니다. 이를 통해 개발자가 일관된 사용자 경험을 제공할 수 있습니다.

 

주요 상태 관리 라이브러리들 간단 비교 / 다운로드 수(2024.08.24기준)

- Redux Toolkit: Redux의 공식 툴킷으로, 복잡한 상태 관리를 위한 많은 기능들을 제공합니다. 다만, 상태 관리에 있어 보일러플레이트 코드가 많아질 수 있습니다. Redux는 전역 상태 관리에 강점을 가지지만, 복잡한 설정이 필요할 수 있습니다.

- Zustand: Redux와 비교하면 훨씬 간단하고, 매우 작은 크기의 상태 관리 라이브러리입니다. 상태 변경을 추적하거나 전역 상태를 쉽게 설정할 수 있으며, 불필요한 리렌더링을 방지하는 등 성능 최적화에도 도움이 됩니다.

다운로드 수를 보면 꾸준히 상승하고 있는게 보임.

- Recoil: Facebook에서 개발한 상태 관리 라이브러리로, React의 동작과 밀접하게 통합되어 있습니다. 주로 복잡한 상태와 비동기 상태 관리를 효율적으로 처리할 수 있습니다. 하지만 프로젝트의 규모가 커질수록 성능에 영향을 줄 수 있습니다.

 

# 예시 코드

로직 순서

로그인(LoginForm.tsx) -> 성공 시 사용자 정보가 Zustand 스토어에 저장(useUserStore.ts) -> 이 정보는 localStorage에 유지되며, 다음번 페이지 방문 시에도 그대로 사용 (page.tsx)

# 폴더구조
src
├── app
│ └── page.tsx
├── component
│ └── LoginForm.tsx
└── store
   └── useUserStore.ts

 

# useUserStore.ts

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { FormData } from '@/types/formData';

interface UserStore {
  user: FormData | undefined ;
  setUser: (user: FormData | undefined) => void;
  logout: () => void;
}

const useUserStore = create<UserStore>()(
   persist(
      (set) => ({
         user: undefined,
         setUser: (user: FormData | undefined) => set({ user }),
         logout: () => set({ user: undefined }),
       }),
     {
         name: 'user-info',
         storage: createJSONStorage(() => localStorage),
     }
   )
);

export default useUserStore;

- create 함수: Zustand에서 상태를 생성할 때 사용하는 기본 함수입니다. create 함수를 통해 useUserStore라는 커스텀 훅을 생성합니다.
- persist 미들웨어: Zustand의 미들웨어 중 하나로, 상태를 브라우저의 localStorage에 저장하여 페이지를 새로고침해도 상태가 유지되도록 합니다. 이 미들웨어는 상태를 자동으로 직렬화하고, 저장소에서 불러옵니다.
- storage: createJSONStorage(() => localStorage): createJSONStorage는 기본적으로 JSON 형식으로 상태를 저장하고 불러옵니다. 이 예제에서는 상태를 localStorage에 저장하도록 설정했습니다.
- 상태와 액션
     - user: 현재 로그인한 사용자의 정보를 나타내는 상태입니다.
     - setUser: user 상태를 업데이트하는 함수입니다.
     - logout: user 상태를 undefined로 초기화하여 로그아웃 처리를 합니다.

 

# LoginForm.txs

const setUserStore = useUserStore((state) => state.setUser); 
const mutation = useMutation<FormData, Error, LoginFormData>({   
     mutationFn: login,   
     onSuccess: (data) => { 
        setUserStore(data);     
        router.push('/'); // 회원가입 성공 시 리다이렉트    },
     onError: (error: Error) => {
        console.error('로그인 실패:', error);   
     }, 
});

- useUserStore((state) => state.setUser): Zustand에서 setUser 액션을 가져옵니다. 이 액션은 로그인 성공 시 사용자 정보를 저장하는 데 사용됩니다.
- setUserStore(data): 로그인 성공 시, data에 있는 사용자 정보를 user 상태로 저장합니다. 이때 Zustand의 setUser 액션을 호출합니다.
# page.tsx

import useUserStore from '@/store/useUserStore';
export default function Home() {
  const user = useUserStore((state) => state.user);
  // user 상태에 따라 다른 UI를 보여줄 수 있음
  return <div>{user ? `Welcome, ${user.username}!` : 'Please log in.'}</div>;
}

- useUserStore((state) => state.user): Zustand에서 특정 상태를 구독할 때 사용하는 패턴입니다. 이 경우 user 상태를 구독하고, 해당 상태가 변경될 때 컴포넌트가 리렌더링됩니다.