상태관리 라이브러리 [임상태] 개발기 - 2

민순기·2023년 7월 18일
1
post-thumbnail

이번 포스팅에서는 임상태의 내부 구현에 대해 작성해보고자 한다.

임상태 깃허브 저장소 링크
임상태 npm 링크

store.ts

store.ts에서는 상태를 저장, 읽기, 쓰기 기능을 제공한다.

먼저 스토어 코드는 createStore라는 함수 내부에 전부 정의되어있다. 기본적으로 전역 상태관리라는 특성으로 인해 상태를 단일 스토어에 저장해야 하지만 경우에 따라 스토어를 여러개 생성하고 싶을때 해당 함수를 사용하여 스토어를 추가적으로 생성하면 된다.
임상태에서는 defaultStore를 제공한다.

store 내부에서는 상태를 저장하는 atomMap selectorMap, 셀렉터의 의존성 아톰을 구독하는 selectorDependencies라는 저장소가 존재한다.
createAtom createAtomFamily등의 메서드를 통해 생성된 아톰 혹은 셀렉터는 해당 map자료형에 저장된다.

createAtom

  function createAtom<Value>(atom: AtomType<Value>): AtomType<Value>;
  function createAtom<Value>(atom: SelectorType<Value>): SelectorType<Value>;
  function createAtom<Value>(atom: AtomOrSelectorType<Value>) {
    if (isSelector(atom)) {
      if (selectorMap.has(atom.key)) throw Error(`selector that has ${atom.key} key already exist`);
      return createNewSelector(atom);
    }

    if (atomMap.has(atom.key)) throw Error(`atom that has ${atom.key} key already exist`);
    return createNewAtom(atom);
  }

createAtom은 인자로 들어오는 아톰의 타입에 따라 내부 동작이 달라진다.
아톰이 들어오면 createNewAtom이 실행되고, 셀렉터가 들어오면 createNewSelector 함수가 실행되는 방식이다.

createNewAtom

  function createNewAtom<Value>(atom: AtomType<Value>) {
    const newAtom: AtomType<Value> = {
      key: atom.key,
      initialState: atom.initialState,
      options: atom.options,
    };
    atomMap.set(atom.key, { ...newAtom, state: atom.initialState });
    if (atom.options?.persistence) {
      createAtomWithPersistence(atom, newAtom);
    }
    return newAtom;
  }

createNewAtom 함수의 동작 방식은 아톰을 인자로 받아 store 내부의 atomMap에 해당 아톰을 저장하는 것이다. 만약 아톰의 옵션으로 persistence가 들어온다면 createAtomWithPersistence 함수를 실행한다.

createNewSelector

  function createNewSelector<Value>(atom: SelectorType<Value>) {
    const newSelector: SelectorType<Value> = {
      key: atom.key,
      get: atom.get,
      options: atom.options,
    };
    const state = atom.get({ get: getter<Value>(atom) });
    selectorMap.set(atom.key, { ...newSelector, state });
    if (atom.options?.persistence) {
      createAtomWithPersistence(atom, newSelector);
    }
    return newSelector;
  }

createNewAtom 함수의 동작 방식은 셀렉터를 인자로 받아 store 내부의 selectorMap에 해당 셀렉터를 저장하는 것이다. 만약 셀럭터의 옵션으로 persistence가 들어온다면 createAtomWithPersistence 함수를 실행한다.
createAtom과의 가장 큰 차이점은 getter 함수의 존재이다.

getter

  function getter<Value>(atom: SelectorType | SelectorFamilyType<Value>) {
    return <Value>(getterState: AtomOrSelectorType<Value>) => {
      // Track selector dependencies
      const dependency = selectorDependencies.get(getterState.key) || new Set();
      dependency.add(atom.key);
      selectorDependencies.set(getterState.key, dependency);

      return readAtomValue(getterState);
    };
  }

getter함수는 아톰을 인자로 받아 해당 아톰의 값을 반환한다. 셀렉터는 다른 아톰의 상태에 의존하기 때문에 셀렉터에서 다른 아톰의 상태에 접근하기 위해 해당 getter 함수를 사용한다. 또한 getter 함수의 인자로 들어온 아톰은 스토어 내부의 selectorDependencies에 저장된다.
만약 해당 아톰의 값이 변경되면 selectorDependencies도 업데이트 되어 최신 상태를 유지할 수 있다.

createAtomWithPersistence

  function createAtomWithPersistence<Value>(
    atom: AtomOrSelectorType<Value> | AtomOrSelectorFamilyType<Value>,
    newAtom: AtomOrSelectorType<Value>
  ) {
    if (!atom.options?.persistence) return;

    const atomInStorage = getParsedStorageItem(atom.options.persistence, atom.key);
    if (atomInStorage) {
      if (isSelector(atom)) {
        if (!isSelector(newAtom)) return;
        selectorMap.set(atom.key, { ...newAtom, state: atomInStorage });
      } else {
        if (isSelector(newAtom)) return;
        atomMap.set(atom.key, { ...newAtom, state: atomInStorage });
      }
      return;
    }
    if (isSelector(atom)) {
      if (!isSelector(newAtom)) return;
      const state = newAtom.get({ get: getter<Value>(newAtom) });
      setItemToStorage(atom.key, state, atom.options.persistence);
    } else {
      if (isSelector(newAtom)) return;
      setItemToStorage(atom.key, newAtom.initialState, atom.options.persistence);
    }
  }

createAtomWithPersistence 함수는 persistence 기능을 위한 함수이다. 아톰의 옵션으로 persistence가 들어온다면 해당 함수가 실행되며 아톰의 상태를 로컬스토리지에 저장한다.

createAtomFamily

  function createAtomFamily<Value, T>(atomFamily: AtomFamilyType<Value, T>): (param: T) => AtomType<Value>;
  function createAtomFamily<Value, T>(atomFamily: SelectorFamilyType<Value, T>): (param: T) => SelectorType<Value>;
  function createAtomFamily<Value, T>(atomFamily: AtomOrSelectorFamilyType<Value, T>) {
    if (isSelector(atomFamily)) {
      if (selectorMap.has(atomFamily.key)) throw Error(`selector that has ${atomFamily.key} key already exist`);
      return createNewSelectorFamily(atomFamily);
    }

    if (atomMap.has(atomFamily.key)) throw Error(`atom that has ${atomFamily.key} key already exist`);
    return createNewAtomFamily(atomFamily);
  }

createAtomFamily함수는 createAtom과 마찬가지로 인자에 따라 아톰패밀리 혹은 셀렉터 패밀리를 생성하는 함수이다.

createNewAtomFamily

  function createNewAtomFamily<Value, T>(atom: AtomFamilyType<Value, T>) {
    const newAtom: (param: T) => AtomType<Value> = (param: T) => {
      return {
        key: atom.key,
        initialState: atom.initialState(param),
        options: atom.options,
      };
    };

    return (param: T) => {
      atomMap.set(atom.key, { ...newAtom(param), state: atom.initialState(param) });
      if (atom.options?.persistence) {
        createAtomWithPersistence(atom, newAtom(param));
      }
      return newAtom(param);
    };
  }

createNewAtomFamilycreateNewAtom과 마찬가지로 새로운 아톰을 생성하고 스토어 내부의 atomMap에 해당 아톰을 저장하는 역할을 한다. 두 함수의 유일한 차이점은 createNewAtom은 값을 반환하고, createNewAtomFamily는 함수를 반환한다는 것이다.

createNewSelectorFamily

  function createNewSelectorFamily<Value, T>(atom: SelectorFamilyType<Value, T>) {
    const newSelector: (param: T) => SelectorType<Value> = (param: T) => {
      return {
        key: atom.key,
        get: atom.get(param),
        options: atom.options,
      };
    };
    return (param: T) => {
      selectorMap.set(atom.key, { ...newSelector(param), state: atom.get(param)({ get: getter<Value>(atom) }) });
      if (atom.options?.persistence) {
        createAtomWithPersistence(atom, newSelector(param));
      }
      return newSelector(param);
    };
  }

createNewSelectorFamilycreateNewSelector과 마찬가지로 새로운 셀렉터을 생성하고 스토어 내부의 selectorMap에 해당 셀렉터를 저장하는 역할을 한다. 두 함수의 차이는 createNewAtomFamilycreateNewAtom의 차이와 같다.

readAtomState

  function readAtomState<Value>(atom: AtomType<Value>): AtomWithStateType<Value>;
  function readAtomState<Value>(atom: SelectorType<Value>): SelectorWithStateType<Value>;
  function readAtomState<Value>(atom: AtomOrSelectorType<Value>) {
    if (isSelector(atom)) {
      if (!selectorMap.has(atom.key)) throw Error(`selector that has ${atom.key} key does not exist`);
      return selectorMap.get(atom.key) as SelectorWithStateType<Value>;
    }

    if (!atomMap.has(atom.key)) throw Error(`atom that has ${atom.key} key does not exist`);
    return atomMap.get(atom.key) as AtomWithStateType<Value>;
  }

readAtomState의 역할은 단순하다. 그저 인자가 아톰인지 셀렉터인지에 따라 각각의 저장소에서 상태를 꺼내오는 것이다.

readAtomValue

  function readAtomValue<Value>(atom: AtomOrSelectorType<Value> | AtomOrSelectorFamilyType<Value>) {
    if (isSelector(atom)) {
      return readAtomState(atom as SelectorType<Value>).state;
    }
    return readAtomState(atom as AtomType<Value>).state;
  }

readAtomValue는 더 단순하다. readAtomState에서 값만 꺼내온다.

writeAtomState

  function writeAtomState<Value>(targetAtom: AtomOrSelectorType<Value>, newState: Value) {
    if (isSelector(targetAtom)) {
      const currentAtom = readAtomState(targetAtom);
      selectorMap.set(targetAtom.key, { ...currentAtom, state: newState });
      if (targetAtom.options?.persistence) {
        window[targetAtom.options.persistence].setItem(targetAtom.key, JSON.stringify(newState));
      }
      updateDependencies(targetAtom);
      return readAtomState(targetAtom);
    }

    const currentAtom = readAtomState(targetAtom);
    atomMap.set(targetAtom.key, { ...currentAtom, state: newState });
    if (targetAtom.options?.persistence) {
      window[targetAtom.options.persistence].setItem(targetAtom.key, JSON.stringify(newState));
    }
    updateDependencies(targetAtom);
    return readAtomState(targetAtom);
  }

writeAtomState는 아톰의 값을 업데이트 하는 역할을 수행한다.
또한 updateDependencies함수를 실행하여 selectorDependencies의 상태도 업데이트한다.

updateDependencies

  function updateDependencies<Value>(atom: AtomOrSelectorType<Value>) {
    const dependencies = selectorDependencies.get(atom.key);
    if (dependencies) {
      dependencies.forEach((key) => {
        const dependent = selectorMap.get(key);
        if (dependent) {
          dependent.state = dependent.get({ get: readAtomValue });
        }
      });
    }
  }

여기까지 store.ts 즉 상태를 저장하는 코드는 모두 살펴보았다. 원래 stateManager.ts까지 이번 포스팅에서 다루려고 했지만 글이 너무 길어지는 것 같아서 stateManager.ts는 다음 포스팅에서 다루도록 하겠다.

profile
2년차 FE 개발자 민순기입니다.

1개의 댓글

comment-user-thumbnail
2023년 7월 18일

글 잘 봤습니다, 감사합니다.

답글 달기