
zustand는 redux 다음으로 다운로드 수가 많으며, weekly downloads는 3위에 해당한다.
redux가 아닌zustand를 선택하면서 장점은?공식 문서에 따르면, 작고 빠르며 확장 가능한 베어본 상태 관리 솔루션이다.
Redux와 크게 차이나진 않지만,provider가 필요없다는 점과action이 없어도 상태 변경이 가능하다는 점이 다르다.
가장 큰 장점은Redux에 비해 코드 양이 현저히 줄어들고Redux의 경우 보일러플레이트의 양이 많이 반면,
Zustand는 적은 코드 양으로 store의 생성 및 업데이트가 가능하다. 특히, 러닝커브도 낮은게 마음에 들었다.
Comparison
npm install zustand
import { create } from "zustand";
const useStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
updateBears: (newBears) => set({ bears: newBears }),
}));
function BearCounter() {
const bears = useStore((state) => state.bears);
return <h1>{bears} around here...</h1>;
}
function Controls() {
const increasePopulation = useStore((state) => state.increasePopulation);
return <button onClick={increasePopulation}>one up</button>;
}
Zustand는 개발자가 State의 타입을 선언하고, creat함수의 파라미터에 함수 형태로 State의 초기값과 state를 변경하는 함수를 선언하는 것 뿐이다.
Zustand는 자동으로 state를 생상하고 state가 변경될 때마다 React 컴포넌트를 업데이트 한다.
작은 단위의 Store로 Store를 분할
- 많은 기능이 추가 될수록 스토어는 점점 커지고 유지 관리가 어려워 질 수가 있다.
- 메인 스토어를 작은 개별 스토어로 분할하여 모듈화
(공식문서 내용)
Slice Pattern
// 공식문서 예제
interface BearSlice {
bears: number;
addBear: () => void;
decrement: () => void;
eatFish: () => void;
}
interface FishSlice {
fishes: number;
addFish: () => void;
}
interface SharedSlice {
addBoth: () => void;
getBoth: () => void;
}
const createBearSlice: StateCreator<
BearSlice & FishSlice, // set/get 호출 store의 제너릭 타입
[],
[],
BearSlice // createBearSlice의 제너릭 타입
> = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 }), false, "addBear"),
decrement: () => set((state) => ({ bears: state.bears - 1 })),
eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
});
const createFishSlice: StateCreator<
BearSlice & FishSlice,
[],
[],
FishSlice
> = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
});
const createSharedSlice: StateCreator<
BearSlice & FishSlice, // Store의 제너릭 타입
[],
[],
SharedSlice // createSharedSlice의 제너릭 타입
> = (set, get) => ({
addBoth: () => {
// you can reuse previous methods
get().addBear();
get().addFish();
// or do them from scratch
// set((state) => ({ bears: state.bears + 1, fishes: state.fishes + 1 })
},
getBoth: () => get().bears + get().fishes,
});
react-zustand/
├── ...
│
├── src/
│ ├── components/
│ │ ├── Counter.tsx
│ │ ├── Todo.tsx
│ │ └── ...
│ │
│ ├── store/
│ │ ├── bearSlice.ts
│ │ ├── ...
│ │ ├── +[slice.ts] // 추가 Slice 파일
│ │ ├── type.d.ts // 중복 Slice 타입 재정의
│ │ └── useStore.tsx
│ │
│ └── App.tsx
└── ...
// type.d.ts
import { StateCreator } from "zustand";
/**
* 하나의 store를 두고 상태마다 Slice를 만들어 병합 해주는 타입 정의
* @type SlicePattern
* @generic T: 상태의 타입
* @generic S: Slice의 타입
*/
declare module "zustand" {
type SlicePattern<T, S = T> = StateCreator<
T,
[["zustand/devtools", never]],
[],
S
>;
}
// bearSlice.ts
import { SlicePattern } from "zustand";
export interface BearSlice {
bears: number;
addBear: () => void;
decrement: () => void;
// eatFish: () => void;
}
/**
* 사용자 정의 Slice 생성
* @param set
* @generic BearSlice 상태 타입
* @generic BearSlice Slice 타입
*/
export const createBearSlice: SlicePattern<BearSlice, BearSlice> = (set) => ({
bears: 0,
addBear: () =>
set((state: BearSlice) => ({ bears: state.bears + 1 }), false, "addBear"),
decrement: () => set((state: BearSlice) => ({ bears: state.bears - 1 })),
});
// fishSlice.ts
import { SlicePattern } from "zustand";
import { BearSlice } from "./bearSlice";
export interface FishSlice {
fishes: number;
addFish: () => void;
}
export const createFishSlice: SlicePattern<BearSlice & FishSlice, FishSlice> = (
set
) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
});
// shareSlice.ts
import { SlicePattern } from "zustand";
import { BearSlice } from "./bearSlice";
import { FishSlice } from "./fishSlice";
export interface SharedSlice {
addBoth: () => void;
getBoth: () => void;
}
export const createSharedSlice: SlicePattern<
BearSlice & FishSlice,
SharedSlice
> = (set, get) => ({
addBoth: () => {
// you can reuse previous methods
get().addBear();
get().addFish();
// or do them from scratch
// set((state) => ({ bears: state.bears + 1, fishes: state.fishes + 1 })
},
getBoth: () => get().bears + get().fishes,
});
// useStore.tsx
import { create, SlicePattern, StateCreator } from "zustand";
import { devtools } from "zustand/middleware";
import { createBearSlice, BearSlice } from "./bearSlice";
import { createFishSlice, FishSlice } from "./fishSlice";
import { createSharedSlice, SharedSlice } from "./sharedSlice";
/**
* Slice Store 타입 정의
*/
type BoundStoreType = BearSlice & FishSlice & SharedSlice;
const useBoundStore = create<BoundStoreType>()(
devtools(
(...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
...createSharedSlice(...a),
}),
{ name: "MyZustandStore" }
)
);
export default useBoundStore;
import React from "react";
import useStore from "../stores/useStore";
import { BearSlice } from "../stores/bearSlice";
import { FishSlice } from "../stores/fishSlice";
const Counter: React.FC = () => {
const { bears, addBear, decrement } = useStore((state: BearSlice) => state);
const { fishes } = useStore((state: FishSlice) => state);
return (
<div>
<h2>Counter</h2>
<p>{bears}</p>
<p>{fishes}</p>
<button onClick={() => addBear()}>Increment</button>
<button onClick={() => decrement()}>Decrement</button>
<button onClick={() => decrement()}>Decrement</button>
</div>
);
};
export default Counter;
기존의 사용했던 Redux와 비교 했을 때, 액션, 액션함수, 리듀서, 타입 정의 등 하나의 디렉토리로 관심사를 모아 CombineRedux 했던 점을 Zustand는 따로 정의 할 필요 없이 장황한 코드가 확연히 줄어들었다.
불변 상태를 관리하기 위한 immer 라이브러리가 적용 했을 때의 이점이 있을지 고민이 필요.
immer 라이브러리란?
immer는 JavaScript 애플리케이션에서 불변 상태를 관리하기 위해 설계된 라이브러리입니다.
immer는 상태를 불변하게 유지하면서도, 가변 상태처럼 간단하고 직관적으로 업데이트할 수 있게 해줍니다
[출처: ChatGPT]
공식 문서가 깔끔하면서도 예제가 자세해 빠르게 학습할 수 있었다.
전역 상태에 Provider를 제거할 수 있어 상대적으로 향후 마이그레이션이 원활할 것이다라는 기대..
devtools는 크롬 확장 프로그램인 Redux Devtools를 이용해 상태를 디버깅 할 수 있게 해주는데, store가 다르면 상태가 호출되었을 때 해당 store만 디버깅하면 된다.
참고
https://docs.pmnd.rs/zustand/guides/slices-pattern
https://docs.pmnd.rs/zustand/guides/typescript#middlewares-and-their-mutators-reference