Devlog.
게시일

상태 관리 시스템 만들기 (atomic)

date
Apr 19, 2024
slug
state-management-atom
status
Published
tags
JavaScript
State Management
summary
자바스크립트로 전역 상태 관리 시스템 만들어보기
type
Post

원자적(Atomic) 상태 관리

중앙 집중식 상태 관리 방법은 전체 애플리케이션 상태에 대한 단일 정보 소스를 갖는 것을 중점을 두지만, 원자적 상태 관리 방법은 더 작고 독립적인 원자의 상태를 관리하는 데 중점을 둔다.
상태는 atom이라는 작고 독립적인 단위로 나뉜다. 이러한 atom에 접근해야하는 컴포넌트는 이를 구독하고 atom이 업데이트되면 다시 렌더링된다.
더 자세한 설명은 Recoil 문서의 주요 개념을 확인하자.

만들어보기

atom 구현하기

먼저 초기 값을 받는 인자가 필요하다. 그리고 atom에 읽기 및 쓰기가 가능해야하고, 해당 atom을 구독할 수 있어야 하므로 다음과 같은 인터페이스가 필요할 것이다.
const atom = (initialState) => { /* 내부 구현 */ return { read, write, subscribe }; }
이제 각 함수를 구현해보자.
const atom = (initialState) => { // atom을 구독한 listener들을 저장하는 set const listeners = new Set(); let state = initialState; // 현재 상태를 반환 const read = () => { return state; }; // 상태를 저장하고 구독자들에게 전파 const write = (nextState) => { // 이전 상태와 같은 경우 전파하지 않음 if (Object.is(state, nextState)) return; state = nextState; listeners.forEach((listener) => listener(state)); }; // 구독하는 함수, 구독을 취소하는 함수 반환 const subscribe = (listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; }; return { read, write, subscribe }; };
작성한 코드를 사용해보자.
const countAtom = atom(0); const unsubscribe = countAtom.subscribe((count) => console.log(`Count = ${count}`), ); countAtom.write(1); // Count = 1 countAtom.write(2); // Count = 2 countAtom.write(3); // Count = 3 unsubscribe(); countAtom.write(4); countAtom.write(5); countAtom.write(6); console.log(`Count = ${countAtom.read()}`); // Count = 6
결과는 다음과 같다.
notion image

selector 구현하기

selector란 다른 atom에 의존하여 새로운 파생 상태를 갖는 atom으로 읽기만 가능한 atom이다.
구현하기 전에 Recoil의 selector를 잠깐 살펴보자.
const fontSizeLabelState = selector({ key: 'fontSizeLabelState', get: ({get}) => { const fontSize = get(fontSizeState); const unit = 'px'; return `${fontSize}${unit}`; }, });
여기서 get 속성을 보면 함수로 선언된 것을 볼 수 있고, 해당 함수는 인자로 atom의 상태를 읽어오는 get이라는 함수를 받고, 새로운 상태를 반환하는 것을 확인할 수 있다.
get 함수로 상태를 읽어온 atom의 값이 변경될 때마다 파생된 상태도 변경돼야 하기 때문에 get 함수에서 해당 atom을 구독하는 로직이 필요할 것이다.
또한 읽기만 가능한 atom이므로 다음과 같은 인터페이스가 필요할 것이다.
const selector = () => { /* 내부 구현 */ return { read, subscribe }; }
이제 selector를 구현해보자.
const selector = (read) => { // 파생 상태를 저장하는 atom const _atom = atom(undefined); // 의존하는 atom들을 저장하는 set const atoms = new Set(); // 의존하는 atom의 값을 읽어오는 함수 const get = (atom) => { // 의존하는 atom이 구독되어 있지 않은 경우 if (!atoms.has(atom)) { // 해당 atom 구독, 해당 atom 업데이트 시 파생 상태도 업데이트 atom.subscribe(() => _atom.write(read(get))); atoms.add(atom); } return atom.read(); }; // 초기값 설정 _atom.write(read(get)); return { read: _atom.read, subscribe: _atom.subscribe, }; };
여기서 인자로 받는 read는 먼저 살펴봤던 Recoil의 selector에서 get 속성에 선언된 함수와 동일하고 보면 된다.
작성한 코드를 사용해보자.
const sizeAtom = atom(0); const sizeLabelAtom = selector((get) => { const fontSize = get(sizeAtom); const unit = 'px'; return `${fontSize}${unit}`; }); sizeLabelAtom.subscribe((sizeLabel) => console.log(sizeLabel)); sizeAtom.write(2); // 2px sizeAtom.write(40); // 40px sizeAtom.write(100); // 100px console.log(sizeLabelAtom.read()); // 100px
결과는 다음과 같다.
notion image

React와 통합하기

외부 저장소를 구독하는 useSyncExternalStore hook을 사용하여 React와 통합할 수 있다.
import { useSyncExternalStore } from 'react'; export const useAtomValue = (atom) => { return useSyncExternalStore(atom.subscribe, atom.read); }; export const useSetAtom = (atom) => { return (value) => { if (!('write' in atom)) { throw new Error('not writable atom'); } if (typeof value === 'function') { atom.write(value(atom.read())); } else { atom.write(value); } }; }; export const useAtom = (atom) => { return [useAtomValue(atom), useSetAtom(atom)]; };
컴포넌트 내에서 atom의 값을 사용할 때 useAtomValue를 사용하여 atom의 값을 가져오고, atom의 값을 업데이트할 때 useSetAtom을 사용하여 atom의 값을 업데이트하는 함수를 가져온다.

Reference