TypeScript, 페이로드 타입에 안전한 이벤트 이미터(Event Emitter)

이벤트 종류마다 전달되는 데이터, 즉 페이로드(payload)의 타입은 모두 다릅니다. 예를들어 아래의 객체에서 key는 이벤트 이름, 값은 페이로드 형식입니다.

// 이벤트 맵 정의: 이벤트 이름 -> 페이로드 타입
type EventMap = {
  login: { userId: string; timestamp: number };
  logout: { userId: string };
  purchase: { itemId: string; price: number };
};

이제 위에서 정의된 이벤트를 등록하고 발생해주는(Emit) 다음과 같은 범용 클래스를 만들 수 있습니다.

class TypedEventEmitter<Events extends Record<string, any>> {
  ...
}

위의 TypedEventEmitter는 이벤트를 등록(on 매서드)할 수 있어야 하고 특정 상황에서 이벤트를 발생(emit 매서드)할 수 있어야 합니다. 그보다 먼저 등록될 이벤트를 저장하기 위한 컨테이너 필드가 필요합니다. 아래처럼요.

  private listeners: {
    [K in keyof Events]?: Array<(payload: Events[K]) => void>;
  } = {};

이벤트를 등록하는 on 매서드는 다음과 같습니다.

  on<K extends keyof Events>(
    event: K,
    callback: (payload: Events[K]) => void
  ): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(callback);
  }

그리고 이벤트를 발생시키는 emit 매서드는 다음과 같구요.

  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    this.listeners[event]?.forEach((cb) => cb(payload));
  }

이렇게 만든 클래스는 다음처럼 사용할 수 있습니다.

// 사용 예시
const emitter = new TypedEventEmitter<EventMap>();

emitter.on("login", (payload) => {
  // payload는 자동으로 { userId: string; timestamp: number } 타입으로 추론됨
  console.log(`${payload.userId}님이 로그인했습니다. (${payload.timestamp})`);
});

emitter.on("purchase", (payload) => {
  console.log(`아이템 ${payload.itemId} 구매, 가격: ${payload.price}원`);
});

emitter.emit("login", { userId: "user123", timestamp: Date.now() });
emitter.emit("purchase", { itemId: "item-42", price: 15000 });

// 아래처럼 잘못된 페이로드를 넘기면 컴파일 타임에 에러 발생!
// emitter.emit("login", { itemId: "wrong" }); // ❌ Type error

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다