해당 포스트는..
이 포스튼 클라이언트 상태관리 라이브러리인 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
사용자 정의 선택기를 만들 수 있음
일반적은 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 의 스토어에 저장되는 상태를 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 에서 클라이언트 컴포넌트에서 퍼시스트를 사용하는 경우 초기 렌더링 시 서버 측에서 컴포넌트 구성요소를 렌더링하고, 그 후 클라이언트 측에서 처리해야 할 요소를 그리기 때문에, 정상적으로 퍼시스트가 적용되지 않을 수 있습니다.
다음과 같은 에러가 발생할 수 있습니다. |
|
따라서 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
'나만의 모음집' 카테고리의 다른 글
[모음집] NestJS(with TypeORM + PostgreSQL) 예제 모음집 (1) | 2024.05.17 |
---|---|
나중에 참고할 수도 있는 코드 모음집 (0) | 2024.05.08 |
[모음집] IT 용어 모음집 (0) | 2024.05.06 |
[나만의 모음집] VSCODE 사용자 코드 조각 아카이브 (0) | 2024.04.30 |
[명령어 모음집] 다양한 명령어 혹은 팁을 모아두는 아카이브 (0) | 2024.04.30 |