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

민순기·2023년 6월 7일
4

jotai와 clone따서 코드를 살펴보다가 비슷한 라이브러리를 문득 직접 구현해보면 코드를 이해하기 쉽겠다라는 생각이 들었다.

jotai의 인터페이스와 내부 구조를 참고해서 직접 구현해보았다. 글 작성 시점에는 어느정도의 기능 개발이 진행 된 상태이고, 추후 기능 업데이트가 있을때마다 포스팅을 추가하도록 진행할 예정이다.

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

라이브러리 이름

정말 많이 고민했는데, jotai가 일본어로 상태라는 뜻이라는 얘기를 듣고 한글로 상태라는 뜻의 이름을 지으면 어떨까 하는 생각이 들었다.
그래서 그냥 sang-tae라고 하려 했는데, 이미 같은 이름의 라이브러리가 npm에 있는지 배포가 안되서 좀 더 고민했다.
결과적으로 전역 상태 관리 -> 전상태 -> im-sang-tae -> 임상태라고 짓기로 했다.

(진짜 상태관리 라이브러리 임상태씨 만들어볼까?)

컨셉

컨셉은 jotai와 마찬가지로 아토믹 패턴을 따르고 기타 프레임워크나 라이브러리에 의존하지 않는 컨셉을 잡았다.
다만 useAtom처럼 편리하게 상태를 조작하는 api는 react코드로 되어있어 임상태에서는 이 또한 직접 구현해봤다.

라이브러리 구조

현재까지 구성된 프로젝트 구조는 다음과 같다.

[im-sang-tae]
├─ [examples] // npm을 통해 im-sang-tae를 설치해서 구현된 예제들.
└─ [src] 
│   ├─ [stateManager] 
│   │  ├─ stateManager.ts // store의 상태 변경을 감지하고 상태를 구독, 조회, 변경하는 api를 제공.
│   │  └─ index.ts
│   │
│   ├─ [store]
│   │  ├─ store.ts // 상태를 저장하는 저장소
│   │  └─ index.ts 
│   │
│   ├─ [types]
│   │   ├─ stateManager.ts
│   │   ├─ store.ts
│   │   └─ index.ts
│   │
│   ├─ [test] // 브라우저 동작 테스트를 위한 테스트 코드
│   │
│   └─ index.ts
│
├─ [test] // jest 테스트 코드
│
└─ ...package.json 등 파일들

주요 기능

임상태의 주요 기능은 다음과 같다.

  • atom 및 selector 생성
  • 인자를 받을수 있는 atomFamily 및 selectorFamily 생성
  • 옵션에 따른 persistence 처리
  • stateManager를 통한 상태 조작 api 지원

아직 완벽하게 구현되지 않았거나 추가적으로 필요한 기능들은 다음과 같다.

  • 비동기 처리 지원 (그냥 async await 사용하면 되기는 하는데... 비동기 처리를 내부에서 처리하고 사용자는 async await을 사용하지 않도록 구현하는것이 목표)
  • stateManager에서 아톰의 상태를 읽어오는 함수를 값으로 처리 (현재는 함수로 되어있다.)

기능 설명

store와 stateManager의 주요 api와 기능에 대한 설명이다.
내부 코드를 자세하게 들여다 보는것은 다음 포스팅에 할 생각이고 우선은 인터페이스를 통해 api를 알아보자.

store

// store.d.ts 
// 블로그 포스팅에는 서술하지 않은 타입도 있다. 주요 api에 대한 설명이니 그 이외에 것들은 제외하겠다.

export type Options = {
    persistence?: "localStorage" | "sessionStorage";
};

export type AtomType<Value = any> = {
    key: string;
    initialState: Value;
    options?: Options;
};
export type AtomFamilyType<Value = any, T = any> = {
    key: string;
    initialState: (param: T) => Value;
    options?: Options;
};
export type SelectorType<Value = any> = {
    key: string;
    get: ({ get }: {
        get: getter;
    }) => Value;
    options?: Options;
};
export type SelectorFamilyType<Value = any, T = any> = {
    key: string;
    get: (param: T) => ({ get }: {
        get: getter;
    }) => Value;
    options?: Options;
};
export type AtomOrSelectorType<Value = any> = AtomType<Value> | SelectorType<Value>;
export type AtomOrSelectorFamilyType<Value = any, T = any> = AtomFamilyType<Value, T> | SelectorFamilyType<Value, T>;
export type AtomMapType = Map<string, AtomType & {
    state: any;
}>;
export type SelectorMapType = Map<string, SelectorType & {
    state: any;
}>;
export type AtomWithStateType<Value> = AtomType<Value> & {
    state: Value;
};
export type SelectorWithStateType<Value> = SelectorType<Value> & {
    state: Value;
};
export interface Store {
    /**
     * Creates a new atom, throwing an error if an atom with the same key already exists.
     */
    createAtom<Value>(atom: AtomOrSelectorType<Value>): AtomOrSelectorType<Value>;
    /**
     * Creates a new atom family, throwing an error if an atom family with the same key already exists.
     */
    createAtomFamily<Value, T>(atomFamily: AtomOrSelectorFamilyType<Value, T>): (param: T) => AtomOrSelectorType<Value>;
    /**
     * Reads the current state of a given atom, throwing an error if the atom does not exist.
     */
    readAtomState<Value>(atom: AtomOrSelectorType<Value>): AtomOrSelectorType<Value>;
    /**
     * Returns the current value of a given atom.
     */
    readAtomValue<Value>(atom: AtomOrSelectorType<Value>): Value;
    /**
     * Updates the state of a given atom to a new value and updates all dependencies.
     */
    writeAtomState<Value>(targetAtom: AtomOrSelectorType<Value>, newState: Value): AtomOrSelectorType<Value>;
}

사용자가 실제로 사용하는 api는 Store라는 interface에서 제공하는 메서드들이다.

  • createAtom : 인자에 따라 selector인지 atom인지 구분하여 AtomMap 혹은 SelectorMap에 저장한다.
  • createAtomFamily : 인자에 따라 selectorFamily인지 atomFamily인지 구분하여 AtomMap 혹은 SelectorMap에 저장한다.
  • readAtomState : 인자로 받은 아톰을 store에서 꺼내와 반환한다.
  • readAtomValue : 인자로 받은 아톰을 store에서 꺼내와 현재값을 반환한다.
  • writeAtomState : 첫번째 인자로 받은 아톰의 현재 값을 두번째 인자로 받은 값으로 업데이트한다.

createAtom과 createAtomFamily

임상태는 atomselector를 모두 지원하지만, createAtom라는 메서드 하나만 가지고 atom과 selector를 생성한다.
atomFamilyselectorFamily를 생성하고 싶다면 createAtomFamily를 통하면 된다.

// atom 생성
const atom = defaultStore.createAtom({
  key: "atom",
  initialState: 1,
});

// selector 생성
const selector = defaultStore.createAtom({
  key: "selector",
  get: ({ get }) => {
    return get(atom) + 1;
  },
});

// atomFamily 생성
const atomFamily = defaultStore.createAtomFamily<number, number>({
  key: "atomFamily",
  initialState: (param) => param + 1,
});

// selectorFamily 생성
const selectorFamily = defaultStore.createAtomFamily<number, number>({
  key: "selectorFamily",
  get:
    (param) =>
    ({ get }) => {
      return get(atom) + param + 1;
    },
});

stateManager

// stateManager.d.ts 

import { AtomOrSelectorType } from "./store";
export type setStateArgument<Value> = Value | Awaited<Value> | ((prevValue: Value | Awaited<Value>) => Value | Awaited<Value>);
export interface StateManager {
    /**
     * Reads the current value of the provided atom or selector.
     * @returns a function that, when invoked, returns the current value of the atom.
     */
    atomValue<Value>(atom: AtomOrSelectorType<Value>): () => Value;
    /**
     * Creates a setter function for a provided atom.
     * The setter function updates the atom's value and triggers a rerender for all subscribers.
     */
    setAtomState<Value>(atom: AtomOrSelectorType<Value>): (argument: setStateArgument<Value>) => void;
    /**
     * Returns a tuple of getter and setter functions for a given atom.
     */
    atomState<Value>(atom: AtomOrSelectorType<Value>): [
        () => Value,
        (newValue: Value | Awaited<Value> | ((prevValue: Value | Awaited<Value>) => Value | Awaited<Value>)) => void
    ];
    /**
     * Subscribes a callback function to one or many atoms or selectors.
     * The callback is called whenever one of the subscribed atoms changes its value.
     */
    subscribe(targetAtom: AtomOrSelectorType | AtomOrSelectorType[], callback: () => void): void;
}

사용자가 실제로 사용하는 api는 StateManager라는 interface에서 제공하는 메서드들이다.

  • atomValue : 아톰의 현재 값을 store에서 가져오는 함수를 반환한다.
  • setAtomState : 아톰의 현재 값을 인자로 받은 값으로 업데이트 하는 함수를 반환한다.
  • atomState : atomValue와 setAtomState의 튜플이다.
  • subscribe : 특정 아톰 혹은 여러 아톰에 콜백을 등록하는 역할을 한다. 등록된 콜백은 setAtomState가 일어날때 실행된다.

예제

import {defaultStore, defaultStateManager} from "im-sang-tae"

const atom = defaultStore.createAtom({
  key: "atom",
  initialState: 1,
});

const [getAtom, setAtom] = defaultStateManger.atomState(atom);

defaultStateManger.subscribe(atom, () => {
	console.log(getAtom()); // 2, 3, 5
});

button.onClick = () => {
   setAtom((prev) => prev + 1);
};

지금까지 임상태의 컨셉, 구조, 주요 기능의 사용법에 대해 알아봤다. 다음 포스팅에는 임상태의 내부 구현 방식에 대해 작성할 생각이다.

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

1개의 댓글

comment-user-thumbnail
2023년 6월 8일

👏 👏 👏 👏

답글 달기