시스템 개요
이 문서는 rest-domain-state-manager 의 전체 아키텍처 레이어 구조와 모듈 간 의존 방향을 기술합니다.
레이어 구조
라이브러리는 다섯 개의 계층으로 구성됩니다. 의존성은 단방향이며 상위 계층이 하위 계층을 호출합니다. 역방향 의존은 허용되지 않습니다.
┌─────────────────────────────────────────────────────────────────────┐
│ 진입점 (Entry Point) │
│ src/index.js │
│ Composition Root — 의존성 조립 │
│ { DomainState, ApiHandler, DomainVO, DomainPipeline, │
│ DomainRenderer, FormBinder, closeDebugChannel } │
└───────────────────────────┬─────────────────────────────────────────┘
│
┌───────────────────┼─────────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌──────────────┐ ┌────────────────────────────┐
│ src/domain/ │ │ src/network/ │ │ src/plugins/ │
│ │ │ │ │ │
│ DomainState │ │ ApiHandler │ │ domain-renderer/ │
│ DomainVO │ │ │ │ DomainRenderer.js │
│ DomainPipeline │ │ │ │ form-binder/ │
│ │ │ │ │ FormBinder.js │
└───────┬─────────┘ └──────┬───────┘ └────────────────────────────┘
│ │
│ │ ┌─────────────────────────────┐
│ │ │ src/adapters/ │
│ │ │ │
│ │ │ react.js (React 어댑터) │
│ │ │ (subpath export) │
│ │ └─────────────────────────────┘
│ │
│ │ ┌─────────────────────────────┐
│ │ │ src/workers/ │
│ │ │ │
│ │ │ serializer.worker.js │
│ │ │ (BroadcastChannel 오프로드)│
│ │ └─────────────────────────────┘
│ │
└────────┬──────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ src/ (내부 계층) │
│ │
│ core/ debug/ common/ │
│ api-proxy.js debug-channel.js clone.js (safeClone) │
│ api-mapper.js freeze.js (deepFreeze) │
│ url-resolver.js constants/ logger.js (devWarn) │
│ dirty.const.js │
│ error.messages.js │
│ log.messages.js │
│ op.const.js │
│ channel.const.js │
│ protocol.const.js │
└─────────────────────────────────────────────────────────────────────┘핵심 모듈 역할
| 모듈 | 레이어 | 역할 |
|---|---|---|
DomainState | domain | 상태 추적의 중심. ProxyWrapper 조합, save() 분기 로직, Shadow State, 디버그 채널 연동. |
api-proxy.js | core | JS Proxy 엔진. set/get/deleteProperty 트랩, changeLog, dirtyFields, 복원 메서드 클로저. |
api-mapper.js | core | changeLog → HTTP payload 변환. toPayload()(PUT/POST), toPatch()(PATCH RFC 6902). |
ApiHandler | network | HTTP 전송 레이어. URL 결정, CSRF 토큰 주입, fetch() 래핑, HttpError 생성. |
DomainVO | domain | 스키마 선언, Skeleton 생성, validate/transform 제공. |
DomainPipeline | domain | 병렬 fetch 조율, after() 체이닝, strict 모드 오류 처리, 보상 트랜잭션(failurePolicy). |
debug-channel.js | debug | BroadcastChannel 싱글톤, 탭 등록/해제, Lazy Initialization. serializer.worker로 직렬화 오프로드. |
clone.js | common | safeClone() — structuredClone 우선, 구형 환경 폴백. 스냅샷 깊은 복사 전담. |
freeze.js | common | deepFreeze() / maybeDeepFreeze() — Shadow State 불변 스냅샷 동결. 프로덕션 no-op. |
logger.js | common | devWarn() / logError() — silent 플래그 통합 로그 제어. |
react.js | adapters | useDomainState() — useSyncExternalStore 기반 React 연동 훅. subpath export. |
serializer.worker.js | workers | _stateRegistry 직렬화 + BroadcastChannel 발화를 메인 스레드에서 오프로드. |
순환 참조 해소 — Composition Root 패턴
DomainState 와 DomainPipeline 은 서로를 참조합니다. DomainState.all() 이 DomainPipeline 인스턴스를 생성하고, DomainPipeline 은 DomainState 인스턴스를 결과로 보유합니다. 정적 import 로 이를 연결하면 ES Module 순환 참조가 발생합니다.
이 문제는 Composition Root 패턴으로 해소됩니다. src/index.js 진입점이 두 모듈을 각각 import한 뒤 DomainState.configure({ pipelineFactory }) 를 호출하여 의존성을 조립합니다. 소비자 코드에서 이 조립 과정을 직접 수행할 필요가 없습니다.
의존 방향 (단방향)
DomainPipeline ──→ DomainState
↑ ↑
└──── index.js ─────┘
(Composition Root)
configure({ pipelineFactory: (...args) => new DomainPipeline(...args) })각 모듈 파일은 서로를 알지 못합니다. DomainState 는 _pipelineFactory 라는 모듈 레벨 클로저 변수를 통해 DomainPipeline 의 존재 없이 파이프라인 인스턴스를 생성합니다.
Vitest 환경에서의 활용
DomainState.configure({ pipelineFactory: vi.fn() }) 으로 DomainPipeline 없이 DomainState 단독 테스트가 가능합니다. 두 모듈이 완전히 독립적으로 테스트될 수 있습니다.
Plugin Architecture
코어 엔진(src/domain/, src/core/)은 브라우저 DOM에 의존하지 않습니다. Node.js, 테스트 환경, Server-Side Rendering 어디서든 완전히 동작합니다.
DOM에 의존하는 기능(FormBinder, DomainRenderer)은 DomainState.use(plugin) 을 통해 런타임에 주입됩니다. 플러그인은 DomainState.prototype 에 메서드를 추가하는 방식으로 동작하며, 한 번 설치된 플러그인은 중복 설치되지 않습니다(Set 기반 설치 이력 추적).
Silent 모드 — 전역 로그 제어
DomainState.configure({ silent: true }) 를 호출하면 라이브러리 내부의 모든 console 출력이 억제됩니다. 운영 환경의 콘솔 노이즈를 막거나 통합 테스트에서 불필요한 로그를 제거할 때 사용합니다.
// 운영 환경
if (process.env.NODE_ENV === 'production') {
DomainState.configure({ silent: true })
}