본문 바로가기

FrontEnd

zustand와 react query를 같이 사용하는 방법

반응형

zustand와 react query는 둘 다 여러 개의 전역 스토어를 지향합니다.

두 라이브러리를 같이 잘 사용하는 방법을 배워봅시다.

대부분의 아이디어는 아래 글에서 가져왔습니다.

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

 

Working with Zustand

Let's dive into some tips for working with Zustand - one of my favourite client state management libraries in React.

tkdodo.eu

Zustand

  • 전역 스토어를 제공하며 셀럭터를 포함한 간단한 API를 포함합니다.
  • 용량도 매우 작습니다.(1.1kb)
  • 개념적으로 리덕스와 비슷합니다.
  • 사용중인 필드를 셀렉터로 추적해야 합니다.
    • 이는 매우 좋습니다. 의존성에 대해 명시적이 되도록 강요하기 때문입니다.
    • 앱이 커질 수록 보상받는 아키텍처 입니다.

 Zustand를 잘 활용하는 방법들은 다음과 같습니다.

주 : Redux와 Zustand 모두 개념적으로, 재사용성(모듈화)의 단위를 Slice로 취급함을 명심하세요.
Store를 합쳐 전역 Store를 만드는 것이 아니라,
Slice를 단일 스토어로 결합하는 것입니다.
Store의 데이터가 아니라 Slice의 메서드에 집중해야 합니다.

반드시 커스텀 훅만 export

zustand를 넘어서 리액트 사용 시 명심해야할 넘버 원 팁입니다.

자세한 내용 참고 : advantages of custom hooks

반응형
// ⬇️ not exported, so that no one can subscribe to the entire store
const useBearStore = create((set) => ({
  bears: 0,
  fish: 0,
  increasePopulation: (by) => set((state) => ({ bears: state.bears + by })),
  eatFish: () => set((state) => ({ fish: state.fish - 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

// 💡 exported - API 컨슈머가 셀렉터를 작성할 필요 없음
export const useBears = () => useBearStore((state) => state.bears)
  • 더 깨끗한 인터페이스를 제공합니다.
  • 모든 곳에서 셀렉터를 반복적으로 작성할 필요가 없습니다.
  • 또한 실수로 전체 스토어를 구독하는 것을 방지합니다.

아래 코드는 위와 결과가 동일하지만, 전체 스토어를 구독하여 불필요한 리렌더링이 많이 일어납니다.

셀렉터 사용은 선택 사항이지만, 반드시 사용해야 한다고 생각합니다.

// ❌ we could do this if useBearStore was exported
const { bears } = useBearStore()

원자적 셀렉터 사용

해당 문서(docs)에서 잘 설명되어 있습니다만,

내용을 잘못 이해하면 렌더링 성능의 저하를 가져올 수 있으니 주의해야 합니다.

 

Zustand는 셀렉터의 리턴값을 이전 렌더링 시 리턴값와 비교하여,
관심 있는 상태가 변경되었음을 컴포넌트에 알릴 시기를 결정합니다.
기본적으로 엄격한 동등성 검사(strict equality check)를 통해 이를 수행합니다.

 

이는 셀렉터가 안정적인 결과를 반환해야 함을 의미합니다.
새 배열 또는 객체를 반환하면 내용이 동일하더라도 항상 변경으로 간주됩니다.

// 🚨 selector returns a new Object in every invocation
const { bears, fish } = useBearStore((state) => ({
  bears: state.bears,
  fish: state.fish,
}))

// 😮 so these two are equivalent
const { bears, fish } = useBearStore()

셀렉터에서 객체 또는 배열을 반환하려는 경우
얕은 비교를 사용하도록 비교 함수를 조정할 수 있습니다. (조금 더 최적화 됨)

import shallow from 'zustand/shallow'

// ⬇️ much better, because optimized
const { bears, fish } = useBearStore(
  (state) => ({ bears: state.bears, fish: state.fish }),
  shallow
)
그러나 저는 두 개의 아토믹한 개별 셀렉터를 내보내는 단순함을 훨씬 선호합니다.
export const useBears = () => useBearStore((state) => state.bears)
export const useFish = () => useBearStore((state) => state.fish)
컴포넌트에 실제로 두 값이 모두 필요한 경우 두 훅을 모두 사용하세요

액션과 상태 분리

액션은 스토어의 값을 업데이트하는 함수입니다.
이들은 정적이며 변경되지 않으므로 기술적으로 "상태"가 아닙니다.
스토어에서 액션을 별도의 객체로 구성하면 성능에 영향을 주지 않으며 모든 컴포넌트에서 사용할 단일 훅으로 노출할 수 있습니다.
const useBearStore = create((set) => ({
  bears: 0,
  fish: 0,
  // ⬇️ separate "namespace" for actions
  actions: {
    increasePopulation: (by) =>
      set((state) => ({ bears: state.bears + by })),
    eatFish: () => set((state) => ({ fish: state.fish - 1 })),
    removeAllBears: () => set({ bears: 0 }),
  },
}))

export const useBears = () => useBearStore((state) => state.bears)
export const useFish = () => useBearStore((state) => state.fish)

// 🎉 one selector for all our actions
export const useBearActions = () => useBearStore((state) => state.actions)​
이제 액션을 구조 분해하고 그 중 하나만 "사용(use)"해도 괜찮습니다.
const { increasePopulation } = useBearActions()

위의 "원자 셀렉터(아토믹 셀렉터)" 팁과 모순되는 것처럼 들릴 수 있지만 실제로는 그렇지 않습니다.
액션은 절대 변하지 않기 때문에 우리가 "모두"를 구독하는 것은 중요하지 않습니다.(성능적 영향 x)
action 객체 전체를 단일 원자 조각으로 볼 수 있습니다.


액션을 이벤트로 모델링

액션은 세터(setter)가 아닙니다.

 

이것은 useReducer, Redux 또는 Zustand로 작업하는지 여부에 관계없이 일반적인 팁입니다.
사실 이 팁은 훌륭한 Redux style guide에서 직접 가져온 것입니다.
컴포넌트가 아닌 스토어 내부에 비즈니스 로직을 유지하는 데 도움이 됩니다.

 

이전의 예제는 이미 이 패턴을 사용하고 있습니다.
로직(예: "인구 증가")은 스토어에 있습니다.
컴포넌트는 액션을 호출하고 스토어는 이를 사용하여 수행할 액션을 결정합니다.


스토어의 스코프를 작게 유지

전체 앱에 대해 단일 스토어를 가져야 하는 Redux와 달리
Zustand는 여러 개의 작은 스토어를 가질 것을 권장합니다.
각 스토어는 단일 상태를 담당할 수 있습니다.
그것들을 결합해야 하는 경우, 물론 사용자 정의 훅을 사용하여 결합할 수 있습니다.
const currentUser = useCredentialsStore((state) => state.currentUser)
const user = useUsersStore((state) => state.users[currentUser])​

참고: Zustand에는 스토어를 슬라이스를 사용해 결합하는 방법(combine stores into slices)이 있지만
저는 그런 방법이 필요하지 않았습니다.
특히 TypeScript가 관련된 경우에는 매우 간단해 보이지 않습니다.
특별히 해당 기능이 필요한 경우 Redux Toolkit을 사용할 것입니다.


다른 라이브러리와 같이 사용하기

앱의 대부분의 상태는 서버 또는 URL 상태이기 때문에
실제로 여러 Zustand 스토어를 자주 결합할 필요가 없었습니다.
 
예를 들어 Zusstand 저장소를 useQuery 또는 useParams와 결합할 가능성이
두 개의 개별 스토어를 결합할 가능성보다 훨신 큽니다.

다시 한 번 동일한 원칙이 적용됩니다.
다른 훅을 Zustand 스토어와 결합해야 하는 경우 커스텀 훅이 가장 좋은 친구입니다.

const useFilterStore = create((set) => ({
  applied: [],
  actions: {
    addFilter: (filter) =>
      set((state) => ({ applied: [...state.applied, filter] })),
  },
}))

export const useAppliedFilters = () =>
  useFilterStore((state) => state.applied)

export const useFiltersActions = () =>
  useFilterStore((state) => state.actions)

// 🚀 combine the zustand store with a query
export const useFilteredTodos = () => {
  const filters = useAppliedFilters()
  return useQuery({
    queryKey: ['todos', filters],
    queryFn: () => getTodos(filters),
  })
}
여기서 적용된 필터는 쿼리 키의 일부이기 때문에 쿼리를 동작하게 만듭니다.
  • addFilter 함수는 UI의 어느 곳에서나 사용할 수 있으며, 해당 함수를 호출할 때마다 자동으로 새 쿼리를 트리거합니다.
  • 트리거 대상 쿼리 또한 UI의 어느 곳에서나 사용할 수도 있습니다.
반응형