콘텐츠로 이동

Middleware는 Store를 변경하지 않고 관찰하는 읽기 전용 옵저버예요. 로깅, 분석, 개발자 도구 통합, 영속화에 적합한 장소예요.

import { type Middleware } from '@hurum/core'
const analytics = (): Middleware => ({
name: 'analytics',
onEvent: (event, state) => {
trackEvent(event.type, event)
},
onError: (error, context) => {
reportError(error, { intent: context.intent })
},
})
// Register in the store builder chain
const MyStore = Store({ state: { count: 0 } })
.on({ /* ... */ })
.intents(MyIntents)
.executors(MyExecutor)
.middleware(logger(), devtools(), analytics())

Middleware는 name과 하나 이상의 선택적 훅 메서드를 가진 객체예요. Store 빌더 체인의 끝에서 .middleware()로 등록해요.

Middleware 훅은 액션이 이미 발생한 후에 호출돼요. 결과를 관찰하는 거지 — 무언가를 방지하거나, 수정하거나, 가로챌 수 없어요.

interface Middleware {
name: string
onEvent?(event: StoreEvent, state: State): void
onStateChange?(prevState: State, nextState: State): void
onIntentStart?(intent: string, payload: unknown): void
onIntentEnd?(intent: string, payload: unknown): void
onError?(error: Error, context: ErrorContext): void
}
호출 시점인자
onEventEvent가 emit되고 상태가 업데이트된 후Event 객체, 새 상태
onStateChange상태가 변경된 후 (Computed 재계산 포함)이전 상태, 다음 상태
onIntentStartIntent 실행이 시작될 때Intent 이름, 페이로드
onIntentEndIntent가 완료될 때 (성공 또는 실패)Intent 이름, 페이로드
onErrorExecutor에서 에러가 발생할 때에러, Intent 정보가 담긴 컨텍스트

Middleware 훅은 intent 라이프사이클의 특정 시점에 호출돼요:

store.send(intent)
→ onIntentStart
→ executor 실행
→ emit(event)
→ Store.on 핸들러 실행 (상태 업데이트)
→ onEvent
→ onStateChange
→ (executor 완료)
→ onIntentEnd

executor가 에러를 던지면 onIntentEnd 대신 onError가 호출돼요.

여러 Middleware를 등록할 수 있어요. 등록된 순서대로 호출돼요:

.middleware(
logger(), // called first
devtools(), // called second
analytics(), // called third
)
const logger = (): Middleware => ({
name: 'logger',
onEvent: (event, state) => {
console.log(`[Event] ${event.type}`, event)
},
onStateChange: (prev, next) => {
console.log('[State]', { prev, next })
},
onIntentStart: (intent, payload) => {
console.log(`[Intent Start] ${intent}`, payload)
},
onIntentEnd: (intent, payload) => {
console.log(`[Intent End] ${intent}`, payload)
},
onError: (error, context) => {
console.error(`[Error] in ${context.intent}:`, error)
},
})
const analyticsMiddleware = (tracker: AnalyticsTracker): Middleware => ({
name: 'analytics',
onEvent: (event) => {
// Track specific events
if (event.type.startsWith('Checkout/')) {
tracker.track(event.type, event)
}
},
onError: (error, context) => {
tracker.trackError(error, {
intent: context.intent,
timestamp: Date.now(),
})
},
})
const persist = (storageKey: string): Middleware => ({
name: 'persist',
onStateChange: (_prev, next) => {
localStorage.setItem(storageKey, JSON.stringify(next))
},
})

Middleware는 단순한 객체이므로, 조건부로 포함할 수 있어요:

const MyStore = Store({ state: { /* ... */ } })
.on({ /* ... */ })
.middleware(
...[
process.env.NODE_ENV === 'development' && logger(),
process.env.NODE_ENV === 'development' && devtools(),
analytics(),
].filter(Boolean) as Middleware[],
)

Hurum은 여러 내장 Middleware를 제공해요:

Middleware목적
logger()Event, 상태 변경, Intent, 에러를 콘솔에 로깅
devtools()타임 트래블 디버깅을 위한 브라우저 개발자 도구와 통합
persist()localStorage 또는 커스텀 스토리지 백엔드에 상태 영속화
undoRedo()실행 취소/다시 실행 지원을 위한 상태 히스토리 추적
  • Middleware는 상태를 수정할 수 없어요. Event에 반응해서 새로운 동작을 트리거해야 한다면, relay를 사용하거나 Executor에서 추가 Event를 emit하세요. Middleware는 순수하게 관찰 전용이에요.
  • Middleware는 가볍게 유지하세요. Middleware 훅은 상태 업데이트 경로에서 동기적으로 실행돼요. 무거운 계산(예: 영속화를 위한 대규모 상태 직렬화)은 requestIdleCallback 등으로 지연시켜야 해요.
  • name은 디버깅에 활용하세요. name 프로퍼티는 에러 메시지와 개발자 도구에서 어떤 Middleware인지 식별하는 데 도움이 돼요.
  • 비즈니스 로직을 Middleware에 넣지 마세요. 발생한 일을 로깅하는 건 괜찮아요. 다음에 무엇이 일어나야 하는지를 결정하는 건 Executor와 relay 핸들러의 역할이에요.