React 어댑터
Stable
@2davi/rest-domain-state-manager 는 코어 라이브러리의 프레임워크 비의존성을 유지하면서 React 연동을 위한 별도 어댑터를 제공합니다. React 없이 Vanilla JS나 Vue에서도 subscribe() / getSnapshot() 을 직접 사용할 수 있습니다.
설치 전 조건
- React 18 이상 설치 필요 (
useSyncExternalStore의존) @2davi/rest-domain-state-manager설치 완료
import 경로
코어 번들과 분리된 subpath export 입니다. 반드시 /adapters/react 경로로 import하세요.
import { useDomainState } from '@2davi/rest-domain-state-manager/adapters/react'Tree-shaking
이 어댑터는 코어 번들에 포함되지 않습니다. React를 사용하지 않는 프로젝트는 이 파일이 번들에 포함되지 않습니다.
useDomainState
DomainState 의 불변 스냅샷을 React 컴포넌트에 연결하는 커스텀 훅입니다. data Proxy 를 통한 변이가 감지되면 컴포넌트를 자동으로 리렌더링합니다.
const snapshot = useDomainState(domainState)기본 사용
import { useDomainState } from '@2davi/rest-domain-state-manager/adapters/react'
function UserProfile({ userState }) {
const data = useDomainState(userState)
return (
<div>
<p>{data.name}</p>
<p>{data.email}</p>
</div>
)
}이벤트 핸들러에서 변이 후 자동 리렌더링
userState.data 에 값을 할당하면 microtask 배칭 완료 후 컴포넌트가 자동으로 리렌더링됩니다.
function UserForm({ userState }) {
const data = useDomainState(userState)
const handleChange = (e) => {
// Proxy set 트랩 발화 → microtask 배칭 → 리렌더링 자동 트리거
userState.data[e.target.name] = e.target.value
}
const handleSave = async () => {
try {
await userState.save('/api/users/1')
} catch (err) {
// 실패 시 userState.data는 save() 이전 상태로 자동 복원됨
console.error('저장 실패:', err.status)
}
}
return (
<>
<input name="name" value={data.name} onChange={handleChange} />
<input name="email" value={data.email} onChange={handleChange} />
<button onClick={handleSave}>저장</button>
</>
)
}동작 원리
useDomainState 는 내부적으로 React 18의 useSyncExternalStore 를 사용합니다.
1. userState.data.name = 'Davi'
→ Proxy set 트랩 발화
→ changeLog 기록 + dirtyFields 갱신
2. microtask 배칭 완료
→ _buildSnapshot() 실행
→ 변경된 경로만 얕은 복사(Structural Sharing)
→ 새로운 #shadowCache 참조 생성
3. _notifyListeners() 호출
→ React가 getSnapshot() 재호출
4. Object.is(prevSnapshot, nextSnapshot)
→ 다른 참조 → 리렌더링 트리거변경된 필드의 상위 경로만 새 참조를 만들고, 변경되지 않은 필드는 기존 메모리 참조를 재사용합니다(Structural Sharing). 불필요한 객체 복사 비용을 최소화합니다.
subscribe() / getSnapshot() 직접 사용
useDomainState 를 사용하지 않고 두 메서드를 직접 연결할 수도 있습니다.
import { useSyncExternalStore } from 'react'
function UserCard({ userState }) {
const data = useSyncExternalStore(
(cb) => userState.subscribe(cb), // 구독 등록, 해제 함수 반환
() => userState.getSnapshot() // 현재 불변 스냅샷 반환
)
return <div>{data.name}</div>
}subscribe(listener)
상태 변경 시 호출될 리스너를 등록합니다. 반환값은 구독 해제 함수입니다. useSyncExternalStore 의 첫 번째 인자 규약을 만족합니다.
const unsubscribe = userState.subscribe(() => {
console.log('상태 변경됨')
})
// 구독 해제
unsubscribe()getSnapshot()
현재 상태의 불변 스냅샷을 반환합니다. 변경이 없으면 이전과 동일한 참조를 반환합니다. 매번 새 객체를 반환하면 useSyncExternalStore 가 무한 리렌더링 루프에 빠지기 때문입니다.
const snap1 = userState.getSnapshot()
userState.data.name = 'Davi'
// microtask 완료 후
await Promise.resolve()
const snap2 = userState.getSnapshot()
console.log(snap1 === snap2) // false — 새 참조
console.log(snap1.email === snap2.email) // true — 미변경 필드는 동일 참조Vue / Vanilla JS 환경
React 없이도 subscribe() / getSnapshot() 을 직접 활용할 수 있습니다.
// Vue 3 — watchEffect 연동
import { ref, watchEffect } from 'vue'
const snapshot = ref(userState.getSnapshot())
const unsubscribe = userState.subscribe(() => {
snapshot.value = userState.getSnapshot()
})
// 컴포넌트 언마운트 시
onUnmounted(() => unsubscribe())// Vanilla JS — DOM 업데이트
const unsubscribe = userState.subscribe(() => {
const data = userState.getSnapshot()
document.getElementById('name').textContent = data.name
})주의사항
Proxy 직접 변이
useDomainState 가 반환하는 data 는 불변 스냅샷입니다. 이 값을 직접 변경하지 마세요. 상태 변경은 반드시 userState.data.field = value 형태로 원본 Proxy 를 통해야 합니다.
// ❌ 잘못된 방법 — 스냅샷 직접 변경 (개발 환경에서 freeze 에러 발생)
data.name = 'Davi'
// ✅ 올바른 방법 — Proxy를 통한 변경
userState.data.name = 'Davi'