Project/JavaScript Project

[React] 전역 상태 관리 라이브러리 제작기 (1)

Paeng 2020. 11. 29. 13:32
728x90

전역 상태 관리 라이브러리 제작기 (1)

React

이전부터 '전역 상태 관리 라이브러리를 만들어보고 싶다'라는 생각을 갖고 있었습니다. 최근 Context APIRedux의 구조가 어떻게 이뤄져 있는지에 대해 알아본 뒤 전역 상태를 관리하는 흐름에 대해 대충 알게 되어서 이를 활용해 라이브러리를 제작해보려 합니다.

라이브러리 설계

Context API를 사용하지 않고 상태 관리를 하도록 작성할 예정입니다. 또한 store가 생성될 때 store에서 데이터를 갖고 있도록 하여 Provider 없이 사용 가능하도록 하려 합니다.

Provider가 없는 상태 관리에 대해서는 zustand 상태 관리 라이브러리를 보고 아이디어를 얻었습니다.

필요한 요소

상태 관리 라이브러리를 만든다고 할 때, 제대로 동작하기 위해서는 다음과 같은 기능이 필요합니다.

  1. 현재 값을 얻을 수 있어야 함.
  2. 값을 업데이트 할 수 있어야 함.
  3. 값이 변경될 때 반응할 수 있어야 함.
const store = createStore(0);

store.getValue(); => 0
store.setValue(1);
store.getValue(); => 1

간단히 위와 같이 동작할 수 있어야 합니다.

리액트에 대응

React에서는 state 또는 props가 변경되어야 리렌더링이 발생하므로 위 작업을 리액트에서 반응할 수 있도록 해야합니다.

리렌더링을 하는 가장 쉬운 방법은 state로 값을 관리하는 것입니다.

const store = createStore(0);

const Counter = () => {
  const [value, setValue] = React.useState(store.getValue());

  React.useEffect(() => {
    const clean = store.onChange((nextValue) => {
      setValue(nextValue);
    });

    // 해당 컴포넌트가 더이상 사용되지 않는다면
    // `setValue` 작업 또한 일어나지 않아도 되므로 정리합니다.
    return clean;
  }, []);
};

새로운 컴포넌트가 생성될 때마다 위 코드를 가져다 사용하는 것은 매우 비효율적이기 때문에 Custom Hook을 만들어 위 기능을 분리하도록 합니다.

const useGlobalStore = (store) => {
  const [value, setValue] = React.useState(store.getValue());

  React.useEffect(() => {
    const clean = store.onChange((nextValue) => {
      setValue(nextValue);
    });

    // 해당 컴포넌트가 더이상 사용되지 않는다면
    // `setValue` 작업 또한 일어나지 않아도 되므로 정리합니다.
    return clean;
  }, [store]);

  return [value, store.setValue];
};

위 hook을 다음과 같이 사용할 수 있습니다.

const store = createStore(0);

const Counter = () => {
  const [value] = useGlobalStore(store);

  return <p>{value}</p>
};

const CounterButton = () => {
  const [val, setVal] = useGlobalStore(store);

  return <button onClick={() => setVal(val + 1)}>+1</button>
};

개선이 필요한 부분도 보이지만, 위와 같이 중복되는 로직을 분리할 수 있습니다.

useGlobalStoreuseState와 유사합니다. 동일한 점으로는 값과 값을 수정할 수 있는 설정 함수를 반환한다는 것이며, 차이점은 useState는 로컬 상태를 생성하지만, useGlobalStore는 전역에서 관리되는 즉, 값이 공유되는 상태를 생성한다는 것입니다.

createStore function

어떻게 사용되면 좋을지 알아봤으니 store을 생성하는 createStore 함수에 대한 구현이 필요합니다.

우선, createStore의 반환 값은 다음과 같습니다.

type SetValueFunction<T = any> = (currValue: T) => T;

export interface CreateStoreReturnValue<T> {
  getValue: () => T;
  setValue: (nextValue: T) => void;
  onChange: (callback: (newValue: T) => void) => void;
}

실제 구현은 다음과 같이 될 것입니다.

const createStore = <T = any>(defaultValue: T): CreateStoreReturnValue<T> = {
  let value = defaultValue;
  const callbackList: Array<(newValue: T) => void> = [];

  const getValue = () => value;
  const setValue = (newValue: T) => {
    value = newValue;
    callbackList.forEach((callback) => callback(newValue));
  }

  // value 값이 변경될 때 실행되는 callbackList를 관리합니다.
  const onChange = (callback: (newValue: any) => void) => {
    callbackList.push(callback);

    // 추가된 callback을 제거하는 함수를 반환합니다.
    return () => {
      const idx = callbackList.findIndex(callback);
      callbackList.splice(idx, 1);
    };
  };

  return { getValue, setValue, onChange };
};
728x90
728x90