전역 상태를 관리할 때, react context, redux-toolkit 등을 자주 사용하는데, 나는 zustand를 접한 후, 굉장히 많이 쓰게 되었다.
왜 사용하는지 장단점에 대해 알아보고 실제 코드도 작성하여 예제를 보고 이해 해 보도록 하자. 공식문서를 참고한 것이니 공식문서를 보아도 좋다
zustand는 redux와 마찬가지로 상태 관리를 위한 라이브러리이며, 불변 상태 모델을 기반으로 한다. zustand는 다음과 같은 장단점들이 있다.
장점
단순성과 사용 용이성
경량
단점
부족한 기능
zustand는 단순하기 때문에, 복잡한 상태 관리가 필요한 대규모 애플리케이션에서는 기능이 부족할 수 있음
작은 생테계
Redux 등에 비해 커뮤니티와 생태계가 비교적 작음
zustnad는 사용방법이 매우 쉽고 단순하다. provider도 필요하지 않으며, 저장소를 생성하고 사용하는 방법만 익히면 된다.
zustand를 설치한다.
npm install zustand
저장소는 create를 이용해 생성하며, create의 콜백함수 인자로 set을 받는다. set함수로 저장소의 상태를 변경할 수 있다.
set으로 상태를 변경할 때, 저장소 안의 상태 변수를 사용하고 싶다면 set함수의 콜백함수 인자(state)로 가져올 수 있다.
import create from 'zustand';
interface StoreState {
count: number
setCount: (newCount: number) => void
increment: () => void
}
const useStore = create<StoreState>(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
setCount: (newCount) => set({ count: newCount }),
}));
직접 정의한 저장소의 상태와 함수를 사용하기 위한 방식이 2가지 있으며, 편한 방식을 사용하면 된다. 작동 방식은 useState와 비슷하다고 생각하면 된다.
(상태가 바뀌면 App이 리렌더링 되는 것)
import useStore from '저장소 있는 곳';
function App() {
//저장소 사용 방식 1
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
const setCount = useStore((state) => state.setCount);
//저장소 사용 방식 2
const { count, increment, setCount } = useStore();
return (
<div>
<button onClick={increment}>+</button>
<button onClick={() => setCount(0)}>Reset</button>
<span>{count}</span>
</div>
);
}
다음의 코드에서 Fish의 수를 관리하는 저장소와 Bear의 수를 관리하는 저장소가 있다. 이 두 스토어가 서로 연관성이 있다고 하면 다음과 같이 하나의 스토어로 통합할 수 있다.
❌ 여러 개의 스토어
import create from 'zustand';
const useFishStore = create(set => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 }))
}));
const useFishStore = create(set => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 }))
}));
✅ 한 개의 스토어
Single Store 패턴을 만족하려면, 다음과 같이 작성해야 한다.
import create from 'zustand';
const useStore = create(set => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 }))
}));
그러나 한 개의 스토어 상태의 개수가 매우 복잡해진다면 유지보수가 힘들어질 수 있다 따라서 Slices 패턴을 사용해서 이 문제를 해결할 수 있다.
다음의 코드에서 FishSlice, BearSlice를 생성한다.
// bearSlice.js
export const createFishSlice = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})
// fishSlice.js
export const createBearSlice = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})
FishSlice와 BearSlice를 하나의 저장소에 집어 넣는다.
import { create } from 'zustand'
import { createBearSlice } from './bearSlice'
import { createFishSlice } from './fishSlice'
export const useBoundStore = create((...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
}))
이렇게 Slice 패턴을 사용하여 Single Store 패턴을 만족시킬 수 있다.
bearSlice와 fishSlice의 상태를 addBear과 addFish를 각각 사용해서 상태를 업데이트 하는 것이 아닌, 한 번에 두 상태 변화를 동시에 발생시킬 수 있는 방법이 있다.
export const createBearFishSlice = (set, get) => ({
addBearAndFish: () => {
get().addBear()
get().addFish()
},
})
함수 생성 후, 해당 함수도 Store에 같이 바인딩 해주면 된다.
export const useBoundStore = create((...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
...createBearFishSlice(...a),
}))
여기서 StateCreator제네릭 매개변수에 대해 살펴보자.
StateCreator<MyState, Middleware, Mutator, MySlice>의 형태로 총 4개의 매개변수를 받는다.
MyState: 스토어에서 관리하는 상태의 구조를 정의한다.
Middleware: zustand에서 사용할 미들웨어를 넣는다. 미들웨어 참조 방식은 여기에서 참고하면 된다.
Mutator: 스토어 변조자의 배열
MySlice: slice의 타입을 정의한다.
//animalsSlice.ts
import { create, StateCreator } from 'zustand'
export interface BearSlice {
bears: number
addBear: () => void
eatFish: () => void
}
export interface FishSlice {
fishes: number
addFish: () => void
}
export const createBearSlice: StateCreator<
BearSlice & FishSlice,
[],
[],
BearSlice
> = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})
export const createFishSlice: StateCreator<
BearSlice & FishSlice,
[],
[],
FishSlice
> = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})
import { create } from "zustand";
import {
type BearSlice,
type FishSlice,
createBearSlice,
createFishSlice,
} from "./animalsSlice.ts";
const useBoundStore = create<BearSlice & FishSlice>()((...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
}));
상태를 가져오는 방법은 기존과 동일하다.
const { bears, eatFish } = useBoundStore();
전역 상태를 로컬 스토리지나 다른 저장 매체에 저장하고, 애플리케이션을 다시 로드할 때 상태를 유지할 수 있게 도와주는 미들웨어 이다.
사용 예시:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
// 상태 인터페이스 정의
interface BearState {
bears: number;
addABear: () => void;
}
// Zustand 스토어 생성
export const useBearStore = create<BearState>()(
persist(
(set, get) => ({
bears: 0,
addABear: () => set({ bears: get().bears + 1 }),
}),
{
name: 'food-storage',
storage: createJSONStorage(() => sessionStorage),\
},
),
);
Options
name: 저장소의 키 이름 (필수 값)
storage: 기본값은 local storage이며, 어떤 저장소를 사용할 것 인지 선택
옵션의 종류는 짧지 않기 때문에 중요한 2개만 정리하고, 나머지는 공식문서에서 확인하자.