전역 상태 store를 만든다.
아래 코드 참고
type Initializer<T> = T extends unknown ? T | ((prev: T) => T) : never;
type Store<State> = {
get: () => State
set: (action: Initializer<State>) => State
subscribe: (callback: () => void) => () => void // 변경을 감지하고 싶은 컴포넌트들의 setState 동작 등록
};
export const createStore = <State>(
initialState: Initializer<State>,
): Store<State> => {
let state = typeof initialState !== 'function' ? initialState : initialState();
// 콜백 함수를 저장하는 곳
const callbacks = new Set<() => void>();
const get = () => state;
const set = (nextState: State | ((prev: State) => State)) => {
state = typeof nextState === 'function' ? (nextState as (prev: State) => State)(state) : nextState;
// 값이 변경됐으므로 콜백 목록을 순회하면서 모든 콜백을 실행한다.
callbacks.forEach((callback) => callback());
return state;
}
//
const subscribe = (callback: () => void) => {
// 콜백 등록
callbacks.add(callback);
// 클린업 실행 시 삭제해 반복적으로 추가되는 것 방지
return () => {
callbacks.delete(callback);
}
}
return {get, set, subscribe};
}
그러나 여기 set을 써서 state 값을 바꾼다고 해서 렌더리이 일어나지는 않는다.
set 함수를 보면 state 값을 바꾼 후에 쌓아둔 콜백 함수들을 실행하는 것을 볼 수 있다.
변경을 감지하고 싶은 컴포넌트들의 useState setter 함수를 전부 돌리기 위함이다.
이 전역 변수 쓰는 모든 컴포넌트를 리렌더링 하기 위해서다.
이 로직은 useStore에 제작,
export type State = { counter: number; text: string };
const useStoreSelector = (
store: Store<State>,
selector: (state: State) => unknown, // store 값에서 어떤 값을 가져올지 정의하는 함수
) => {
const [state, setState] = useState(() => selector(store.get()));
useEffect(() => {
const unsubscribe = store.subscribe(() => {
const value = selector(store.get());
setState(value);
});
return unsubscribe;
}, [store, selector]);
return state;
};
여기서 보면 useState를 사용해서 스토어 값을 return state 해주는 것을 확인 할 수 있다. 그리고 setState를 store 콜백에 저장해주고 있다.
사용법은 아래와 같다.
// Counter.tsx
const Counter = (props: {
store: Store<{ counter: number; text: string }>;
}) => {
const { store } = props;
const counter = useStoreSelector(
store,
useCallback(state => state.counter, []),
);
function handleClick() {
store.set(prev => ({ ...prev, counter: prev.counter + 1 }));
}
return (
<>
<h1>Counter</h1>
<h3>{counter}</h3>
<button onClick={handleClick}>+</button>
</>
);
};
이렇게 되면 useStore를 사용하는 모든 컴포넌트의 전역 상태를 바라보는 지역 useState가 생긴다. 그리고 지역 useState의 setter 함수가 전역 콜백으로 들어가 전역 set 함수가 샐행될 때 실행되므로 리렌더링이 발생한다.
useCallback(state => state.counter, []),
이 친구(selector)의 정체는 만일 전역 상태가 객체일 때 객체의 일부만 사용하느 경우를 위해서 작성되었다.
{a:1,b:2}일 때 b만 사용한 경우 a가 바뀌었을 때 useState의 setter가 돌지 않도록, 즉 리렌더링을 막을 수 있게 된다.
useCallBack을 사용한 이유는 리렌더링이 되었을 경우 불필요한 동작을 막기 위해서다. useStoreSelector 안 useEffect를 보면 의존성 배열에 selector가 들어가 있다.
컴포넌트가 리렌더링 되어서 이 함수가 새로 생성이 되면 같은 로직인 함수 임에도 불구하고 useEffect가 의미없이 한 번 돌아가기 때문이다.