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

민순기·2023년 7월 21일
0
post-thumbnail

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

저번 포스팅에 이어 이번에는 stateManager.ts 구현부에 대해 다뤄보고자 한다.

stateManager.ts

stateManager.ts (이하 상태 매니저)에서는 상태를 읽고 업데이트하는 직관적인 인터페이스를 제공한다. 인터페이스 구조는 reactuseState를 참고했다.

상태 매니저 코드는 createStateManager라는 함수 내부에 정의되어있다. createStateManager는 인자로 store를 받기 때문에 유저가 임의로 createStore함수를 호출해서 새로 스토어를 만들면 상태 매니저도 하나 새로 만들어야 한다.
임상태에서는 defaultStateManager를 제공한다.

상태 매니저 내부에서는 상태를 읽고 수정하는 메서드와 상태 변화를 구독하는 메서드를 외부에 제공하고 내부적으로는 해당 구독자 배열과 배칭 프로세스를 처리하는 로직이 캡슐화되어있다.

내부 메서드로는 상태를 읽는 atomValue 상태를 쓰는 setAtomState 이 둘의 튜플인 atomState가 있고 상태를 구독하는 subscribe 메서드가 있다.

atomValue

  function atomValue<Value>(atom: AtomOrSelectorType<Value>) {
    return () => store.readAtomValue(atom);
  }

스토어로부터 인자로 들어온 아톰의 현재 값을 읽어오는 함수이다. useState와의 차이는 반환값이 함수라는 것인데, 이유는 상태가 변할때마다 atomValue의 리턴값이 바껴야 하기 때문에 값을 읽어올때마다 매번 함수를 실행해줘야 하기 때문이다.

setAtomState

  function setAtomState<Value>(atom: AtomOrSelectorType<Value>) {
    let newValue: Value | Awaited<Value>;

    const result = (argument: setStateArgument<Value>) => {
      if (typeof argument === "function") {
        const setter = argument as (prevValue: Value | Awaited<Value>) => Value | Awaited<Value>;
        const prevValue = store.readAtomValue(atom);
        newValue = setter(prevValue);
      } else {
        newValue = argument;
      }
      if (!atomBatchingQueue[atom.key]) atomBatchingQueue[atom.key] = [];
      atomBatchingQueue[atom.key].push({ atom, newValue });
      batching((last) => {
        store.writeAtomState(last.atom, last.newValue);
        render(last.atom);
      });
    };

    return result;
  }

setAtomState는 인자로 들어온 아톰의 값을 새로운 값으로 수정하는 함수이다. 반환값은 새로운 값을 인자로 받는 함수이다. useStatesetState처럼 인자로 함수를 받을수도 있으며 해당 함수의 인자는 이전 상태값이다. 또한 배칭 프로세스가 적용되어있는데, 이는 동시에 같은 상태를 수정하는 경우, 마지막에 일어난 수정만 일어나도록 하기 위함이다. 리액트의 useState도 이와 유사한 배칭 프로세스가 적용되어있다.
또한 상태가 업데이트 되면 render 함수를 실행하여 상태 변화를 즉각 반영한다.

render

  function render<Value>(atom: AtomOrSelectorType<Value>) {
    const listeners = subscriptions.get(atom.key);
    if (!listeners) return;

    listeners.forEach((callback) => callback());
  }

render 함수는 subscriptions에 저장된 함수를 꺼내 실행하는 역할이다. subscription에는 리액트 기준으로 컴포넌트가 저장된다고 생각하면 된다.

batching

  const atomBatchingQueue: { [key: string]: { atom: AtomOrSelectorType; newValue: any }[] } = {};

  let processingQueueFlag = false;

  function batching(process: (last: { atom: AtomOrSelectorType; newValue: any }) => void) {
    if (processingQueueFlag) return;

    processingQueueFlag = true;

    Promise.resolve().then(() => {
      const atomKeyList = Object.keys(atomBatchingQueue);

      const promises = atomKeyList.map((key) => {
        const batchList = atomBatchingQueue[key];
        atomBatchingQueue[key] = [];

        const last = batchList.pop();
        if (!last) return Promise.resolve();

        return Promise.resolve(process(last)).catch((error) => {
          console.error(`Failed to process for ${last.atom.key}:`, error);
        });
      });

      Promise.all(promises).finally(() => {
        processingQueueFlag = false;
      });
    });
  }

이것은 배칭을 처리하는 실제 코드이다. 배칭 프로세스가 아직 실행되지 않고 있을때에만 동작한다. 우선 배칭 프로세스가 한번 시작되면, 배칭이 진행되고 있는지 여부에 대한 플래그를 true로 바꾼다. 그 후 상태를 새로 쓰는 로직을 Promise를 사용하여 배칭 큐에 저장된 상태 업데이트 요청들을 마지막에 들어온 요청들만 모아놓은 비동기 처리 리스트로 만들고, 기존 배칭 큐에 저장된 값들을 비워준다. 그 후 비동기 처리를 마치면 배칭 프로세스 플래그를 false로 바꾼다.

atomState

  function atomState<Value>(
    atom: AtomOrSelectorType<Value>
  ): [
    () => Value,
    (newValue: Value | Awaited<Value> | ((prevValue: Value | Awaited<Value>) => Value | Awaited<Value>)) => void
  ] {
    return [atomValue(atom), setAtomState(atom)];
  }

atomValuesetAtomState의 튜플이다.

subscribe

  function subscribe(
    targetAtom:
      | (AtomOrSelectorType | ((param: any) => AtomOrSelectorType))
      | (((param: any) => AtomOrSelectorType) | AtomOrSelectorType)[],
    callback: () => void
  ) {
    if (Array.isArray(targetAtom)) {
      targetAtom.forEach((atom) => {
        subscribeCallbackToSingleAtom(typeof atom === "function" ? atom(null) : atom, callback);
      });
    } else {
      subscribeCallbackToSingleAtom(typeof targetAtom === "function" ? targetAtom(null) : targetAtom, callback);
    }
  }
  function subscribeCallbackToSingleAtom(atom: AtomOrSelectorType, callback: () => void) {
    const existingSubscriptions = subscriptions.get(atom.key) || [];
    subscriptions.set(atom.key, [...existingSubscriptions, callback]);
  }

subscribe 함수는 구독할 상태값 혹은 상태값의 배열과, 상태가 업데이트 되면 실행될 콜백함수를 인자로 받는다. 콜백함수에는 리액트 기준으로 컴포넌트가 들어간다고 생각하면 된다.

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

0개의 댓글