Backgrounds
최근 프로젝트에서 다양한 메시지 핸들러 클래스를 생성하고 관리할 필요가 생겼습니다.
메시지 핸들러는 프로젝트 내 모듈들 간의 통신을 담당하며
각각의 메시지 핸들러는 자신이 해야할 일(action)과 데이터(payload), 실제 송수신 방법에 대한 메서드를 구현하고 있었습니다.
(ex "사이드 패널 활성화 메시지 핸들러", "팝업 활성화 메시지 핸들러", "타겟 정보 데이터 전송 메시지 핸들러")
이때 앱 내 메시지 핸들러의 종류가 점차 많아짐에 따라,
각 메시지 타입(action)에 대응하는 구체적인 메시지 핸들러 클래스들을 손쉽게 생성하고 관리하기 위해 팩토리 패턴을 적용했습니다.
import {
MessageHandlerPort,
Message,
} from "@/application/message-handler/port";
import { CheckSidePanelActiveMessageHandler, CheckPopupActiveMessageHandler, TargetElementMessageHandler } from "../handlers";
type ActionKeyType = Message["action"]
type MessageHandlerConstructor<T extends MessageHandlerPort<any>> = new () => T;
const handlerInstances: Record<ActionKeyType, MessageHandlerPort<any>> = {};
const handlerMap: Record<
ActionKeyType,
MessageHandlerConstructor<MessageHandlerPort<any>>
> = {
"check-side-panel-active": CheckSidePanelActiveMessageHandler,
"check-popup-active": CheckPopupActiveMessageHandler,
"target-element": TargetElementMessageHandler,
};
export class MessageHandlerRegistry {
static getHandler(action: ActionKeyType): MessageHandlerPort<any> {
if (!handlerInstances[action]) {
const HandlerClass = handlerMap[action];
if (!HandlerClass) {
throw new Error(`No handler found for action: ${action}`);
}
handlerInstances[action] = new HandlerClass();
}
return handlerInstances[action];
}
}
애플리케이션 내부적으로 각 메시지 핸들러 인스턴스를 싱글톤으로 구현해야 했고
이를 위해 Factory보다는 Registry를 생성하여 만들어진 메시지 핸들러 인스턴스들을 handlerInstances 상태로 관리했습니다.
이를 통해 사용처에서는 각 메시지 핸들러 객체들의 구현을 신경 쓰지 않고,
Registry가 제공하는 인터페이스에 맞는 인스턴스들을 반환받을 수 있었습니다.
// 송신
MessageHandlerRegistry.getHandler('check-side-panel-active').send()
// 수신
MessageHandlerRegistry.getHandler('check-side-panel-active').addListener((message) => {})
Problems
이때 send나 addListener 메서드 인자에 대한 타입 추론이 이뤄지지 않는 문제가 발생했습니다.
각 메시지 핸들러 클래스는 공통된 인터페이스를 공유하면서도 내부 구현이 달랐고
따라서 사용처에서 MessageHandlerRegistry로 조회한 message handler들의 타입을 알 필요가 있었습니다.
// 송신
// TODO: check-side-panel-active 메시지 핸들러는, send 메서드에 어떤 값을 허용하는지 추론되어야 함
MessageHandlerRegistry.getHandler('check-side-panel-active').send()
// 수신
// TODO: check-side-panel-active 메시지 핸들러는, addListener 메서드의 콜백 함수의 인자로 어떤 값이 넘어오는지 추론되어야함
MessageHandlerRegistry.getHandler('check-side-panel-active').addListener((message) => {})
Caused
타입 추론이 불가능한 이유는 단순합니다. 타입 지정을 해주지 않았기 때문!
// MessageHandlerPort는 send, addListener가 정의된 인터페이스에 불과합니다
getHandler(action: Message["action"]): MessageHandlerPort<any>
MessageHandlerRegistry의 getHandler 메서드는 MessageHandlerPort<any>를 반환할 뿐이고,
해당 타입은 각각의 메시지 핸들러 클래스가 구현(implement)하는 단순 인터페이스에 불과합니다!
즉, MessageHandlerRegistry로 반환받은 인스턴스는
실제 구현체의 타입이 아닌, MessageHandlerPort의 타입으로밖에 추론될 수 없는 것이죠.
Solutions
Registry 클래스 생성 시, 각 메시지 핸들러의 타입 맵을 정의하는 것으로 해결할 수 있습니다!
import {
MessageHandlerPort,
Message,
} from "@/application/message-handler/port";
import { CheckSidePanelActiveMessageHandler, CheckPopupActiveMessageHandler, TargetElementMessageHandler } from "../handlers";
// 메시지 핸들러의 타입 맵을 정의!
interface MessageHandlerMap {
"check-side-panel-active": CheckSidePanelActiveMessageHandler,
"check-popup-active": CheckPopupActiveMessageHandler,
"target-element": TargetElementMessageHandler,
}
type ActionKeyType = Message["action"]
type MessageHandlerConstructor<T extends MessageHandlerPort<any>> = new () => T;
const handlerInstances: Partial<{ [K in keyof MessageHandlerMap]: MessageHandlerMap[K] }> = {};
const handlerMap: { [K in ActionKeyType]: MessageHandlerConstructor<MessageHandlerMap[K]> } = {
"check-side-panel-active": CheckSidePanelActiveMessageHandler,
"check-popup-active": CheckPopupActiveMessageHandler,
"target-element": TargetElementMessageHandler,
};
export class MessageHandlerRegistry {
static getHandler<K extends keyof MessageHandlerMap>(action: K): MessageHandlerMap[K] {
if (!handlerInstances[action]) {
const HandlerClass = handlerMap[action];
if (!HandlerClass) {
throw new Error(`No handler found for action: ${action}`);
}
handlerInstances[action] = new HandlerClass();
}
return handlerInstances[action] as MessageHandlerMap[K];
}
}
MessageHandlerMap 인터페이스 정의를 통해
앱에서 지정한 메시지 액션 타입(ActionKeyType)으로 각각의 클래스 구현체 타입을 반환받을 수 있는 것이죠!
// 송신
// data는 check-side-panel-active 메시지 핸들러가 정의한 타입입니다
MessageHandlerRegistry.getHandler('check-side-panel-active').send(data)
// 수신
// message은 check-popup-active 메시지 핸들러가 정의한 타입입니다
MessageHandlerRegistry.getHandler('check-popup-active').addListener((message) => {})
MessageHandlerRegistry에서도 getHandler 메서드의 반환값을 MessageHandlerMap[k]로 선언함으로써,
입력받은 action(ActionKeyType과 동일하겠죠)으로 해당 구현체의 타입을 추론할 수 있게 됩니다!
'source-code > TypeScript' 카테고리의 다른 글
"'React'은(는) UMD 전역을 참조하지만 현재 파일은 모듈입니다" 에러 해결 (2) | 2023.11.18 |
---|---|
getElementsByClassName에 for each 사용해 HTMLElement 접근하기 (0) | 2023.11.16 |
react-hook-form TS와 똑똑하게 사용하기 (0) | 2023.08.21 |
index signature 관련 타입 에러 (0) | 2023.08.21 |
JsToTs : axios (0) | 2023.08.21 |