👶 TypeScript

비동기 스트림 - 이벤트 방출기

개발자 린다씨 2023. 1. 30. 16:26
반응형

이벤트 방출기

이벤트 방출기는 채널로 이벤트를 방출하고 채널에서 발생하는 이벤트를 리스닝하는 API를 제공합니다.

interface Emitter {
    // 이벤트 방출
    emit(channel: string, value: unknown): void
    // 이벤트가 방출되었을 때 어떤 작업을 수행
    on(channel: string, f:(value: unknown)=>void): void
}

이벤트 방출기는 JavaScript에서 자주 사용하는 디자인 패턴입니다.

 

DOM 이벤트, 제이쿼리 이벤트, NodeJS의 EventEmitter 등을 사용하면서 이미 이벤트 방출기를 사용해 본 적 있으신 분도 계실 겁니다.

 

대부분의 언어에서 이런 형태의 이벤트 방출기는 안전하지 않습니다.

 

value의 타입이 특정 channel에 의존하는데 대부분의 언어에선 이런 관계를 타입으로 표현할 수 없기 때문입니다.

 

언어에서 오버로드된 함수 시그니처와 리터럴 타입을 모두 지원하지 않으면 "이 채널에선 이런 타입의 이벤트를 방출한다"라고 표현하는 데 문제가 생깁니다.

 

이벤트를 방출하고 각 채널에 리스닝하는 메서드를 생성하는 매크로로 이 문제를 해결할 수 있습니다.

 

하지만 TypeScript에선 이런 기법을 사용하지 않아도 타입 시스템을 이용해 자연스럽고 안전하게 표현할 수 있습니다.

 

예를 들어 NodeRedis 클라이언트를 사용한다고 가정해 봅시다.

 

다음은 이 클라이언트를 사용하는 예입니다.

import Redis from 'redis'

// 새로운 Redis 클라이언트 인스턴스 생성
let client = redis.createClient()

// 클라이언트가 방출하는 몇 가지 이벤트 리스닝
client.on('ready', () => console.info('클라이언트 준비 됨.'))
client.on('error', e => console.error('에러 발생!', e))
client.on('reconnecting', params => console.info('다시 연결중...', params))

Redis 라이브러리를 사용하는 프로그래머로서 on API를 사용할 때 콜백의 인수 타입이 무엇인지 궁금해졌다고 해봅시다.

 

하지만 인수의 타입은 Redis가 방출하는 채널에 따라 달라질 수 있으므로 한 가지 타입으론 표현할 수 없습니다.

 

만약 라이브러리의 저자였다면 오버로드된 타입을 사용하는 것이 가장 안전하고 구현하기도 간단한 방법이라고 생각했을 것입니다.

type RedisClient = {
    on(event: 'ready', f:() => void): void
    on(event: 'error', f:(e:Error)=> void):void
    on(event: 'reconnecting', f: (params: {attempt: number, delay: number}) => void): void
}

위의 코드는 잘 동작합니다만 뭔가 장황하니 이벤트 정의를 Events라는 별도의 타입으로 뽑아내보겠습니다.

 

매핑된 타입을 이용하겠습니다.

type Events = { // ①
    ready: void
    error: Error
    reconnecting: {attempt: number, delay: number}
}

type RedisClient = { // ②
    on<E extends keyof Events>(
        event: E,
        Async Streams | 190
        f: (arg: Events[E]) => void
    ): void
}
  1. 일단 Redis 클라이언트가 방출할 수 있는 모든 이벤트의 타입을 나열하는 객체 타입을 하나 정의했습니다.
  2. Events 타입을 매핑파면서 여기서 정의한 모든 이벤트에서 on을 호출할 수 있음을 TypeScript에게 알려줬습니다.

이어서 emit과 on 두 메서드의 타입을 가능한 안전하게 정의해서 NodeRedis 라이브러리를 더 안전하게 사용할 수 있도록 만들어보겠습니다.

type Events = {
    ready: void
    error: Error
    reconnecting: {attempt: number, delay: number}
}

type RedisClient = {
    on<E extends keyof Events>(
        event: E,
        f: (arg: Events[E]) => void
    ): void

    emit<E extends keyof Events>(
        event: E,
        arg: Events[E]
    ): void
}

이벤트 이름과 인수를 하나의 형태로 따로 빼내고, 리스너와 방출기를 생성하는데 이 형태에 매핑하는 패턴은 실무의 TypeScript 코드에서 자주 볼 수 있습니다.

 

이 기법은 간결할 뿐 아니라 매우 안전합니다.

 

이런 식으로 방출기의 타입을 지정하면, 키의 철자가 틀리거나 인수 타입을 잘못 사용하거나 인수 전달을 빼먹는 실수를 방지할 수 있습니다.

 

또한 코드 편집기가 리스닝할 수 있는 이벤트와 이벤트의 콜백 매개변수 타입을 제시해 주게 되므로 다른 개발자에게 코드가 하는 일을 설명하는 문서화 역할도 제공합니다.

반응형