import { create } from "zustand";
interface IStore {
value: number;
setValue: (value: number) => void;
}
const useStore = create<IStore>((set) => ({
value: 0,
setValue: (value: number) => set({ value }),
}));
export const App = () => {
const value = useStore((state) => state.value);
const setValue = useStore((state) => state.setValue);
const handleValueUpdate = () => {
setValue(value + 1);
};
return (
<div>
<h1>Hello Value: {value}!!</h1>
<button onClick={handleValueUpdate}>+1</button>
</div>
);
};
버튼을 누르면 숫자가 증가하는 매우 쉬운 예제
그러나 이 예제는 이렇게도 바꿀 수 있다.
import { create } from "zustand";
interface IStore {
value: number;
setValue: (value: number) => void;
}
const useStore = create<IStore>((set) => ({
value: 0,
setValue: (value: number) => set({ value }),
}));
const handleValueUpdate = () => {
useStore.setState((state) => ({ value: state.value + 1 }));
};
export const App = () => {
const value = useStore((state) => state.value);
return (
<div>
<h1>Hello Value: {value}!!</h1>
<button onClick={handleValueUpdate}>+1</button>
</div>
);
};
handleValueUpdate
는 컴포넌트 외부에 있고, useStore.setState
는 훅을 사용하지 않는 것으로 보인다. 이들은 어떻게 반응성을 구현했을까?
버전 4.5.2를 대상으로 한다.
zustand는 코드의 길이가 매우 짧은 편에 속하지만 타입스크립트를 정말 알차게 사용해서 이해가 어려웠다.
공식 문서 의외의 글을 거의 참조하지 않아서, 잘못되거나 다른 해석이 존재할 수 있습니다.
중복해서 나오는 타입이나 개념에 대해서 미리 정리하자.
https://github.com/pmndrs/zustand/blob/v4.5.2/src/vanilla.ts#L1
type SetStateInternal<T> = {
_(
partial: T | Partial<T> | { _(state: T): T | Partial<T> }['_'],
replace?: boolean | undefined,
): void
}['_']
예시에서 나오는 useStore.setState
의 타입이다. zustand에서는 Generic T
를 항상 store
의 state
로 취급한다.
매우 복잡해 보이지만 간단하게 정리하면 다음과 같다.
type SetStateInternalSimple<T> = {
partial: T | Partial<T> | ((state: T) => T | Partial<T>);
};
이렇게 간편하게 선언할 수 있는 것을 Object 내부에 _
으로 선언하고 해당 속성을 지정해서 정의했을까?
https://blog.axlight.com/posts/why-zustand-typescript-implementation-is-so-ugly/
제작자의 블로그에 나온 이유
// 제작자 블로그의 쉬운 예제
type Test1<T> = {
_(a: T): void;
}["_"];
type Test2<T> = (a: T) => void;
type State = { name: string; age: number };
declare function run<Fn extends Test1<unknown>>(fn: Fn): void;
declare function run2<Fn extends Test2<unknown>>(fn: Fn): void;
declare const setState: Test1<State>;
declare const setState2: Test2<State>;
run(setState);
// 런타임 에러 발생
run2(setState2);
타입스크립트의 로직에 대한 일종의 우회로 보인다.
아무튼 SetStateInternal<T>
는 store
의 새로운 state
일부 혹은 전체를 받거나 함수형 업데이트를 지원하는 타입이다.
// 구현체는 이런 식으로 사용 가능
useStore.setState({
value: 0
});
useStore.setState((prev)=>({
value: prev - 1
}));
https://github.com/pmndrs/zustand/blob/v4.5.2/src/vanilla.ts#L8C1-L17C2
export interface StoreApi<T> {
setState: SetStateInternal<T>
getState: () => T
getInitialState: () => T
subscribe: (listener: (state: T, prevState: T) => void) => () => void
/**
* @deprecated Use `unsubscribe` returned by `subscribe`
*/
destroy: () => void
}
StoreApi
는 create
의 결과로 리턴되는 store 조작 메서드에 대한 타입이다.
subscribe
은 리스너 함수를 파라미터로 받고, unsubscribe
함수를 리턴한다. destroy
도 존재하지만 deprecated 상태이다.
https://github.com/pmndrs/zustand/blob/v4.5.2/src/vanilla.ts#L19
type Get<T, K, F> = K extends keyof T ? T[K] : F
K
가 T
에 존재할 경우 T[K]
가 되고 없을 경우 F
가 되는 조건부 타입이다.
더 자세히 설명하자면 Get
은 Generic으로 3개의 타입이 주어진다. K extends keyof T
은 조건부 타입을 정의한다. T
타입의 key들 중 하나에 K
가 할당할 수 있다면 T[K]
이고 아니라면 F
이다.
아래 예제를 참조하자!!
type A = {
"1": 2;
2: 4;
};
// B는 "1" | 2 이다.
type B = keyof A;
// 2는 B에 할당될 수 있기에 "yes"라는 리터럴 타입이다.
type C = 2 extends B ? "yes" : "no";
여기까진 그래도 이해할만 하다!!
https://github.com/pmndrs/zustand/blob/v4.5.2/src/vanilla.ts#L21C1-L27C14
export type Mutate<S, Ms> = number extends Ms['length' & keyof Ms]
? S
: Ms extends []
? S
: Ms extends [[infer Mi, infer Ma], ...infer Mrs]
? Mutate<StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier], Mrs>
: never
Ms
: middlewares이다.첫번째 조건문을 보자
number extends Ms['length' & keyof Ms]
은 Ms
의 length
속성이 number
인지 체크한다. 즉 arrayLike
인지 체크하기 위한 것이다.
그런데 단순히 Ms['length']
로 체크하지 않고 복잡하게 접근하는 이유는 무엇일까?
type nonArray = {
_: number;
};
// error
type B = nonArray["length"];
여기서 B
의 타입은 any이다. 타입스크립트에서는 알 수 없는 객체의 구조를 단언하지 않기에 그렇다.
그러므로 아래와 같은 경우에 문제가 발생한다.
type nonArray = {
_: number;
};
type originalCheck<T> = number extends T["length" & keyof T] ? true : false;
// 명시되지 않은 속성은 any로 취급함. length가 없다면 number는 any에 할당 가능하므로 true
type errorCheck<T> = number extends T["length"] ? true : false;
// [false, true]
type checks = [
originalCheck<nonArray>,
errorCheck<nonArray>,
];
그렇다면 왜 arrayLike
를 통해 검증하지 않을까?
아래 예제를 보면 명확하게 확인 가능하다.
type arrayLike = {
length: number;
};
type tuple = [number, number];
type nonArray = {
_: number;
};
// tuple은 거름
type originalCheck<T> = number extends T["length" & keyof T] ? true : false;
type errorCheck<T> = number extends T["length"] ? true : false;
// tuple도 포함
type otherCheck<T> = T extends ArrayLike<any> ? true : false;
// [true, false, false, true, false, true, true, true, false]
type checks = [
originalCheck<arrayLike>,
originalCheck<tuple>,
originalCheck<nonArray>,
errorCheck<arrayLike>,
errorCheck<tuple>,
errorCheck<nonArray>,
otherCheck<arrayLike>,
otherCheck<tuple>,
otherCheck<nonArray>,
];
아하 길이가 정해진 배열(튜플)에 대해서는 다른 곳에서 취급하기 위해서 였다.
그럼 두번째 조건문의 분석은 쉬워진다.
// 정확하게는 length만 정의되어도 되므로 유사 배열은 아니지만 비슷하게 보자~
export type Mutate<S, Ms> =
number extends Ms['length' & keyof Ms] // 튜플이 아닌 유사 배열인가?
? S
: Ms extends [] // Ms가 빈 튜플인가?
? S
: Ms extends [[infer Mi, infer Ma], ...infer Mrs] // Ms는 반드시 이런 형식
? Mutate<StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier], Mrs>
: never
슬슬 감이 온다.
두번째 조건문부터 세번째 조건문은 재귀적으로 타입을 정의하고 있다!!(Ms extends []
가 종료 조건)
Ms extends [[infer Mi, infer Ma], ...infer Mrs] // Ms는 반드시 이런 형식
? Mutate<StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier], Mrs>
: never
Ms
의 첫번째 요소의 첫번째 요소를 Mi
로, 두번째 요소를 Ma
로 참조한다.
Mi
: 확장된 StoreMutatorIdentifier
, StoreMutator를 식별하기 위한 키이다(미들웨어의 이름).Ma
: redux 미들웨어에서는 action 관련 제네릭을 추가해주기 위해 사용한다. Middleware Argument 같은 약자 아닐까 싶다. 즉 추가적인 타입 정보를 제공해주는 역활일듯?Mi
와 Ma
그리고 S
를 이용해서 새로운 타입(StoreMutators
)을 정의하고, 그 타입을 다시금 Mutate
의 S
로 넣어주고 튜플의 나머지 요소를 Ms
로 넣어준다.
https://github.com/pmndrs/zustand/blob/v4.5.2/src/vanilla.ts#L41C1-L43C1
export interface StoreMutators<S, A> {}
export type StoreMutatorIdentifier = keyof StoreMutators<unknown, unknown>
텅텅 비어있다. 왜 그럴까?
https://github.com/pmndrs/zustand/blob/v4.5.2/src/middleware/immer.ts#L13C1-L19C1
declare module '../vanilla' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface StoreMutators<S, A> {
['zustand/immer']: WithImmer<S>
}
}
바로 미들웨어에서 사용 예시를 볼 수 있는데, StoreMutators
의 타입을 확장시켜 원하는 대로 S
를 이용해 store의 메서드를 확장시킬 수 있다. 여기서 정황상 S
는 store의 타입(StoreApi
혹은 확장된)인 것을 유추 가능하다.
https://github.com/pmndrs/zustand/blob/v4.5.2/src/vanilla.ts#L21C1-L27C14
export type Mutate<S, Ms> = number extends Ms['length' & keyof Ms]
? S
: Ms extends []
? S
: Ms extends [[infer Mi, infer Ma], ...infer Mrs]
? Mutate<StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier], Mrs>
: never
Mutate
에 대해 정리해보겠다.
Mutate
는 2가지 제네릭을 받는다. S
는 store의 타입(StoreApi
), Ms
는 포함할 middleware들의 타입이다.Ms
가 튜플이 아니라 유사 배열이라면 S
를 그대로 사용Ms
가 빈 튜플이라면 S
를 그대로 사용Ms
가 만약 존재한다면 반드시 [Mi, Ma]
형태이고 Mi
는 StoreMutatorIdentifier
, Ma
는 제네릭을 통해 추가적인 타입 정보를 제공한다.결론적으로 이 타입은 StoreApi
를 받아 Ms
(미들웨어들의 정보)를 통해 재귀적으로 변환을 수행하는 타입이다.
https://github.com/pmndrs/zustand/blob/v4.5.2/src/react.ts#L21C1-L29C1
type ExtractState<S> = S extends { getState: () => infer T } ? T : never
type ReadonlyStoreApi<T> = Pick<StoreApi<T>, 'getState' | 'subscribe'>
type WithReact<S extends ReadonlyStoreApi<unknown>> = S & {
/** @deprecated please use api.getInitialState() */
getServerState?: () => ExtractState<S>
}
ExtractState
는 S
(store) 타입으로 부터 getState
메서드의 리턴 값을 가져온다. 즉 store의 state를 가져오는 유틸리티 타입이다.
ReadonlyStoreApi
는 T
(state) 타입으로부터 StoreApi
를 생성 후 write method를 제외해서 반환한다.
WithReact
는 현재로써 deprecated된 속성만을 포함하고 있으므로 사실상 아무런 의미가 없는 래퍼이다.
여기까지가 기본 지식.
이제 실제 코드 분석한다.
우선 스토어를 만드는 create
함수를 먼저 파악해보자.
진입점에는 아무런 내용이 없다.
export * from './vanilla.ts'
export * from './react.ts'
export { default } from './react.ts'
vanilla.ts
는 create
함수가 없으므로 넘어간다.
create
를 금방 발견했다!!
https://github.com/pmndrs/zustand/blob/v4.5.2/src/react.ts#L124C14-L124C20
export const create = (<T>(createState: StateCreator<T, [], []> | undefined) =>
createState ? createImpl(createState) : createImpl) as Create
이것만 봐서는 이해가 힘드므로 Create
, StateCreator
, createImpl
도 살펴봐야겠다.
우선 createState
가 nullable이고 조건이 존재하는 이유는 사용시에 다음과 같은 2가지 사용 사례가 있기 때문이다. 이게 2가지로 나뉘게 된 이유에 대해서는 https://docs.pmnd.rs/zustand/guides/typescript을 참조하면 좋을 듯 하다.
const useStore = create((set) => ({
bears: 0,
})) // createImpl(createState)을 호출
const useStore = create()((set) => ({
bears: 0,
})) // 반환된 createImpl을 다시 호출
아래 create
타입을 살펴보자.
https://github.com/pmndrs/zustand/blob/v4.5.2/src/react.ts#L91
type Create = {
<T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
): UseBoundStore<Mutate<StoreApi<T>, Mos>>
<T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
) => UseBoundStore<Mutate<StoreApi<T>, Mos>>
/**
* @deprecated Use `useStore` hook to bind store
*/
<S extends StoreApi<unknown>>(store: S): UseBoundStore<S>
}
Create
타입은 함수 오버로딩을 통해 구현되어 있다. 3번째는 무시하고 1번째와 2번째만 보자면 결국
(initializer: StateCreator<T, [], Mos>) => UseBoundStore<Mutate<StoreApi<T>, Mos>>
위의 사용에서 보이듯이 이 함수 자체이거나 이 함수를 반환하는 고차 함수이다.
나머지에 대해서는 뒤에서 다루겠다.
https://github.com/pmndrs/zustand/blob/v4.5.2/src/vanilla.ts#L41C1-L43C1
export type StateCreator<
T,
Mis extends [StoreMutatorIdentifier, unknown][] = [],
Mos extends [StoreMutatorIdentifier, unknown][] = [],
U = T,
> = ((
setState: Get<Mutate<StoreApi<T>, Mis>, 'setState', never>,
getState: Get<Mutate<StoreApi<T>, Mis>, 'getState', never>,
store: Mutate<StoreApi<T>, Mis>,
) => U) & { $$storeMutators?: Mos }
이제 뭔가 감이 온다.
T
는 state, Mis
는 이 create
의 내부에 위치한 미들웨어들의 타입 확장 정보들이다.
// 참고
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface BearState {
bears: number
increase: (by: number) => void
}
const useBearStore = create<BearState>()(
devtools(
persist(
(set, get) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}),
{ name: 'bearStore' },
),
),
)
Mos
는 외부에 초기화 메서드가 위치한 미들웨어들의 타입 확장 정보!! U
는 리턴 타입인데 기본적으로는 T
이다. 그러나 리턴 값을 확장하는 미들웨어가 있다면 달라질 것이다.
$$storeMutators
는 아무래도 서드파티 라이브러리를 위한 외부 미들웨어의 정보를 제공해주는 듯 혹은 그냥 에러 나지 말라고 사용하는 것일 수도 있음.
StateCreator
는 말 그대로 set
,get
,store
3가지 파라미터를 받아 state를 생성해주는 함수이다. 여기서 이 set
,get
,store
3가지는 StoreApi
와 동일하다.
https://github.com/pmndrs/zustand/blob/v4.5.2/src/react.ts#L79C1-L89C6
export type UseBoundStore<S extends WithReact<ReadonlyStoreApi<unknown>>> = {
(): ExtractState<S>
<U>(selector: (state: ExtractState<S>) => U): U
/**
* @deprecated Use `createWithEqualityFn` from 'zustand/traditional'
*/
<U>(
selector: (state: ExtractState<S>) => U,
equalityFn: (a: U, b: U) => boolean,
): U
} & S
기초 지식에서 S extends WithReact<ReadonlyStoreApi<unknown>>
가 그냥 최소한 store의 read-only 메서드들만을 포함한 것을 확인했다.
UseBoundStore
는 2가지 함수가 오버로딩 되어있다.
(): ExtractState<S>
: Store의 State를 리턴하는 함수<U>(selector: (state: ExtractState<S>) => U): U
: selector를 통해 state를 획득하는 함수UseBoundStore
는 바로 그 useStore
의 리턴 타입이다!!
const useStore = create<IStore>()((set) => ({
value: 0,
setValue: (value: number) => set({ value }),
}));
export const App = () => {
// 2가지가 오버로딩 되어있으니.
const value = useStore((state) => state.value);
const value2 = useStore().value;
// ...
}
(initializer: StateCreator<T, [], Mos>) => UseBoundStore<Mutate<StoreApi<T>, Mos>>
그럼 이 타입에은 바깥에서 받아온 외부 미들웨어의 정보(Mos
)를 포함한 StateCreator
인 initializer
로 UseBoundStore<Mutate<StoreApi<T>, Mos>>
를 정의한다.
여기서 initializer: StateCreator
의 제네릭에 <T, [], Mos>
이 들어간 이유는 create
의 외부에는 미들웨어가 위치하지 않기 때문이다. 모든 미들웨어는 이 initializer
를 가지고 확장 후 변환된 StateCreator
를 리턴한다.
UseBoundStore
의 제네릭에 Mutate<StoreApi<T>, Mos>
이 들어간 이유는 최종적인 store의 결과물의 타입의 형태는 외부 미들웨어들(Mos
)의 변환을 적용시킨 StoreApi
이기 때문이다.
https://github.com/pmndrs/zustand/blob/v4.5.2/src/middleware/immer.ts#L5C1-L11C62
immer의 타입과 사용 예시
type Immer = <
T,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = [],
>(
initializer: StateCreator<T, [...Mps, ['zustand/immer', never]], Mcs>,
) => StateCreator<T, Mps, [['zustand/immer', never], ...Mcs]>
// 예시
const useStore = create<IStore>()(
immer((set) => ({
value: 0,
setValue: (value: number) => set({ value }),
})),
);
https://github.com/pmndrs/zustand/blob/v4.5.2/src/react.ts#L104
const createImpl = <T>(createState: StateCreator<T, [], []>) => {
if (
import.meta.env?.MODE !== 'production' &&
typeof createState !== 'function'
) {
console.warn(
"[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.",
)
}
const api =
typeof createState === 'function' ? createStore(createState) : createState
const useBoundStore: any = (selector?: any, equalityFn?: any) =>
useStore(api, selector, equalityFn)
Object.assign(useBoundStore, api)
return useBoundStore
}
파라미터 createState
의 타입이 함수가 아닌 경우(직접 만든 store를 넘기는 경우?)는 현재 deprecated 됐다.
createStore
로 따라가보자.
https://github.com/pmndrs/zustand/blob/v4.5.2/src/vanilla.ts#L44C1-L52C2
https://github.com/pmndrs/zustand/blob/v4.5.2/src/vanilla.ts#L109
type CreateStore = {
<T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
): Mutate<StoreApi<T>, Mos>
<T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
) => Mutate<StoreApi<T>, Mos>
}
// ...
export const createStore = ((createState) =>
createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore
파라미터 createState
는 Create 타입
과 마찬가지로 2가지 형태로 오버로딩 되어 있다. 그러나 일반적으로는 createState
가 null인 경우가 없다. 현재는 deprecated된 context 관련 코드에서 그 흔적을 찾아볼 수 있다.
https://github.com/pmndrs/zustand/blob/v4.5.2/src/context.ts#L58
흔적
여담이지만 v5에서도 저 코드가 남아있는데, 이유를 도저히 모르겠어서 PR에 질문을 남겼다…
https://github.com/pmndrs/zustand/pull/2138#issuecomment-1987342446
미들웨어 타입스크립트를 위한 코드라고 한다.
https://github.com/pmndrs/zustand/blob/v4.5.2/src/vanilla.ts#L54C1-L59C30
https://github.com/pmndrs/zustand/blob/v4.5.2/src/vanilla.ts#L61C1-L107C2
type CreateStoreImpl = <
T,
Mos extends [StoreMutatorIdentifier, unknown][] = [],
>(
initializer: StateCreator<T, [], Mos>,
) => Mutate<StoreApi<T>, Mos>
// ...
const createStoreImpl: CreateStoreImpl = (createState) => {
type TState = ReturnType<typeof createState>
type Listener = (state: TState, prevState: TState) => void
let state: TState
const listeners: Set<Listener> = new Set()
const setState: StoreApi<TState>['setState'] = (partial, replace) => {
// TODO: Remove type assertion once https://github.com/microsoft/TypeScript/issues/37663 is resolved
// https://github.com/microsoft/TypeScript/issues/37663#issuecomment-759728342
const nextState =
typeof partial === 'function'
? (partial as (state: TState) => TState)(state)
: partial
if (!Object.is(nextState, state)) {
const previousState = state
state =
replace ?? (typeof nextState !== 'object' || nextState === null)
? (nextState as TState)
: Object.assign({}, state, nextState)
listeners.forEach((listener) => listener(state, previousState))
}
}
const getState: StoreApi<TState>['getState'] = () => state
const getInitialState: StoreApi<TState>['getInitialState'] = () =>
initialState
const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
listeners.add(listener)
// Unsubscribe
return () => listeners.delete(listener)
}
const destroy: StoreApi<TState>['destroy'] = () => {
if (import.meta.env?.MODE !== 'production') {
console.warn(
'[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected.',
)
}
listeners.clear()
}
const api = { setState, getState, getInitialState, subscribe, destroy }
const initialState = (state = createState(setState, getState, api))
return api as any
}
인라인 타입인 TState
를 정의해 state에 대한 구체적인 정보 없이도 api의 타입을 결정 지을 수 있도록 해준다. setState
는 주소 값 기준으로 비교 후 다르다면 state를 갱신하고 갱신을 전파한다. Listener
는 Set로 구현되어 subscribe
시 저장되며, 전파된다.
생각보다 핵심 구현부가 간결해서 신기했다.
https://github.com/pmndrs/zustand/blob/v4.5.2/src/react.ts#L34C1-L77C2
export function useStore<S extends WithReact<StoreApi<unknown>>>(
api: S,
): ExtractState<S>
export function useStore<S extends WithReact<StoreApi<unknown>>, U>(
api: S,
selector: (state: ExtractState<S>) => U,
): U
/**
* @deprecated The usage with three arguments is deprecated. Use `useStoreWithEqualityFn` from 'zustand/traditional'. The usage with one or two arguments is not deprecated.
* https://github.com/pmndrs/zustand/discussions/1937
*/
export function useStore<S extends WithReact<StoreApi<unknown>>, U>(
api: S,
selector: (state: ExtractState<S>) => U,
equalityFn: ((a: U, b: U) => boolean) | undefined,
): U
export function useStore<TState, StateSlice>(
api: WithReact<StoreApi<TState>>,
selector: (state: TState) => StateSlice = identity as any,
equalityFn?: (a: StateSlice, b: StateSlice) => boolean,
) {
if (
import.meta.env?.MODE !== 'production' &&
equalityFn &&
!didWarnAboutEqualityFn
) {
console.warn(
"[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937",
)
didWarnAboutEqualityFn = true
}
const slice = useSyncExternalStoreWithSelector(
api.subscribe,
api.getState,
api.getServerState || api.getInitialState,
selector,
equalityFn,
)
useDebugValue(slice)
return slice
}
조금 길긴 하지만 별 내용이 없다. create
의 결과인 훅은 최종적으로 이곳에서 만들어 진다. 위에서도 그러하듯이 equalityFn
을 직접 넘기는 것은 deprecated 됐다.
api
는 생성되어 넘겨지고, selector
가 주어지거나 안 주어지는 2가지 형태로 오버로딩 되어 있다.
// selector가 주어진 경우
const value = useStore((state) => state.value);
// selector가 주어지지 않은 경우
const value2 = useStore().value;
그리고 주어진 storeApi
의 메서드들을 리액트의 use-sync-external-store 패키지의 useSyncExternalStoreWithSelector
에 제공하여 리액트가 store의 변화에 반응하도록 한다.
해당 훅의 자세한 정보는
rfcs-0214-use-sync-external-store와
https://github.com/reactwg/react-18/discussions/86에 기원과 구현이 나온다.
selector를 넘기는 방식은 현재 리액트에서 하위호환으로 유지해주고 있기에, 라이브러리의 자체 구현으로 변경해야한다. 그러나 4.X 버전에서는 현재 구현 방식을 유지하고 5.X 버전에서 자체 구현(useMemo
를 사용한)으로 변경할 것으로 보인다(https://github.com/pmndrs/zustand/blob/v5.0.0-alpha.5/src/react.ts)
// createImple에서 useStore 사용 부
const useBoundStore: any = (selector?: any, equalityFn?: any) =>
useStore(api, selector, equalityFn)
Object.assign(useBoundStore, api)
return useBoundStore
useBoundStore
를 호출해서 리턴되는 slice
는 react state이다. store를 외부에서 접근해서 여러 메서드를 사용 가능하도록 storeApi
를 useBoundStore
에 할당해준다.
우리는 이 결과물 오브젝트를 호출하여 반응 상태를 얻거나 메서드를 호출하여 state를 조작하는 등의 일이 가능하다.
여기까지가 zustand의 핵심 코드에 대한 분석이다.간단하게 구현 부분만 요약하면 다음과 같다.
https://tkdodo.eu/blog/working-with-zustand 참조했습니다.
// 권장
const value = useStore((state) => state.value);
// 권장 안함
const value2 = useStore.getState().value;
// 출처 블로그 예시
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 a = {
get: () => 1,
};
const b = {};
Object.assign(b, a);
// false, true
console.log(Object.is(a, b), Object.is(a.get, b.get));
v5를 분석해볼걸 싶다..
의도에 대해서 좀 더 분석해보고 싶었는데, 사소한건 넘어간게 아쉽다.
왜 저런 형태로만 미들웨어를 사용하도록 강제했는지? 같은 궁금증이 생기긴 한다.
틀린 해석이나 논쟁의 여지가 있는 부분은 댓글로 남겨주시면 고민해서 반영하겠습니다.