콘텐츠로 이동

범위 지정 인스턴스

범위 지정 인스턴스는 Store.create()로 생성된 store예요. 자체적인 상태, 구독, 실행 중인 executor, 중첩된 자식 store를 가지고 있어요. 전역 싱글톤을 포함해서 동일한 store의 다른 모든 인스턴스와 완전히 독립적이에요.

싱글톤만으로 충분하지 않을 때 범위 지정 인스턴스가 필요해요:

  • 여러 개의 독립적인 복사본이 필요한 경우 (차트, 폼, 위젯)
  • 서버 데이터가 각 인스턴스를 다르게 초기화하는 경우
  • 인스턴스마다 커스텀 의존성이 필요한 경우 (실제 API vs. mock, 요청별 인증 토큰)
  • SSR에서 각 요청이 자체 상태를 가져야 하는 경우
  • 테스팅에서 격리되고 재현 가능한 설정이 필요한 경우
const store = PurchaseStore.create({
initialState: { purchase: serverData, saving: false },
deps: { repository: new PurchaseRepository() },
})
OptionTypeDefaultDescription
initialStatePartial<RawState>Store 기본값store의 기본 상태와 깊은 병합
depsPartial<Deps>Store 기본값store의 기본 deps와 얕은 병합

둘 다 선택 사항이에요. Store.create()를 인자 없이 호출하면 store의 기본 상태와 deps를 사용해요.

재정의하려는 필드만 제공하면 돼요. 중첩된 객체는 재귀적으로 병합돼요:

// 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'은 재정의됨

각 최상위 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>
)
}

서버에는 전역 싱글톤이 없어요. 각 요청은 서로 다른 데이터를 가진 다른 사용자에게 제공돼요. 요청마다 새 인스턴스를 생성해야 해요:

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()는 결정론적 정리를 수행해요:

  1. 실행 중인 모든 executor를 취소 (abort signal 트리거)
  2. 모든 리스너 구독 해제
  3. 중첩된 자식 store를 재귀적으로 폐기
  4. 인스턴스를 폐기됨으로 표시

폐기 후:

  • 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. 범위 지정 — 빠른 참조”
SingletonScoped (Store.create)
생성첫 접근 시 지연 생성Store.create()로 명시적으로
생명주기앱 생명주기사용자가 제어
상태전역으로 공유인스턴스마다 독립적
초기 상태Store 기본값만initialState로 커스텀
의존성Store 기본값만deps로 커스텀
SSR안전하지 않음필수
여러 인스턴스불가가능
Provider 필요불필요필요
  • Provider & withProvider — 컴포넌트 트리에서 범위 지정 인스턴스를 사용하는 방법
  • Testing Overview — 내부적으로 Store.create()를 사용하는 테스트 유틸리티