npm install zustand
zustand에서 제공하는 create 함수를 호출하여 간결하게 store를 생성할 수 있다.
import { create } from 'zustand'
interface BearStoreType {
bears: number;
increaseBear: () => void;
removeAllBears: () => void;
}
export const useBearStore = create<BearStoreType>((set) => ({
bears: 0,
increaseBear: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
state값 외에 state의 상태를 업데이트하는 함수들은 하나로 묶은 후 구독하여 사용할 수도 있다.
Action을 따로 묶어서 export하면,
자동 완성 기능을 활용하여 더 쉽게 작업할 수 있고, 성능 상 문제도 없다.
interface BearStoreType {
bears: number;
actions: {
increaseBear: () => void;
removeAllBears: () => void;
};
}
const useBearStore = create<BearStoreType>((set) => ({
bears: 0,
actions: {
increaseBear: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
},
}));
export const useBearActions = () => useBearStore((state) => state.actions);
const { increaseBear } = useBearActions();
set 함수를 사용할 때, 기존 state의 불변성을 유지하는 것이 중요하다.
불변성 유지와 좀 더 간결한 set 함수 사용을 위해 zustand에서 제공하는 immer 미들웨어를 사용할 수 있다.
npm install immer로 설치 후 immer로 감싸준다.
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
// ...interface 생략
export const useBearStore = create<BearStoreType>()(
immer((set) => ({
bears: 0,
increaseBear: () =>
set((state) => {
state.bears += 1;
}),
removeAllBears: () =>
set((state) => {
state.bears = 0;
}),
})),
);
// immer 적용 전
export const useBearStore = create<BearStoreType>((set) => ({
bears: 0,
increaseBear: () => set((state) => ({ bears: state.bears + 1 })),
}));
// immer 적용 후
export const useBearStore = create<BearStoreType>()(
immer((set) => ({
bears: 0,
increaseBear: () => set((state) => { state.bears += 1 }),
})));
상태가 복잡하고 많아질수록 immer 사용의 차이를 체감할 수 있다.
컴포넌트에서 import해서 바로 사용할 수 있다.
import { useBearStore } from '@store/useBearStore';
function BearCounter() {
const bears = useBearStore((state) => state.bears);
const increaseBear= useBearStore((state) => state.increaseBear);
return (
<div>
<h1>{bears}</h1>
<button onClick={increaseBear}>one up</button>
</div>
);
}
selector 형태로 가져오지 않고, 아래처럼 호출하는 경우이다.
const { bears } = useBearStore();
이렇게 사용하는 것이 더 쉬워 보이지만, selector 없이 호출하여 사용하는 경우,
bears 뿐만 아니라 store 전체를 구독하게 된다.
전체 store를 구독하게 되면,
store에 담고 있는 모든 state가 변경될 때마다 구독 중인 컴포넌트가 불필요하게 렌더링 되기 때문에
selector 형태로 접근하여 필요한 state만 구독하는 것이 권장된다.
// Good 👍
const bears = useBearStore((state) => state.bears);
const increaseBear = useBearStore((state) => state.increaseBear);
// Bad 👎
const { bears, increaseBear } = useBearStore();
만약 여러 개의 state를 한번에 불러오고 싶다면, useShallow 훅을 사용할 수 있다.
zustand에서 제공하는 useShallow를 사용하면
전체 객체에 대해 얕은 비교를 하여, 불러온 속성들의 값이 변경되었을 때만 리렌더링 되도록 한다.
import { useShallow } from 'zustand/react/shallow';
// Object 패턴
const { bears, increaseBear } = useBearStore(
useShallow((state) => ({ bears: state.bears, increaseBear: state.increaseBear })),
)
// Array 패턴
const [bears, increaseBear] = useBearStore(
useShallow((state) => [state.bears, state.increaseBear]),
)
zustand에서 제공하는 devtools 미들웨어를 사용하면,
chrome의 redux devtools 확장 프로그램으로 상태를 직관적으로 추적할 수 있다.
import { devtools } from "zustand/middleware";
const devtoolsMiddleware = (process.env.NODE_ENV === "production"
? (fn: any) => fn
: devtools) as unknown as typeof devtools;
export default devtoolsMiddleware;
개발 환경에서만 추적할 수 있도록 설정해준다.
const useBearStore = create<BearStoreType>()(
devtoolsMiddleware(
(set) => ({
bears: 0,
actions: {
increaseBear: () => set((state) => ({ bears: state.bears + 1 }), false, 'INCREASE'),
removeAllBears: () => set({ bears: 0 }, false, 'REMOVE'),
}
}),
{ name: 'BEAR' }, //action 그룹 이름
),
);
import한 devtools로 감싸주면, 해당 state 변경 시 확장 프로그램에서 설정해준 이름으로 확인할 수 있다.
chrome 확장 프로그램 redux devtools 설치 > 개발자도구 Redux 탭에서 확인 가능하다.
zustand는 각 store를 분리해서 사용하는 것을 권장하고 있다.
store를 만들 때마다 devtools나 immer등의 미들웨어를 작성해주는 것은 번거롭기 때문에, 공용으로 빼 중복 코드를 줄일 수 있다.
//store.ts
import { create, StateCreator } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { devtools } from 'zustand/middleware';
const devtoolsMiddleware = (process.env.NODE_ENV === 'production'
? (fn: unknown) => fn
: devtools) as unknown as typeof devtools;
export const createStore = <T extends object>(
initializer: StateCreator<T, [['zustand/devtools', never], ['zustand/immer', never]]>,
options?: { name: string },
) =>
create<T, [['zustand/devtools', never], ['zustand/immer', never]]>(devtoolsMiddleware(immer(initializer), options));
export const useBearStore = createStore<BearStoreType>((set) => ({
...
}))