이번 포스팅에서는 임상태의 내부 구현에 대해 작성해보고자 한다.
store.ts
에서는 상태를 저장, 읽기, 쓰기 기능을 제공한다.
먼저 스토어 코드는 createStore
라는 함수 내부에 전부 정의되어있다. 기본적으로 전역 상태관리라는 특성으로 인해 상태를 단일 스토어에 저장해야 하지만 경우에 따라 스토어를 여러개 생성하고 싶을때 해당 함수를 사용하여 스토어를 추가적으로 생성하면 된다.
임상태에서는 defaultStore
를 제공한다.
store 내부에서는 상태를 저장하는 atomMap
selectorMap
, 셀렉터의 의존성 아톰을 구독하는 selectorDependencies
라는 저장소가 존재한다.
createAtom
createAtomFamily
등의 메서드를 통해 생성된 아톰 혹은 셀렉터는 해당 map자료형에 저장된다.
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
함수가 실행되는 방식이다.
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
함수를 실행한다.
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
함수의 존재이다.
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
도 업데이트 되어 최신 상태를 유지할 수 있다.
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가 들어온다면 해당 함수가 실행되며 아톰의 상태를 로컬스토리지에 저장한다.
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
과 마찬가지로 인자에 따라 아톰패밀리 혹은 셀렉터 패밀리를 생성하는 함수이다.
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);
};
}
createNewAtomFamily
는 createNewAtom
과 마찬가지로 새로운 아톰을 생성하고 스토어 내부의 atomMap
에 해당 아톰을 저장하는 역할을 한다. 두 함수의 유일한 차이점은 createNewAtom
은 값을 반환하고, createNewAtomFamily
는 함수를 반환한다는 것이다.
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);
};
}
createNewSelectorFamily
는 createNewSelector
과 마찬가지로 새로운 셀렉터을 생성하고 스토어 내부의 selectorMap
에 해당 셀렉터를 저장하는 역할을 한다. 두 함수의 차이는 createNewAtomFamily
와 createNewAtom
의 차이와 같다.
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
의 역할은 단순하다. 그저 인자가 아톰인지 셀렉터인지에 따라 각각의 저장소에서 상태를 꺼내오는 것이다.
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
에서 값만 꺼내온다.
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
의 상태도 업데이트한다.
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
는 다음 포스팅에서 다루도록 하겠다.
글 잘 봤습니다, 감사합니다.