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를 분석해볼걸 싶다..
의도에 대해서 좀 더 분석해보고 싶었는데, 사소한건 넘어간게 아쉽다.
왜 저런 형태로만 미들웨어를 사용하도록 강제했는지? 같은 궁금증이 생기긴 한다.
틀린 해석이나 논쟁의 여지가 있는 부분은 댓글로 남겨주시면 고민해서 반영하겠습니다.