Skip to content

Latest commit

 

History

History
216 lines (162 loc) · 7.41 KB

3주차_성지현_개인.md

File metadata and controls

216 lines (162 loc) · 7.41 KB

상태 구독 메커니즘과 subscribe 메서드 구현 분석

상태 구독하기

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의 의미

useStore는 create를 호출한 결과이므로, create 호출에 대해 살펴보자

export const create = (createState) => (createState ? createImpl(createState) : createImpl);

create 호출은 간단히 createImpl 호출이라고 볼 수 있다.

createImpl 호출의 의미

createImpl 함수를 한 줄 씩 살펴보자

const createImpl = (createState) => {
  const api = createStore(createState);

  const useBoundStore = (selector) => useStore(api, selector);

  Object.assign(useBoundStore, api);

  return useBoundStore;
};

1. const api = createStore(createState);

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;
};

2. const useBoundStore = (selector) => useStore(api, selector);

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;
}
  1. 컴포넌트 마운트

  2. useCountStore를 호출

  3. useSyncExternalStore를 호출

    1. 스토어에 저장된 상태의 스냅샷을 초깃값으로 useState로 저장
    2. 스토어에 저장된 상태의 스냅샷을 가져와서, useSyncExternalStore 내부에서 useState로 관리하는 상태와 비교하고, 다르다면 setState를 호출하는 함수를 subscribe 호출 시 콜백으로 줌
      1. subscribe를 호출하는 로직은 useEffect의 콜백해서 실행 (즉, return unsubsribe 하면 언마운트 시 클린업 구독해제 가능)
  4. 컴포넌트가 구독하는 상태가 변경됨 = 즉, 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));
      }
    };
  5. setState가 호출됐으므로 해당 컴포넌트는 리렌더링 대상!