범위 지정 인스턴스
범위 지정 인스턴스란
섹션 제목: “범위 지정 인스턴스란”범위 지정 인스턴스는 Store.create()로 생성된 store예요. 자체적인 상태, 구독, 실행 중인 executor, 중첩된 자식 store를 가지고 있어요. 전역 싱글톤을 포함해서 동일한 store의 다른 모든 인스턴스와 완전히 독립적이에요.
싱글톤만으로 충분하지 않을 때 범위 지정 인스턴스가 필요해요:
- 여러 개의 독립적인 복사본이 필요한 경우 (차트, 폼, 위젯)
- 서버 데이터가 각 인스턴스를 다르게 초기화하는 경우
- 인스턴스마다 커스텀 의존성이 필요한 경우 (실제 API vs. mock, 요청별 인증 토큰)
- SSR에서 각 요청이 자체 상태를 가져야 하는 경우
- 테스팅에서 격리되고 재현 가능한 설정이 필요한 경우
Store.create(options?)
섹션 제목: “Store.create(options?)”const store = PurchaseStore.create({ initialState: { purchase: serverData, saving: false }, deps: { repository: new PurchaseRepository() },})Options
섹션 제목: “Options”| Option | Type | Default | Description |
|---|---|---|---|
initialState | Partial<RawState> | Store 기본값 | store의 기본 상태와 깊은 병합 |
deps | Partial<Deps> | Store 기본값 | store의 기본 deps와 얕은 병합 |
둘 다 선택 사항이에요. Store.create()를 인자 없이 호출하면 store의 기본 상태와 deps를 사용해요.
initialState의 깊은 병합
섹션 제목: “initialState의 깊은 병합”재정의하려는 필드만 제공하면 돼요. 중첩된 객체는 재귀적으로 병합돼요:
// Store 기본값: { user: null, settings: { theme: 'light', language: 'en' } }const store = SettingsStore.create({ initialState: { settings: { theme: 'dark' } },})
store.getState()// { user: null, settings: { theme: 'dark', language: 'en' } }// 'language'는 기본값에서 유지되고, 'theme'은 재정의됨deps의 얕은 병합
섹션 제목: “deps의 얕은 병합”각 최상위 dep 키는 기본값을 완전히 대체해요. 다른 키는 유지돼요:
// Store의 deps: { repository: defaultRepo, analytics: defaultAnalytics }const store = PurchaseStore.create({ deps: { repository: mockRepo }, // analytics는 여전히 defaultAnalytics 사용})이렇게 하면 다른 의존성을 건드리지 않고 테스트를 위해 하나의 의존성을 쉽게 교체할 수 있어요.
사용 사례
섹션 제목: “사용 사례”서버 데이터를 사용한 페이지별 상태
섹션 제목: “서버 데이터를 사용한 페이지별 상태”제품 페이지는 서버로부터 데이터를 받아요. 다른 제품으로 이동할 때마다 해당 제품의 데이터로 새로운 store가 생성돼요. 이전 페이지의 잔여 상태가 남아있지 않아요.
import { StoreProvider } from '@hurum/react'import { ProductStore } from './store'
function ProductPage({ product }: { product: Product }) { const [store] = useState(() => ProductStore.create({ initialState: { product }, deps: { api: new ProductApi(product.storeId) }, }))
return ( <StoreProvider of={ProductStore} store={store}> <ProductContent /> </StoreProvider> )}독립적인 부모 인스턴스를 가진 중첩 store
섹션 제목: “독립적인 부모 인스턴스를 가진 중첩 store”store가 Nested.array 또는 Nested.map을 사용할 때, 각 부모 인스턴스는 자체 자식 store 집합을 관리해요. 두 개의 부모 인스턴스를 생성하면 두 개의 완전히 독립적인 계층 구조를 얻게 돼요:
import { StoreProvider } from '@hurum/react'import { PurchaseStore } from './store'
// 각 PurchaseForm은 자체 ItemStore 자식들을 가진 자체 PurchaseStore를 가짐function App() { return ( <> <PurchaseForm serverData={order123} /> <PurchaseForm serverData={newDraft} /> </> )}
function PurchaseForm({ serverData }: { serverData: Purchase }) { const [store] = useState(() => PurchaseStore.create({ initialState: { id: serverData.id, items: serverData.items }, })) // Hurum이 배열의 각 item에 대해 자식 ItemStore 인스턴스를 자동 생성
return ( <StoreProvider of={PurchaseStore} store={store}> <PurchaseContent /> </StoreProvider> )}SSR — 요청별 인스턴스
섹션 제목: “SSR — 요청별 인스턴스”서버에는 전역 싱글톤이 없어요. 각 요청은 서로 다른 데이터를 가진 다른 사용자에게 제공돼요. 요청마다 새 인스턴스를 생성해야 해요:
import { StoreProvider } from '@hurum/react'import { PurchaseStore } from './store'
function ServerPage({ data }: { data: PageData }) { const store = PurchaseStore.create({ initialState: { purchase: data.purchase }, deps: { repository: new ServerPurchaseRepository(data.token) }, })
return ( <StoreProvider of={PurchaseStore} store={store}> <PurchaseContent /> </StoreProvider> )}테스팅
섹션 제목: “테스팅”Store.create()는 초기 상태와 mock된 의존성에 대한 완전한 제어를 제공해서 테스트를 격리되고 재현 가능하게 만들어요. 테스트 전용 래퍼는 TestStore를 참조하세요.
store.dispose()는 결정론적 정리를 수행해요:
- 실행 중인 모든 executor를 취소 (abort signal 트리거)
- 모든 리스너 구독 해제
- 중첩된 자식 store를 재귀적으로 폐기
- 인스턴스를 폐기됨으로 표시
폐기 후:
store.send()는 에러를 던져요- 여전히 실행 중인 executor 내부의
emit()은 조용히 무시돼요
폐기가 중요한 경우
섹션 제목: “폐기가 중요한 경우”폐기는 많은 경우에 선택 사항이에요. store가 범위를 벗어나고 아무것도 참조하지 않으면 JavaScript의 가비지 컬렉터가 정리를 처리해요.
다음의 경우 명시적으로 폐기하세요:
- 장시간 실행되는 executor가 진행 중인 경우 (예: 폴링 루프, 대용량 파일 업로드). 폐기는 abort signal을 즉시 트리거해서 낭비되는 작업과 네트워크 요청을 중지해요.
- 결정론적 정리 타이밍이 필요한 경우. GC는 비결정론적이에요. executor가 WebSocket 연결이나 타이머를 보유하고 있다면, 명시적 폐기가 즉시 닫아요.
- 메모리에 민감한 환경. 폐기는 GC를 기다리는 대신 모든 내부 참조(구독, 중첩 store, 미들웨어 상태)를 즉시 해제해요.
import { StoreProvider } from '@hurum/react'import { PurchaseStore } from './store'
function PurchaseWidget({ data }: { data: Purchase }) { const [store] = useState(() => PurchaseStore.create({ initialState: { purchase: data }, }))
useEffect(() => { return () => store.dispose() }, [store])
return ( <StoreProvider of={PurchaseStore} store={store}> <PurchaseContent /> </StoreProvider> )}Provider가 자동 폐기하지 않는 이유
섹션 제목: “Provider가 자동 폐기하지 않는 이유”React Strict Mode는 개발 환경에서 컴포넌트를 두 번 마운트하고 언마운트해요. StoreProvider가 언마운트 시 자동 폐기한다면, 첫 번째 언마운트가 store를 파괴해요. 두 번째 마운트는 폐기된 store를 사용하게 되어 상태가 깨지고, 이벤트가 삼켜지고, 혼란스러운 오류가 발생해요.
폐기를 사용자에게 맡김으로써, Hurum은 이런 버그 클래스 전체를 피해요. 정리가 발생하는 시점을 사용자가 선택해요.
싱글톤 vs. 범위 지정 — 빠른 참조
섹션 제목: “싱글톤 vs. 범위 지정 — 빠른 참조”| Singleton | Scoped (Store.create) | |
|---|---|---|
| 생성 | 첫 접근 시 지연 생성 | Store.create()로 명시적으로 |
| 생명주기 | 앱 생명주기 | 사용자가 제어 |
| 상태 | 전역으로 공유 | 인스턴스마다 독립적 |
| 초기 상태 | Store 기본값만 | initialState로 커스텀 |
| 의존성 | Store 기본값만 | deps로 커스텀 |
| SSR | 안전하지 않음 | 필수 |
| 여러 인스턴스 | 불가 | 가능 |
| Provider 필요 | 불필요 | 필요 |
다음 단계
섹션 제목: “다음 단계”- Provider & withProvider — 컴포넌트 트리에서 범위 지정 인스턴스를 사용하는 방법
- Testing Overview — 내부적으로
Store.create()를 사용하는 테스트 유틸리티