const useCountStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
const Counter = () => {
const count = useCountStore((state) => state.count); // count를 구독한다.
return <div>{count}</div>;
};
useStore는 create를 호출한 결과이므로, create 호출에 대해 살펴보자
export const create = (createState) => (createState ? createImpl(createState) : createImpl);
create 호출은 간단히 createImpl 호출이라고 볼 수 있다.
createImpl 함수를 한 줄 씩 살펴보자
const createImpl = (createState) => {
const api = createStore(createState);
const useBoundStore = (selector) => useStore(api, selector);
Object.assign(useBoundStore, api);
return useBoundStore;
};
createStore 함수의 내부는 이렇게 생겼다. 우선 굵직하게 살펴보자.
const createStoreImpl = (createState) => {
let state; // 1. 스토어의 상태가 선언된다.
const listeners = new Set(); // 2. 리스너를 모아두는 Set가 선언된다.
const setState = (partial, replace) => {
// 3. setState 함수가 정의된다.
const nextState = typeof partial === 'function' ? partial(state) : partial;
if (!Object.is(nextState, state)) {
const previousState = state;
state =
replace || typeof nextState !== 'object' || nextState === null
? nextState
: Object.assign({}, state, nextState);
listeners.forEach((listener) => listener(state, previousState));
}
};
const getState = () => state; // 4. getState 함수가 정의된다.
const getInitialState = () => initialState; // 5. getInitialState 함수가 정의된다.
const subscribe = (listener) => {
// 6. subscribe 함수가 정의된다. 리스너를 Set에 추가하거나 삭제하는 역할을 한다.
listeners.add(listener);
// Unsubscribe
return () => listeners.delete(listener);
};
// 7. api 객체가 선언된다.
// 이 객체는 상태 변경, 상태 조회, 초기 상태 조회, 리스너 등록을 할 수 있는 함수를 가지고 있다.
const api = { setState, getState, getInitialState, subscribe };
const initialState = (state = createState(setState, getState, api));
return api;
};
함수를 호출하여 리턴받는 api는 상태 변경
/상태 조회
/초기 상태 조회
/리스너 등록 함수
로 구성된 객체이다.
그리고, create를 호출할 때 넘겼던 ]콜백 함수(이하 StateCreator) 함수가 처음으로 호출된다.
Count 스토어를 구성할 때 넘겼던 StateCreator를 대입해 보면,
// createState(setState, getState, api)는 아래 함수를 실행하는 것과 같다.
(setState, getState, api) => ({
count: 0,
increment: () => setState((state) => ({ count: state.count + 1 })),
});
initialState와 state는 다음 객체인 것을 알 수 있다. 즉, 스토어에 첫 번째로 저장되는 상태가 결정된다.
initialState = state = {
count: 0,
increment: () => setState((state) => ({ count: state.count + 1 })),
};
const createImpl = (createState) => {
const api = createStore(createState); // 스토어에 이니셜 상태를 저장하고, 상태를 다루는 api를 받게되는 과정
const useBoundStore = (selector) => useStore(api, selector);
Object.assign(useBoundStore, api);
return useBoundStore;
};
useBoundStore는 selector와 api로 useStore를 호출할 수 있는 함수이다. 이 useBoundStore는 createImpl을 호출한 리턴값이자 useCountStore에 해당한다.
그 useCountStore를 다음과 같이 사용하게 되는데, 여기서 콜백 함수가 selector에 해당한다.
const useCountStore = create( ... );
useCountStore((state) => state.count);
// selector는 (state) => state.count 함수
이어서 createImpl 내부의 useStore를 살펴보자.
// 현재 getState의 리턴값은 count와 increment 함수를 가지고 있는 객체라고 가정한다.
export function useStore(api, selector = identity) {
const slice = React.useSyncExternalStore(
api.subscribe, // api의 subscribe 함수가 전달된다.
() => selector(api.getState()),
() => selector(api.getInitialState())
);
React.useDebugValue(slice);
// useStore의 리턴값은 스토어의 저장된 state로 selector 함수를 호출한 결과값이다. (참고: useSyncExternalStore는 스토어의 상태를 리턴한다.)
return slice;
}
따라서, 다음 두 코드는 스토어의 state에서 count와 increment 프로퍼티를 참조하는 것임을 알 수 있다.
const count = useCountStore((state) => state.count);
const increment = useCountStore((state) => state.increment);
(참고용) useSyncExternalStore를 간단히 구현한 예제
import { useEffect, useState, useRef } from 'react';
function useSyncExternalStore(subscribe, getSnapshot) {
// 현재 스토어의 스냅샷을 상태로 보관
const [snapshot, setSnapshot] = useState(getSnapshot);
// 스냅샷과 이전 상태를 비교하기 위해 참조 변수 사용
const snapshotRef = useRef(snapshot);
useEffect(() => {
// 상태가 변경될 때마다 새로운 스냅샷을 업데이트하는 함수
const checkForUpdates = () => {
const newSnapshot = getSnapshot();
if (snapshotRef.current !== newSnapshot) {
snapshotRef.current = newSnapshot;
setSnapshot(newSnapshot);
}
};
// 구독을 시작하고 컴포넌트가 마운트될 때 구독 해제 함수 반환
const unsubscribe = subscribe(checkForUpdates);
return unsubscribe;
}, [subscribe, getSnapshot]);
return snapshot;
}
-
컴포넌트 마운트
-
useCountStore를 호출
-
useSyncExternalStore를 호출
- 스토어에 저장된 상태의 스냅샷을 초깃값으로 useState로 저장
스토어에 저장된 상태의 스냅샷을 가져와서, useSyncExternalStore 내부에서 useState로 관리하는 상태와 비교하고, 다르다면 setState를 호출하는 함수
를 subscribe 호출 시 콜백으로 줌- subscribe를 호출하는 로직은 useEffect의 콜백해서 실행 (즉, return unsubsribe 하면 언마운트 시 클린업 구독해제 가능)
-
컴포넌트가 구독하는 상태가 변경됨 = 즉, api.setState 호출
- setState 내부에서는 listeners Set을 순회하며 모두 실행하는 로직이 있음
- 즉,
스토어에 저장된 상태의 스냅샷을 가져와서, useSyncExternalStore 내부에서 useState로 관리하는 상태와 비교하고, 다르다면 setState를 호출하는 함수
가 호출됨
export const setState = (partial, replace) => { const nextState = typeof partial === 'function' ? partial(state) : partial; if (!Object.is(nextState, state)) { const previousState = state; state = replace || typeof nextState !== 'object' || nextState === null ? nextState : Object.assign({}, state, nextState); listeners.forEach((listener) => listener(state, previousState)); } };
-
setState가 호출됐으므로 해당 컴포넌트는 리렌더링 대상!