본문 바로가기

나만의 모음집

[모음집] Zustand 모음집

반응형

해당 포스트는..

이 포스튼 클라이언트 상태관리 라이브러리인 Zustand 에 대한 일부 팁이나 내용들에 대한 것들을 모음집 형태로 모아두기 위한 포스트 입니다. 추가된 내용이 있으면 당일 날짜로 업데이트 됩니다.

 

저장소 내에 꼭 상태와 업데이트 함수를 같이 둘 필요는 없음

일반적으로 Zustand 를 사용 시 스토어를 정의하면

일반적인 방식으로 스토어를 정의하면 아래와 같이 상태와 업데이트 함수가 하나의 스토어에 같이 위치하고 있습니다.이 방식은 권장하고 있기도 하구요.

export const useBoundStore = create((set) => ({
  count: 0,
  text: 'hello',
  inc: () => set((state) => ({ count: state.count + 1 })),
  setText: (text) => set({ text }),
}))

 

외부에서 상태 업데이트 함수를 정의 후 주입하면

그런데, Zustand 에서는 외부에서 setState 를 정의하여 이를 스토어에 주입하여 처리하는 패턴도 가능하다고 합니다. 바로 아래와 같이 말이죠.

export const useBoundStore = create(() => ({
  count: 0,
  text: 'hello',
}))

export const inc = () =>
  useBoundStore.setState((state) => ({ count: state.count + 1 }))

export const setText = (text) => useBoundStore.setState({ text })

 

 

이 이후에는 어떻게 사용되는지 예시가 나오지 않아서 별도로 테스트 해보고 사용 예시를 아래와 같이 추가해 보았습니다. 이렇게 보니 상태 변수에 접근하는 방식이 단순화되고, 각 setState 함수를 별도로 import 하여 사용하는 것은 좋아보이지만, store 의 개수가 많아질수록 각 setState 가 어떤 store 객체를 업데이트하는지 알 수 없기 때문에, 유지보수가 힘들지 않을까 하는 생각도 드네요.

import { useBoundStore, inc, setText } from './yourFile.js';

// 상태 조회
console.log(useBoundStore.getState()); // { count: 0, text: 'hello' }

// 액션 호출
inc(); // count를 1 증가시킴
setText('new text'); // text를 'new text'로 설정

// 변경된 상태 확인
console.log(useBoundStore.getState()); // { count: 1, text: 'new text' }

 

 

아무튼, 이러한 사용법도 있다는 것을 알아보는 시간이었습니다.

 

참고자료

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

 

Zustand Documentation

Zustand is a small, fast and scalable bearbones state-management solution, it has a comfy api based on hooks

docs.pmnd.rs

 

사용자 정의 선택기를 만들 수 있음

일반적은 store 접근 방식

보통 우리가 Zustand 의 store에 접근하기 위해서는 흔히 아래와 같이 접근을 많이합니다. 이것이 기본 동작이기도 하죠.

const bears = useBearStore((state) => state.bears)

 

 

그러나 이 부분을 더 간소화 시킬 수 있는 방법이 있습니다. 이는 Zustand 공식문서(https://docs.pmnd.rs/zustand/guides/auto-generating-selectors)에 친절하게 설명이 나와 있는 부분입니다.

 

커스텀 선택기를 이용한 store 접근 방식

아래 타입스크립트 코드를 복사 붙여넣기 합니다. 해당 예제는 앞서 링크한 주소로 들어가시면 바로 확인할 수 있습니다. 

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
}

 

만일 아래와 같은 store 를 정의해서 사용하고 있다고 가정해봅시다.

interface BearState {
  bears: number
  increase: (by: number) => void
  increment: () => void
}

const useBearStoreBase = create<BearState>()((set) => ({
  bears: 0,
  increase: (by) => set((state) => ({ bears: state.bears + by })),
  increment: () => set((state) => ({ bears: state.bears + 1 })),
}))

 

그리고, 앞서 생성했던 커스텀 선택기 함수의 인자로 해당 userBearStoreBase 를 전달해줍니다.

const useBearStore = createSelectors(useBearStoreBase)

 

그 다음에는 useBearStore 를 내보내기 한 후, 사용하고자 하는 컴포넌트에서 아래와 같이 사용하면 끝입니다. 별도의 콜백함수 호출 없이도 .use.bear() 와 같이 접근이 가능해졌습니다. 

// get the property
const bears = useBearStore.use.bears()

// get the action
const increment = useBearStore.use.increment(

 

한 가지 특이한 점이 있다면 bears 라는 상태 변수를 접근할 때 bears() 와 같이 작성해주어야 한다는 점입니다. 이는 제가 보기로는 store.use 에 접근 시 내부적으로 콜백함수를 정의하고 있기 때문에 이를 호출해주어야 상태에 접근할 수 있기 때문이 아닌가 싶습니다.

 

전역 비동기 상태관리

zustand 는 비동기 처리(ex. fetch ) 를 전역적으로 사용할 수 있도록 비동기 함수 또한 생성할 수 있습니다. 보통 리액트의 useState 를 활용한다면, 커스텀을 통해서 loading, error 등을 처리할 수 있겠지만, zustand 는 그 보다 더 쉽고 효율적인 방식으로 관리할 수 있도록 해줍니다.

import create from 'zustand';

const useStore = create((set) => ({
  data: null,
  loading: false,
  error: null,
  fetchData: async () => {
    set({ loading: true, error: null });
    try {
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      set({ data, loading: false });
    } catch (error) {
      set({ error, loading: false });
    }
  }
}));

// 컴포넌트에서 상태 사용
function DataFetcher() {
  const { data, loading, error, fetchData } = useStore();

  React.useEffect(() => {
    fetchData();
  }, [fetchData]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return <div>{JSON.stringify(data)}</div>;
}

export default DataFetcher;

 

상태 퍼시스트 | 로컬스토로지 캐싱 활용

 

Zustand Documentation

Zustand is a small, fast and scalable bearbones state-management solution, it has a comfy api based on hooks

docs.pmnd.rs

 

zustand 의 스토어에 저장되는 상태를 LocalStorage 에 저장하여 새로고침 이후에도 해당 데이터를 유지할 수 있는 캐싱 처리를 돕습니다. 여기서 로컬스토로지는 실제 브라우저의 로컬스토로지와 같습니다.

import create from 'zustand';
import { persist } from 'zustand/middleware';

// 퍼시스트 미들웨어 사용
const useStore = create(persist((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 }))
}), {
  name: 'counter-storage', // 로컬 스토리지 키
}));

function Counter() {
  const { count, increment, decrement } = useStore();
  
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

export default Counter;

 

NextJS 에서 퍼시스트 사용

NextJS 에서 클라이언트 컴포넌트에서  퍼시스트를 사용하는 경우 초기 렌더링 시 서버 측에서 컴포넌트 구성요소를 렌더링하고, 그 후 클라이언트 측에서 처리해야 할 요소를 그리기 때문에, 정상적으로 퍼시스트가 적용되지 않을 수 있습니다.

다음과 같은 에러가 발생할 수 있습니다.
  • 텍스트 내용이 서버에서 렌더링된 HTML과 일치하지 않습니다.
  • 초기 UI가 서버에서 렌더링된 것과 일치하지 않아 하이드레이션에 실패했습니다.
  • 수분 공급 중에 오류가 발생했습니다. 오류가 Suspense 경계 외부에서 발생했기 때문에 전체 루트가 클라이언트 렌더링으로 전환됩니다.

 

따라서 NextJS 에서는 서버 측 처리가 완전히 완료될 때 까지 기다릴 수 있도록 별도의 커스텀 훅을 작성해 주어야 합니다. 이렇게만 해두면 zustand 는 해당 스토어를 선언적으로 캐치 후 기존 스토어를 대체하여 사용하게됩니다. 

// useStore.ts
import { useState, useEffect } from 'react'

const useStore = <T, F>(
  store: (callback: (state: T) => unknown) => unknown,
  callback: (state: T) => F,
) => {
  const result = store(callback) as F
  const [data, setData] = useState<F>()

  useEffect(() => {
    setData(result)
  }, [result])

  return data
}

export default useStore

 

그 다음에는 기존과 동일하게 퍼시스트를 적용하면 됩니다.

// useBearStore.ts

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

// the store itself does not need any change
export const useBearStore = create(
  persist(
    (set, get) => ({
      bears: 0,
      addABear: () => set({ bears: get().bears + 1 }),
    }),
    {
      name: 'food-storage',
    },
  ),
)

 

이제 사용하고자 하는 컴포넌트에서 동일한 방식으로 사용하기만 하면 끝입니다.

// yourComponent.tsx

import useStore from './useStore'
import { useBearStore } from './stores/useBearStore'

const bears = useStore(useBearStore, (state) => state.bears)

 

참고자료

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

 

Zustand Documentation

Zustand is a small, fast and scalable bearbones state-management solution, it has a comfy api based on hooks

docs.pmnd.rs

 

반응형