아키텍처
이 페이지는 Hurum의 내부 구조를 설명해요. Hurum을 사용하는 데 이 정보가 필요하지는 않지만, 디버깅, 기여, 또는 통합을 구축할 때 도움이 돼요.
패키지 구조
섹션 제목: “패키지 구조”Hurum은 엄격한 의존성 방향을 가진 두 패키지로 제공돼요:
@hurum/react → @hurum/core (peer dep) (zero deps)@hurum/core
섹션 제목: “@hurum/core”프레임워크에 구애받지 않는 상태 머신 런타임이에요. 런타임 의존성이 없어요. CJS + ESM 듀얼 빌드.
| 모듈 | 역할 |
|---|---|
events.ts | Events() / Event() 팩토리, Event 타입 브랜딩 |
command-executor.ts | CommandExecutor() 팩토리, passthrough 단축키, Executor 컨텍스트 |
intent.ts | Intent() / Intents() / Intent.all() / Intent.allSettled(), PreparedIntent |
store.ts | Store() 빌더, createStoreInstance(), Nested 관리, relay, Computed, 구독 |
computed.ts | 프록시 기반 의존성 추적, 구조적 동등성 |
nested.ts | Nested() / Nested.array() / Nested.map() 마커 |
selector.ts | Selector 타입과 isSelector 가드 |
middleware.ts | Middleware / MiddlewareFactory 타입 |
middleware/ | 내장 middleware: logger, persist, devtools, undoRedo |
types.ts | 유틸리티 타입: StoreOf, StateOf, RawStateOf, DepsOf, DetectConflicts |
@hurum/react
섹션 제목: “@hurum/react”얇은 React 바인딩 레이어예요. React 18+와 @hurum/core에 peer dependency를 가져요.
| 모듈 | 역할 |
|---|---|
use-store.ts | useStore() 훅 — 컨텍스트 인식: StoreProvider에서 스코프된 인스턴스를 해석하거나 싱글턴으로 폴백 |
use-selector.ts | useSelector() — 커스텀 Selector를 사용하는 useSyncExternalStore |
hooks.ts | Proxy를 통한 내부 use.* 필드 훅 |
provider.tsx | StoreProvider 컴포넌트 + React 컨텍스트 |
with-provider.tsx | withProvider(def, Component) HOC |
singleton.ts | 폴백을 위한 싱글턴 인스턴스 관리 |
Store 생명주기
섹션 제목: “Store 생명주기”1. 정의 (빌드 타임)
섹션 제목: “1. 정의 (빌드 타임)”Store()는 빌더를 반환해요. 각 체이닝된 메서드 (.on(), .intents(), .computed(), .middleware(), .nested(), .deps())는 내부 BuilderConfig에 설정을 추가해요. 빌더는 불변이에요 — 각 호출은 새 빌더 인스턴스를 반환해요.
const def = Store({ state: { count: 0 } }) .on(CounterEvent, { incremented: (s, p) => ({ ...s, count: s.count + p.amount }) }) .intents(CounterIntents) .computed({ doubled: (s) => s.count * 2 })마지막 .intents() 또는 .computed() 호출은 StoreDefinition을 생성해요 — 인스턴스를 생성할 수 있는 동결된 설정 객체예요.
2. 인스턴스화 (런타임)
섹션 제목: “2. 인스턴스화 (런타임)”def.create(options?)는 createStoreInstance()를 호출하며, 이 함수는:
- 초기 상태를 머지 —
options.initialState가 정의의 기본 상태와 딥 머지돼요. - Nested Store 초기화 — 상태의 각
Nested,Nested.array,Nested.map마커에 대해 자식StoreDefinition을 사용해서 자식StoreInstance를 생성해요. - Nested 기본값 해석 — Nested single 슬롯은 자식 Store의 초기 상태로 채워져요 (
null이 아닌). - Nested 동기화 설정 — 자식 상태 변경이 부모의 결합된 상태를 업데이트하도록 자식 Store를 구독해요.
- Executor 인덱스 구축 — 디스패치 시 빠른 조회를 위해 각
Command를Executor에 매핑해요. - Middleware 초기화 — Store 인스턴스로 각
MiddlewareFactory.create()를 호출해요. - 초기 Computed 값 계산 — 초기 상태에 대해 모든 Computed 함수를 실행해요.
3. 디스패치 (런타임)
섹션 제목: “3. 디스패치 (런타임)”store.send(intent) 호출 시:
send(intent) ├─ middleware.onIntentStart(intent) ├─ resolve intent → Command[] ├─ for each Command (sequential by default): │ ├─ find Executor for Command │ ├─ executor(command, { deps, emit, getState, signal, scope }) │ │ ├─ emit(event) ← synchronous │ │ │ ├─ middleware.onEvent(event) │ │ │ ├─ store.on[event.type](state, payload) → newState │ │ │ ├─ middleware.onStateChange(prev, next) │ │ │ ├─ processRelay(event, state) │ │ │ │ └─ forwardEventToNestedChildren(relayedEvents) │ │ │ ├─ recompute affected computed values │ │ │ └─ notify subscribers │ │ └─ (next emit...) │ └─ if executor throws → middleware.onError(error) └─ middleware.onIntentEnd(intent)핵심 불변 조건: emit()은 동기적이에요. 상태가 업데이트되고, Computed 값이 재계산되고, 구독자에게 알림이 전달된 후에 emit()이 반환돼요.
4. 해제
섹션 제목: “4. 해제”store.dispose():
- 모든 Nested 자식 Store를 재귀적으로 해제해요.
- 모든 구독을 제거해요.
- 인스턴스를 해제됨으로 표시해요.
- 이후
send()호출은 예외를 던져요. 이후emit()호출은 조용히 무시돼요.
Nested Store 내부
섹션 제목: “Nested Store 내부”각 Nested 타입에는 대응하는 내부 관리자가 있어요:
| Nested 타입 | 관리자 | 스토리지 |
|---|---|---|
Nested(ChildStore) | NestedSingleManager | 단일 StoreInstance |
Nested.array(ChildStore) | NestedArrayManager | 항목 ID로 키가 지정된 Map<string, StoreInstance> |
Nested.map(ChildStore) | NestedMapManager | 맵 키로 키가 지정된 Map<string, StoreInstance> |
상태 동기화
섹션 제목: “상태 동기화”부모 상태는 모든 자식의 결합된 상태를 포함해요. 자식의 상태가 변경될 때:
- 자식이 구독을 통해 부모에게 알려요.
- 부모가
recomputeFromNestedChange()를 호출해요. - 부모의 결합된 상태가 재구축돼요.
- 부모의 Computed 값이 재계산돼요.
- 부모의 구독자에게 알려요.
Event 흐름
섹션 제목: “Event 흐름”Event는 두 방향으로 흘러요:
하향 (relay): 부모 relay 핸들러가 forwardEventToNestedChildren()으로 자식 Store에 전달될 Event를 생성할 수 있어요. relay에는 무한 순환을 방지하기 위한 깊이 제한 (MAX_RELAY_DEPTH = 5)이 있어요.
상향 (버블링): 자식 Store가 Event를 처리하면 부모에게 알려요. 부모가 해당 Event 타입에 대한 relay 핸들러가 있으면 반응할 수 있어요.
배열/맵 동기화
섹션 제목: “배열/맵 동기화”raw 상태가 변경되면 (예: 배열에 새 항목 출현) syncNestedArray() / syncNestedMap()이 현재 인스턴스와 새 상태를 비교해요:
- 새 키 — 새 자식 인스턴스 생성
- 제거된 키 — 제거된 자식 인스턴스 해제
- 기존 키 — 자식 인스턴스 유지 (상태는 자체 Event 핸들러가 관리)
Computed 내부
섹션 제목: “Computed 내부”Computed 값은 프록시 기반 의존성 추적을 사용해요:
- 첫 실행 시
Proxy가 상태 객체를 감싸요. - Computed 함수가 프록시에 대해 실행돼요.
- 프록시에 대한 모든 속성 접근이 의존성으로 기록돼요.
- 이후 상태 변경 시 의존성이 변경된 Computed 함수만 재계산돼요.
구조적 동등성: 재계산 후 새 값이 이전 값과 structuralEqual()로 비교돼요. 동일하면 불필요한 구독자 알림을 방지하기 위해 이전 참조가 유지돼요.
Middleware 파이프라인
섹션 제목: “Middleware 파이프라인”Middleware는 생명주기 훅을 받아요:
type Middleware = { onIntentStart?(intent): void onIntentEnd?(intent): void onEvent?(event): void onStateChange?(prev, next): void onError?(error): void}MiddlewareFactory는 name과 Middleware를 반환하는 create(store) 메서드를 가져요. 내장 팩토리:
| 팩토리 | 목적 |
|---|---|
logger() | Event, 상태 변경, 오류를 콘솔에 로깅 |
persist() | 상태 영속화 및 복원 (localStorage, 커스텀 스토리지) |
devtools() | 브라우저 DevTools 확장 프로그램에 연결 |
undoRedo() | 실행 취소/다시 실행을 위한 상태 이력 추적 |
React 바인딩 내부
섹션 제목: “React 바인딩 내부”useStore
섹션 제목: “useStore”useStore(def)는 React 컴포넌트에서 Store에 접근하기 위한 주요 훅이에요:
- React 컨텍스트로 주어진 정의에 대한
StoreProvider가 컴포넌트 트리에 있는지 확인해요. - 발견되면 스코프된 인스턴스를 감싸는 핸들을 반환해요.
- 발견되지 않으면 글로벌 싱글턴으로 폴백해요 (첫 접근 시 지연 생성).
반환된 UseStoreReturn 객체는 다음을 제공해요:
use.*— 필드별 훅을 생성하는Proxy.store.use.count()는useSyncExternalStore로state.count를 구독하는 훅을 반환해요.send— Store 인스턴스의 send 프록시로, Intent 이름 단축키가 유지돼요.useSelector(fn)—useSyncExternalStore를 사용하는 커스텀 Selector 훅.getState,subscribe,cancel,cancelAll,dispose,scope— 기반 Store 인스턴스 메서드에 대한 직접 접근.
useStore(instance)는 주어진 StoreInstance를 직접 감싸는 오버로드로, 컨텍스트 해석을 우회해요.
StoreProvider
섹션 제목: “StoreProvider”StoreProvider는 of prop으로 Store 정의를 받고, 선택적으로 기존 store 인스턴스를 받아요. 인스턴스가 제공되지 않으면 선택적 initialState와 deps props를 사용해서 자동으로 생성해요. React 컨텍스트로 인스턴스를 자식에게 제공해요.
싱글턴 vs. 스코프
섹션 제목: “싱글턴 vs. 스코프”- 싱글턴 (
StoreProvider가 없을 때의 폴백): 첫 접근 시 생성되는 모듈 수준 인스턴스. 클라이언트 전용. - 스코프 (
<StoreProvider>+useStore()): Provider당 인스턴스. SSR, 테스팅, 다중 인스턴스 시나리오에 필수.
확장 포인트
섹션 제목: “확장 포인트”| 확장 | 메커니즘 |
|---|---|
| 커스텀 사이드 이펙트 | CommandExecutor 작성 |
| 횡단 관심사 | MiddlewareFactory 작성 |
| 프레임워크 바인딩 | store.subscribe()로 StoreInstance 구독 |
| 커스텀 파생 상태 | .computed()에 Computed 함수 추가 |
| Store 간 조율 | Executor에서 relay와 scope 사용 |
Hurum은 의도적으로 플러그인 시스템이나 훅 레지스트리가 없어요. 모든 확장은 아키텍처의 일급 개념이에요.