-
Zustand 맛보기Frameworks, Platforms and Libraries/React 2025. 2. 28. 23:33
이번에 여러 project에서 공용으로 사용할 모듈을 따로 package로 구성하면서 개발하게 되어 간단하게 사용할 상태 관리 라이브러리를 찾다가 Zustand를 써보게 되었습니다. Zustand를 사용하면서 해당 내용을 정리해 보게 되었습니다.
Zustand?
Zustand는 React 상태 관리 라이브러리 중 하나이며 hooks 기반의 API를 제공하는 작고 빠르며 확장 가능한 상태 관리 솔루션입니다.
사용 방법
먼저 Zustand를 설치합니다.
npm install zustand # or yarn add zustand
기본 사용법
Zustand를 사용하는 예시 코드를 작성해보겠습니다. 예시 코드의 환경은 React, TypeScript 환경으로 가정하겠습니다.
// src/store/count.ts import { create } from "zustand"; interface CountState { count: number; plusCount: () => void; updateCount: (count: number) => void; } export const useCountStore = create<CountState>((set, get) => ({ count: 0, plusCount: () => { const state = get(); const { count } = state; set({ count: count + 1 }); }, updateCount: (count: number) => set({ count: count }), }));
Zustand에서는 create를 이용하여 store을 생성하며 create를 통해 state와 state를 수정하는 action를 생성합니다. action에서는 create의 콜백 함수인 get과 set 함수를 통해 state를 조회 및 변경을 하여 state를 업데이트합니다.
store 네이밍은 use<feature name>Store로 컨벤션을 맞춰줍니다.
위의 store를 컴포넌트 내에서 사용 예제는 아래와 같습니다.
// src/components/Count.tsx import { useCountStore } from "@/store"; function Count() { const count = useCountStore((state) => state.count); const plusCount = useCountStore((state) => state.plusCount); const updateCount = useCountStore((state) => state.updateCount); return ( <> <p>count: {count}</p> <button onClick={() => plusCount()}>+1</button> <button onClick={() => updateCount(0)}>reset</button> </> ); } export default Count;
action get, set 정리
action에서 get 함수를 사용하지 않고 set 함수에서 바로 state를 얻어 사용할 수 있습니다.
import { create } from "zustand"; interface CountState { count: number; plusCount: () => void; updateCount: (count: number) => void; } export const useCountStore = create<CountState>((set) => ({ count: 0, plusCount: () => { set(({ count }) => ({ count: count + 1 })); }, updateCount: (count: number) => set({ count: count }), }));
action 분리
컴포넌트 내에서 state와 action을 사용시 편의성을 위해 state와 action을 분리해보겠습니다.
import { create } from "zustand"; interface CountState { count: number; actions: { plusCount: () => void; updateCount: (count: number) => void; }; } export const useCountStore = create<CountState>((set) => ({ count: 0, actions: { plusCount: () => { set(({ count }) => ({ count: count + 1 })); }, updateCount: (count: number) => set({ count: count }), }, }));
import { useCountStore } from "@/store"; function Count() { const count = useCountStore((state) => state.count); const { plusCount, updateCount } = useCountStore( (state) => state.actions, ); return ( <> <p>count: {count}</p> <button onClick={() => plusCount()}>+1</button> <button onClick={() => updateCount(0)}>reset</button> </> ); } export default Count;
action이 많은 케이스에서 좀 더 편하게 사용 가능해졌습니다.
Middleware
Zustand는 store를 구성할 때 다양한 기능을 사용할 수 있는 middleware를 제공합니다.
상태 타입 추론 combine
예제를 통해 바로 살펴보겠습니다.
import { create } from "zustand"; export const useCountStore = create((set) => ({ count: 0, actions: { plusCount: () => { // count의 type을 못찾는 이슈 발생!!! set(({ count }) => ({ count: count + 1 })); }, updateCount: (count: number) => set({ count: count }), }, }));
해당 케이스에서 count의 타입이 명시되지 않아 set 함수에서 state를 참조 시 에러가 발생합니다.
combine을 사용하여 state의 타입을 추론하여 오류를 처리해 보겠습니다.
import { create } from "zustand"; import { combine } from "zustand/middleware"; export const useCountStore = create( combine({ count: 0 }, (set) => ({ actions: { plusCount: () => { set(({ count }) => ({ count: count + 1 })); }, updateCount: (count: number) => set({ count: count }), }, })) );
타입을 따로 명시해 주지 않아도 combine을 이용하면 state의 타입을 추론할 수 있어 state의 타입을 못 찾는 이슈를 해결할 수 있습니다.
불변 업데이트 immer
Reference type의 state를 변경할 때에는 불변성을 지켜야 합니다. 불변성을 지키면서 state를 업데이트하는 작업은 state의 중첩성이 커질수록 복잡해집니다. immer를 통해 간편히 업데이트 코드 작성이 가능합니다.
immer middleware을 사용하기 위해선 immer 라이브러리가 필요합니다.
npm install immer # or yarn add immer
import { create } from "zustand"; import { immer } from "zustand/middleware/immer"; import { User } from "@/interfaces"; interface UserState { user: User; actions: { updateFirstName: (name: string) => void; }; } export const useUserStore = create( immer<UserState>((set) => ({ user: { name: { firstName: "firstName", lastName: "lastName" } }, actions: { updateFirstName: (name: string) => set((state) => { state.user.name.firstName = name; }), }, })) );
middleware immer에 immer 라이브러리가 디펜더시가 걸려있지만 Reference type의 state를 쉽게 불변성을 지키며 업데이트할 수 있습니다.
상태 변경 구독 subscribeWithSelector
subscribeWithSelector middleware는 특정 state를 구독할 수 있습니다. 예제를 통해 살펴보겠습니다.
import { create } from "zustand"; import { subscribeWithSelector } from "zustand/middleware"; interface CountState { count: number; double: number; actions: { updateCount: (count: number) => void; }; } export const useCountStore = create( subscribeWithSelector<CountState>((set) => ({ count: 1, double: 2, actions: { updateCount: (count: number) => set({ count: count }), }, })) ); useCountStore.subscribe( (state) => state.count, (count) => { useCountStore.setState(() => ({ double: count * 2 })); } );
예시의 코드처럼 count state를 구독하여 해당 state가 변경이 있을 경우 double state가 변경이 됩니다. count state에 따라 자동으로 계산되어 computed state로 관리할 수 있습니다.
※ subscribeWithSelector을 사용하지 않고 store을 subscribe 할 경우 해당 store의 모든 state가 구독됩니다.storage state 저장 persist
persist middleware를 통해 storage에 state를 저장시켜 application이 reload 되더라도 state를 유지시킬 수 있습니다.
import { create } from "zustand"; import { persist } from "zustand/middleware"; interface CountState { count: number; actions: { updateCount: (count: number) => void; }; } export const useCountStore = create( persist<CountState>( (set) => ({ count: 1, actions: { updateCount: (count: number) => set({ count: count }), }, }), { name: "count", } ) );
개발자 도구 devtools
devtools middleware는 Redux없이 Redux DevTools Extension을 사용할 수 있습니다.
import { create } from "zustand"; import { devtools } from "zustand/middleware"; interface CountState { count: number; actions: { updateCount: (count: number) => void; }; } export const useCountStore = create( devtools<CountState>((set) => ({ count: 1, actions: { updateCount: (count: number) => set({ count: count }), }, })) );
actions과 reducers를 통한 업데이트 redux
해당 내용은 Zustand보다 Redux의 구조와 연관성이 깊어 공식 문서 링크로 내용을 대체하겠습니다.
마무리
최근 많이 사용하는 상태 관리 라이브러리인 Zustand, Jotai, Recoil의 다운로드 수를 비교하면 Zustand가 다른 상태 관리 라이브러리와 비교하여 압도적으로 많은 선택을 받음을 알 수 있습니다.
https://npmtrends.com/jotai-vs-recoil-vs-zustand 이번에 Zustand를 사용하면서 따로 전역 상태를 설정하는 Provider가 없고 보일러 플레이트도 만들지 않으며 쉽게 작업할 수 있었습니다. 처음 Redux를 사용할 때와 비교하면 새로 나오는 상태 관리 라이브러리들은 점점 사용이 간편하고 러닝 커브 도 적게 나오는 것 같아 사용자들이 편하게 사용할 수 있을 것 같습니다.
참고 자료
https://zustand.docs.pmnd.rs/getting-started/introduction
https://www.heropy.dev/p/n74Tgc
https://ui.toast.com/posts/ko_20210812
'Frameworks, Platforms and Libraries > React' 카테고리의 다른 글
Goodbye Recoil (2) 2024.10.31 useState 함수형 업데이트 (2) 2024.08.18 String to HTML, dangerouslySetInnerHTML (0) 2024.06.09 Recoil 도입기 (0) 2024.05.19 useTransition와 useDeferredValue 알아보기 (1) 2024.04.28