전역 상태 관리

Viking_J·2024년 10월 15일

결론:

컴포넌트 내부에서만 사용할 수 있는 useState를 극복하기 위해 전역상태관리 라이브러리 등장. 작동방식은 컴포넌트 밖 어딘가에 상태를 둔다.(컴포넌트 최상단 또는 격린된 파일) 그리고 이 외부 상태 변경을 각자의 방식으로 감지해 컴포넌트를 렌더링 한다. 예를 들면 변경이 되면 useState의 set 함수를 돌려 렌더링 시킨다.


전역 상태 관리 조건 3가지

  1. 여러 컴포넌트에서 접근 가능해야 한다.
  2. 상태를 바꾸면 이 상태를 가지고 있는 모든 컴포넌트에서 리렌더링이 일어나야 한다.
  3. 상태가 객체인 경우, 예를 들어 {a:1,b:2} 경우 a만을 사용하는 컴포넌트는 b가 변경 되었을 때 렌더링이 일어나서는 안 된다.

직접 구현

전역 상태 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가 의미없이 한 번 돌아가기 때문이다.

profile
모험을 떠나보자

0개의 댓글