jotai와 clone따서 코드를 살펴보다가 비슷한 라이브러리를 문득 직접 구현해보면 코드를 이해하기 쉽겠다라는 생각이 들었다.
jotai의 인터페이스와 내부 구조를 참고해서 직접 구현해보았다. 글 작성 시점에는 어느정도의 기능 개발이 진행 된 상태이고, 추후 기능 업데이트가 있을때마다 포스팅을 추가하도록 진행할 예정이다.
정말 많이 고민했는데, 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 등 파일들
임상태의 주요 기능은 다음과 같다.
아직 완벽하게 구현되지 않았거나 추가적으로 필요한 기능들은 다음과 같다.
store와 stateManager의 주요 api와 기능에 대한 설명이다.
내부 코드를 자세하게 들여다 보는것은 다음 포스팅에 할 생각이고 우선은 인터페이스를 통해 api를 알아보자.
// 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
: 첫번째 인자로 받은 아톰의 현재 값을 두번째 인자로 받은 값으로 업데이트한다.임상태는 atom과 selector를 모두 지원하지만, createAtom
라는 메서드 하나만 가지고 atom과 selector를 생성한다.
atomFamily와 selectorFamily를 생성하고 싶다면 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.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);
};
지금까지 임상태의 컨셉, 구조, 주요 기능의 사용법에 대해 알아봤다. 다음 포스팅에는 임상태의 내부 구현 방식에 대해 작성할 생각이다.
👏 👏 👏 👏