subscribe
subscribe()는 리스너를 등록하고 cleanup 함수를 반환합니다.
subscribe(listener: () => void): () => void리스너는 상태가 바뀐 뒤 호출됩니다. 인자를 받지 않으므로 최신 값이 필요하면 리스너 안에서 getState()를 호출하세요.
기본 사용법
const unsubscribe = store.subscribe(() => {
console.log(store.getState());
});
store.setState((prev) => ({ ...prev, ready: true }));
unsubscribe();cleanup은 계약의 일부입니다
subscribe()를 호출할 때마다 내부 Set에 리스너가 추가됩니다. 반환된 구독 해제 함수를 호출하지 않으면 그 리스너는 계속 등록된 상태로 남습니다.
cleanup을 소유자의 lifecycle에 연결하세요.
function connectPanel(panel: { render(value: unknown): void; destroy(cb: () => void): void }) {
const unsubscribe = store.subscribe(() => {
panel.render(store.getState());
});
panel.destroy(unsubscribe);
}알림 시점
리스너는 상태가 저장된 뒤 동기적으로 실행됩니다. 구현은 저장된 값마다 당시의 구독을 기록합니다. 전달 중 추가한 리스너는 등록 전에 기록된 알림을 받지 않습니다. 구독 해제 함수를 호출하면 해당 등록은 즉시 비활성화되므로, 아직 전달하지 않은 기록에 포함되어 있어도 건너뜁니다.
같은 값이면 알림이 없습니다
setState()가 Object.is 기준 같은 값 또는 같은 참조로 계산되면 리스너는 실행되지 않습니다.
const unsubscribe = store.subscribe(() => {
console.log("changed");
});
store.setState((prev) => prev); // log 없음
unsubscribe();파생된 값에 구독하기
listener가 store 전체 대신 상태의 일부분(slice)에 반응하길 원한다면 subscribeSelector()를 사용하세요. 이 메서드는 subscribe의 오버로드가 아니라 별개의 메서드라서, subscribe(listener)는 그대로 (listener: () => void) => () => void 형태를 유지하며 서브클래스에서 override subscribe(...)를 그대로 쓸 수 있습니다.
subscribeSelector<Selection>(
selector: (state: Readonly<T>) => Selection,
listener: (nextSelection: Selection, previousSelection: Selection) => void,
equalityFn?: (previousSelection: Selection, nextSelection: Selection) => boolean
): () => voidlistener는 구독을 등록하는 시점에 즉시 호출되지 않습니다. store가 업데이트되고 선택된 값이 바뀐 뒤에만 실행됩니다.
type User = { id: string; name: string };
type UserState = { user: User; revision: number };
const userStore = new Store<UserState>({
user: { id: "1", name: "Ada" },
revision: 0,
});
const unsubscribe = userStore.subscribeSelector(
(state) => state.user,
(nextUser, previousUser) => {
console.log("user changed:", previousUser.name, "->", nextUser.name);
}
);
userStore.setState((prev) => ({
...prev,
user: { ...prev.user, name: "Grace" },
revision: prev.revision + 1,
}));
unsubscribe();listener는 (nextSelection, previousSelection)를 인자로 받습니다. nextSelection으로 새 slice를 읽고, previousSelection으로 직전 값과 비교하세요.
기본 equality는 Object.is입니다. 매번 새 참조가 반환되지만 의미상으로는 같다고 봐야 하는 경우에는 직접 equalityFn(previousSelection, nextSelection)을 넘기세요.
const unsubscribe = userStore.subscribeSelector(
(state) => state.user,
(nextUser) => {
console.log("user identity changed:", nextUser.id);
},
(previousUser, nextUser) => previousUser.id === nextUser.id
);equality 함수(기본 Object.is 또는 직접 넘긴 함수)가 이전/다음 selection을 같다고 판단하면 내부 상태 참조가 바뀌었더라도 그 업데이트에 대해서는 listener가 실행되지 않습니다. 이 동작이 selector 구독이 "실제로 관심 있는 slice에 영향이 없는 state 변경"으로 인해 재실행되는 것을 막아 줍니다.
selector는 subscribeSelector()가 처음 호출될 때 한 번 실행되어 이전 previousSelection을 시드합니다. 등록 시점에 throw가 발생하면 그 error는 subscribeSelector() 호출 밖으로 전파되고 listener는 store에 등록되지 않습니다. 등록 시점에 selection이 일시적으로 잘못될 수 있다면 try/catch로 감싸세요.
이후 top-level 상태 변경이 알림 단계에 도달할 때마다 selector가 다시 실행되어 nextSelection을 계산하고, 그 다음 equality 함수가 previousSelection과 nextSelection을 비교합니다. listener는 equality 함수가 변경을 보고했을 때만 실행되고, 그렇지 않으면 이 구독에 대한 알림 cycle은 여기서 끝납니다.
subscribeSelector() 등록도 subscribe() listener와 같은 내부 Set에 저장되므로, 알림 시점, 같은 값이면 알림이 없습니다, cleanup은 계약의 일부입니다에 정리된 규칙이 그대로 적용됩니다. 상태가 저장된 뒤 동기적으로 실행되고, setState()가 상태 레벨에서 같은 참조로 계산되면 실행되지 않으며, 반환된 unsubscribe 함수를 호출하면 selector listener가 제거됩니다.
Listener error
등록 오류와 알림 오류는 서로 다른 경계에서 처리됩니다.
- 등록 (
subscribeSelector()호출). selector만 실행됩니다(이전previousSelection을 시드하기 위해). throw가 발생하면 그 throw는subscribeSelector()호출 밖으로 그대로 전파됩니다. listener는 store에 등록되지 않으므로 이후setState()에서도 호출되지 않습니다. - 알림 (이후의
setState()호출). selector가 실행되고, 그 다음 equality 함수가 실행되며, equality 함수가 변경을 보고한 경우에만 listener가 실행됩니다. 셋 모두 알림 cycle 안에서 동기적으로 진행됩니다. 오류가 나도 다른 활성 리스너와 대기 중인 알림은 처리합니다. 가장 바깥 알림 처리가 끝나면 원래 오류를 담은AggregateError를 던집니다.
selector, equality 함수, listener는 작게 유지하거나 예상 가능한 error는 listener 안에서 잡으세요.
구독 소유권
동일한 콜백을 여러 번 등록해도 각 구독과 해제 함수가 독립적입니다. 해제는 아직 전달되지 않은 알림에도 즉시 적용됩니다. 중첩 업데이트는 즉시 저장되지만 알림은 FIFO로 처리합니다. selector는 각 저장 시점의 값을 받으며, getState()는 항상 가장 최신 값을 반환합니다.