zustand와 react query는 둘 다 여러 개의 전역 스토어를 지향합니다.
두 라이브러리를 같이 잘 사용하는 방법을 배워봅시다.
대부분의 아이디어는 아래 글에서 가져왔습니다.
https://tkdodo.eu/blog/working-with-zustand
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)
const { increasePopulation } = useBearActions()
위의 "원자 셀렉터(아토믹 셀렉터)" 팁과 모순되는 것처럼 들릴 수 있지만 실제로는 그렇지 않습니다.
액션은 절대 변하지 않기 때문에 우리가 "모두"를 구독하는 것은 중요하지 않습니다.(성능적 영향 x)
action 객체 전체를 단일 원자 조각으로 볼 수 있습니다.
액션을 이벤트로 모델링
액션은 세터(setter)가 아닙니다.
이것은 useReducer, Redux 또는 Zustand로 작업하는지 여부에 관계없이 일반적인 팁입니다.
사실 이 팁은 훌륭한 Redux style guide에서 직접 가져온 것입니다.
컴포넌트가 아닌 스토어 내부에 비즈니스 로직을 유지하는 데 도움이 됩니다.
이전의 예제는 이미 이 패턴을 사용하고 있습니다.
로직(예: "인구 증가")은 스토어에 있습니다.
컴포넌트는 액션을 호출하고 스토어는 이를 사용하여 수행할 액션을 결정합니다.
스토어의 스코프를 작게 유지
const currentUser = useCredentialsStore((state) => state.currentUser)
const user = useUsersStore((state) => state.users[currentUser])
참고: Zustand에는 스토어를 슬라이스를 사용해 결합하는 방법(combine stores into slices)이 있지만
저는 그런 방법이 필요하지 않았습니다.
특히 TypeScript가 관련된 경우에는 매우 간단해 보이지 않습니다.
특별히 해당 기능이 필요한 경우 Redux Toolkit을 사용할 것입니다.
다른 라이브러리와 같이 사용하기
다시 한 번 동일한 원칙이 적용됩니다.
다른 훅을 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의 어느 곳에서나 사용할 수도 있습니다.
'FrontEnd' 카테고리의 다른 글
Vue3의 반응성 완벽 이해 2편 : ref와 computed (0) | 2022.12.11 |
---|---|
Vue3의 반응성 완벽 이해 1편 : 종속성 추적 (4) | 2022.12.10 |
[번역] Vue3 Coding Better Composables : await 없이 비동기를 처리하는 컴포저블 잘만들기 (0) | 2022.12.09 |
[번역] Vue3 Coding Better Composables : 인터페이스를 잘 설계하여 컴포저블 잘 만들기 (0) | 2022.12.09 |
[번역] Vue3 Coding Better Composables : 다형적인 리턴값을 활용하여 컴포저블 잘만들기 (0) | 2022.12.08 |