핸들러 레지스트리 패턴
핸들러 레지스트리 패턴은 handler의 ID, priority, dispatch 이름을 중앙에서 관리하는 방식입니다. 이 패턴을 도입하면 이름 충돌을 줄이고, 실행 순서를 문서화하기 쉬워지며, 테스트와 디버깅도 훨씬 단순해집니다.
왜 필요한가
규모가 커질수록 다음 문제가 자주 생깁니다.
- 동일한 액션 이름을 서로 다른 의미로 사용
- handler 등록 위치가 흩어져 우선순위 파악이 어려움
- module별 handler를 동적으로 붙일 때 naming 규칙이 깨짐
레지스트리는 이런 문제를 “하나의 명명 규칙”과 “하나의 우선순위 규칙”으로 정리합니다.
기본 구조
ts
export interface HandlerConfig {
readonly id: string;
readonly priority: number;
readonly dispatchName: string;
readonly description?: string;
}
export function createHandlerConfig(
domain: string,
action: string,
priority: number
): HandlerConfig {
return {
id: `${domain}.${action}`,
priority,
dispatchName: action,
};
}이 패턴에서 중요한 것은 dispatchName과 id를 분리하는 점입니다.
dispatchName은 view/action 레이어가 쓰는 공개 이름id는 내부 등록과 추적용 식별자
우선순위 설계 방식
우선순위를 숫자로만 두면 금방 혼란스러워집니다. 실무에서는 범위를 나눠 쓰는 것이 좋습니다.
text
100-199 validation
200-299 processing
300-399 cleanup
400+ cross-domain or advanced workflows이렇게 해두면 코드만 봐도 “이 handler가 어떤 단계에 속하는지”를 알 수 있습니다.
도메인별 레지스트리 예시
ts
export const CHECKOUT_HANDLERS = {
VALIDATE: createHandlerConfig('checkout', 'validate', 100),
SUBMIT: createHandlerConfig('checkout', 'submit', 200),
RESET: createHandlerConfig('checkout', 'reset', 300),
} as const;이런 구성이 있으면:
actions/에서는dispatch(CHECKOUT_HANDLERS.SUBMIT.dispatchName, payload)를 사용handlers/에서는CHECKOUT_HANDLERS.SUBMIT.id와priority를 그대로 사용
즉 view와 handler가 같은 계약을 공유하게 됩니다.
모듈별 확장
여러 인스턴스를 동시에 띄우는 경우에는 moduleId를 접두사로 붙이는 방식이 유용합니다.
ts
function createModuleHandlerConfig(
moduleId: string,
domain: string,
action: string,
priority: number
) {
return {
id: `${moduleId}.${domain}.${action}`,
priority,
dispatchName: action,
};
}이렇게 하면 같은 checkout 기능이 여러 화면이나 탭에서 동시에 살아 있어도 충돌을 줄일 수 있습니다.
운영 규칙
- handler ID는 사람이 읽을 수 있어야 합니다.
- priority는 규칙 없이 임의 숫자를 쓰지 말고 밴드를 정해서 쓰는 것이 좋습니다.
- 레지스트리 파일은 가능한 한
handlers/handler-registry.ts처럼 한 곳에 둡니다. - 테스트에서는 레지스트리 숫자 자체보다 “실행 순서가 의도와 맞는지”를 검증하세요.
테스트 포인트
- 같은 액션에 여러 handler가 등록될 때 priority 순으로 실행되는가
- 특정 moduleId를 붙여도 ID 충돌이 없는가
- dispatch 이름 변경 시 actions와 handlers가 함께 깨지는가
- cleanup handler가 마지막에 실행되는가