Zustand 더 알아보기

dev K·2024년 11월 15일
post-thumbnail
  • 기본적으로 Zustand는 셀렉터 구조로 state를 가져와 사용함
    const bears = useStore((state) => state.bears)
  • 하지만 스토어가 많이 쪼개져 있는 실무 프로젝트 구조 상에서는, 스토어에서 state와 action을 가져오는 코드가 너무 길어지고 있었음
  • 이를 개선할 수 있는 방법은?

1. Selector 구조를 더 단순하게! Auto Generating Selectors

  • Zustand 공식 문서에서 제안하는 구조
  • store에서 import해야하는 것이 많아질 경우, selector 구조를 반복적으로 사용해야하는 점을 개선
import { StoreApi, UseBoundStore } from 'zustand'

type WithSelectors<S> = S extends { getState: () => infer T }
  ? S & { use: { [K in keyof T]: () => T[K] } }
  : never

const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(
  _store: S,
) => {
  let store = _store as WithSelectors<typeof _store>
  store.use = {}
  for (let k of Object.keys(store.getState())) {
    ;(store.use as any)[k] = () => store((s) => s[k as keyof typeof s])
  }

  return store
}
  • 사용 예
export const useOrderStore = createSelectors(
  create<IOrderState>()(
    devtools(
      (set, get) => ({
        orderType: null,
        isBid: false,
        completed: false,

        setOrderType: (orderType: OrderType) => set({ orderType }),
        setIsBid: (isBid: boolean) => set({ isBid }),
        setCompleted: (completed: boolean) => set({ completed }),
      }),
      {
        name: 'useOrderStore',
      },
    ),
  ),
)

// 기존 Zustand에서 사용하는 방식대로 스토어를 create()해서 구성하고, 
// 해당 스토어를 createSelectors로 묶어서 export 해준다.

const isBid = useOrderStore.use.isBid()
const setIsBid = useOrderStore.use.setIsBid()

// 이런식으로 간단하게 state나 action을 가져다 사용할 수 있음.

2. export 되는 Actions들을 한 번에 묶어서!

  • Actions는 스토어의 값을 업데이트하는 함수이다.
  • 기존 스토어들은 Zustand의 기본 틀을 따라 create()()안에서 action도 state와 동일하게 세팅되고 그대로 selector로 가져다가 사용함
  • 이를 편리하게 하고자 스토어를 통으로 import하기도 했으나, 현재는 모두 useShallow()로 묶여 있음.
    // ❌ 스토어 사용이 가능은 하지만 모든 state가 변경될 때마다 컴포넌트를 업데이트 할 것임.
    const store = useOrderProductStore() 
  • 현재 useShallow() 사용 중
    // ✅ 객체 형태로 여러 개의 state를 가져오는 방식
    const {setName, setThumb, setBrandBgColor} = useOrderProductStore
      useShallow((state) => {
        setName: state.setName,
        setThumb: state.setThumb,
        setBrandBgColor: state.setBrandBgColor,
      })
    )
    
    // ✅ 배열 형태로 여러 개의 state를 가져오는 방식
    const [setName, setThumb, setBrandBgColor] = useOrderProductStore
      useShallow((state) => [
        state.setName,
        state.setThumb,
        state.setBrandBgColor,
      ]),
    )
  • Actions 를 묶어서 미리 export 하는 방식
    • actions는 state와 함께 묶여있기는 하지만, 정적이고 변경되지 않기 때문에 엄밀히 말하면 ‘state’가 아님.
    • 따라서 별도의 객체로 묶어서 컴포넌트에서 사용할 하나의 훅으로 export 할 수 있음.
// 스토어 세팅 
const useOrderProductStore = createSelectors(
  create<OrderProductState>()(
    devtools(
      (set, get) => ({
        name: '',
        thumb: '',
        brandBgColor: '',
        size: '',
        inventoryId: 0,
        productGrade: PRODUCT_GRADE_TRADE_CODE,

        actions: {
          setName: (name: string) => set((state) => ({ name })),
          setThumb: (thumb: string) => set((state) => ({ thumb })),
          setBrandBgColor: (brandBgColor: string) => set((state) => ({ brandBgColor })),
          setSize: (size: string) => set((state) => ({ size })),
          setInventoryId: (inventoryId: number) => set((state) => ({ inventoryId })),
          setProductGrade: (productGrade) => set(() => ({ productGrade })),
        },
      }),
      {
        name: 'useOrderProductStore',
      },
    ),
  ),
)

// ✅ 스토어에서 selector를 하나의 훅으로 export
export const useOrderProductStoreActions = () => useOrderProductStore((state) => state.actions)

****
// ✅ 컴포넌트에서 import 하여 사용(구조분해 할당 가능)
const { setName, setThumb, setBrandBgColor, setSize, setProductGrade } =
    useOrderProductStoreActions()

3. Flux 패턴을 사용하고 싶은 경우!

  • redux와 유사하게 reducer 패턴을 사용하고 싶은 경우, store의 최상단에 dispatch 함수를 정의할 수 있음

const types = { increase: 'INCREASE', decrease: 'DECREASE' }

const reducer = (state, { type, by = 1 }) => {
  switch (type) {
    case types.increase:
      return { grumpiness: state.grumpiness + by }
    case types.decrease:
      return { grumpiness: state.grumpiness - by }
  }
}

const useGrumpyStore = create((set) => ({
  grumpiness: 0,
  dispatch: (args) => set((state) => reducer(state, args)),
}))

const dispatch = useGrumpyStore((state) => state.dispatch)
dispatch({ type: types.increase, by: 2 })

Reference

https://tkdodo.eu/blog/working-with-zustand

https://docs.pmnd.rs/zustand/guides/practice-with-no-store-actions

https://docs.pmnd.rs/zustand/guides/auto-generating-selectors

https://docs.pmnd.rs/zustand/guides/flux-inspired-practice#redux-like-patterns

profile
🪐

0개의 댓글