pipe
pipe는 @ilokesto/state/utils의 builder-only composition helper입니다.
pipe.use(middleware): PipeBuilder
PipeBuilder.use(middleware): PipeBuilder
PipeBuilder.create<T>(initialState: T): Store<T>Root pipe object는 .use()만 제공합니다. 각 .use()는 다른 middleware를 등록하거나 .create(initialState)를 호출할 수 있는 builder를 반환합니다. .create()는 새 Store<T>를 만들고 등록된 middleware를 선언 순서대로 적용한 뒤 framework adapter에 넘길 store를 반환합니다.
기본 사용법
import { create } from '@ilokesto/state/react';
import { jsonStorage, logger, persist } from '@ilokesto/state/middleware';
import { pipe } from '@ilokesto/state/utils';
type CounterState = { count: number };
const decodeCounter = (value: unknown): CounterState | null => {
if (typeof value !== 'object' || value === null) return null;
if (!('count' in value) || typeof value.count !== 'number') return null;
return { count: value.count };
};
const counterStore = pipe
.use(persist({ key: 'counter', storage: jsonStorage(() => window.localStorage), decode: decodeCounter }))
.use(logger({ collapsed: true, diff: true }))
.create<CounterState>({ count: 0 });
export const useCounter = create(counterStore);
export async function restoreCounter() {
await counterStore.persist.rehydrate();
}Middleware setup은 첫 번째 .use()부터 마지막 .use()까지 실행됩니다. Update에서는 처음 등록한 middleware가 가장 바깥쪽입니다. 순서가 중요하며 pipe는 선언 순서를 바꾸지 않고 검증합니다. 생성 시 영속화 I/O는 없습니다. 클라이언트 시작 코드나 effect에서 restoreCounter()를 호출하고 오류를 처리한 뒤 편집하세요.
Public pipe type
@ilokesto/state/utils는 root builder용 Pipe와 구성된 chain용 PipeBuilder를 export합니다. PipeMiddleware는 state-specific middleware를, PipeAnyMiddleware는 모든 state type에 동작하는 middleware를 나타냅니다. PipeCapability는 middleware가 추가한 store API를, PipeMiddlewareMetadata는 ID와 관계를, PipeDuplicatePolicy는 duplicate ID 정책을 설명합니다.
PipeMiddlewareConflictDiagnostic은 선언한 metadata가 충돌할 때 노출되는 compile-time diagnostic입니다. Runtime의 잘못된 configuration은 PipeConfigurationError를 던지며, code의 type은 PipeConfigurationErrorCode입니다.
Builder syntax를 쓰는 이유
pipe는 호출할 수 없고 variadic middleware list도 받지 않습니다. .use()마다 middleware 하나를 등록한 뒤 plain initial state로 store를 만드세요.
const store = pipe
.use(validate(counterSchema))
.use(logger({ collapsed: true }))
.create({ count: 0 });위에서 아래로 읽으면 됩니다: validation 등록, logging 등록, state 생성.
Custom middleware
.use()에 전달하는 모든 값은 definePipeableMiddleware()로 등록되어야 합니다. 각 middleware에 id를 주고 metadata로 adds, requires, before, after, conflicts, optional duplicate policy를 선언하세요. 앞선 바깥 middleware가 추가한 capability는 이후 안쪽 middleware에서 사용할 수 있습니다. Chain에 없는 middleware와의 관계는 무시합니다. 존재하는 before 또는 after 관계에서는 pipe가 선언한 방향을 검증하고, 위반된(반대) 순서만 거부합니다. 유효한 관계는 선언한 순서를 유지합니다. Conflict, duplicate ID, cycle도 재정렬 없이 거부됩니다.
Configuration errors
PipeConfigurationError는 문맥에 따른 기본 id, 관련 ids, 그리고 DUPLICATE_CAPABILITY, DUPLICATE_MIDDLEWARE, INVALID_METADATA, INVALID_MIDDLEWARE_RESULT, INVALID_STORE_INPUT, MISSING_CAPABILITY, MIDDLEWARE_CONFLICT, MIDDLEWARE_CYCLE, MIDDLEWARE_ORDER 중 하나인 code를 노출합니다. 기본 ID는 middleware나 capability를 가리킬 수 있고 적용할 identifier가 없으면 비어 있습니다. ids에는 관련된 문맥 identifier가 들어가며 비어 있을 수도 있습니다.
history()는 동기 commit이 필요하므로 어느 순서든 debounce() 또는 throttle()과 같은 chain에 둘 수 없고 이 조합은 MIDDLEWARE_CONFLICT를 사용합니다. debounce()는 persist()보다 먼저 선언하세요. 반대 순서는 계속 MIDDLEWARE_ORDER로 거부됩니다. 영속화는 실제 commit을 관찰합니다. 복원은 시간 제어 미들웨어를 우회하고 새 history 기준점이 되며 flush()는 debounce 타이머를 강제로 실행하지 않습니다.
Validation과 함께 쓰기
Validation은 side effect를 수행하는 middleware보다 앞에 두는 경우가 많습니다. 그래야 invalid state가 persist되거나 tooling으로 전달되지 않습니다.
import { devtools, validate } from '@ilokesto/state/middleware';
import { pipe } from '@ilokesto/state/utils';
const settingsStore = pipe
.use(validate(settingsSchema))
.use(devtools('settings'))
.create({ theme: 'system' as 'system' | 'light' | 'dark' });Validation이 update를 거부하면 뒤쪽 middleware는 invalid state를 보지 않아야 합니다.
Reducer와 함께 쓰기
pipe는 store를 준비합니다. Reducer는 여전히 adapter의 create(reduce, store) overload에 속합니다.
import { create } from '@ilokesto/state/react';
import { logger } from '@ilokesto/state/middleware';
import { pipe } from '@ilokesto/state/utils';
const todoStore = pipe
.use(logger({ diff: true }))
.create({ items: [] as string[] });
export const useTodos = create(reduceTodos, todoStore);Reducer action은 next state로 변환되고, 그 결과 update가 store middleware pipeline을 통과합니다.
기존 Store instance
pipe는 항상 plain initial state에서 새 Store<T>를 만듭니다. .create()는 기존 Store를 거부합니다. 다른 module이 정확한 store instance를 이미 소유한다면 그 store를 framework adapter에 직접 넘기고, 생성에는 pipe를 사용하지 마세요.
import { Store } from '@ilokesto/store';
import { create } from '@ilokesto/state/react';
const existingStore = new Store({ count: 0 });
const useCounter = create(existingStore);다른 module이 이미 store를 subscribe하고 있거나 utility layer 밖 infrastructure가 store를 만든 경우 이 방식을 사용하세요.
Piped store 테스트하기
pipe는 plain Store<T>를 반환하므로 framework adapter 없이 먼저 test할 수 있습니다.
const store = pipe
.use(validate(counterSchema))
.create({ count: 0 });
store.setState({ count: 1 });
expect(store.getState()).toEqual({ count: 1 });Persistence나 browser-only middleware는 필요한 storage API를 제공하는 test 환경에서 실행하거나 해당 API를 mock하세요.
자주 하는 실수
pipe(initialState, ...middleware)호출하기.pipe는 object입니다.pipe.use(...)로 시작하고.create(initialState)로 끝내세요.- 기존 store를
.create()에 넘기기. Builder는 plain initial state만 받고 새Store<T>를 만듭니다. - 순서 무시하기. Middleware는 선언 순서대로 등록되며 side-effect middleware는 보통 validation 뒤에 둡니다.
- framework adapter call을
pipe안에 넣기.pipe는 store middleware를 조합하지 React/Vue/Svelte/Solid/Angular adapter call을 조합하지 않습니다. - store 생성 후 update에
pipe를 사용하기. Store가 이미 있으면setState,dispatch, 또는adaptor를 사용하세요.