이벤트 방출기
이벤트 방출기는 채널로 이벤트를 방출하고 채널에서 발생하는 이벤트를 리스닝하는 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
}
- 일단 Redis 클라이언트가 방출할 수 있는 모든 이벤트의 타입을 나열하는 객체 타입을 하나 정의했습니다.
- 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 코드에서 자주 볼 수 있습니다.
이 기법은 간결할 뿐 아니라 매우 안전합니다.
이런 식으로 방출기의 타입을 지정하면, 키의 철자가 틀리거나 인수 타입을 잘못 사용하거나 인수 전달을 빼먹는 실수를 방지할 수 있습니다.
또한 코드 편집기가 리스닝할 수 있는 이벤트와 이벤트의 콜백 매개변수 타입을 제시해 주게 되므로 다른 개발자에게 코드가 하는 일을 설명하는 문서화 역할도 제공합니다.
'👶 TypeScript' 카테고리의 다른 글
서버에서 타입스크립트 실행하기 (0) | 2023.02.01 |
---|---|
프로미스로 정상 회복하기 (0) | 2023.01.31 |
타입 안전 API (0) | 2023.01.29 |
TSX = JSX + TypeScript (0) | 2023.01.28 |
프론트엔드 프레임워크 - 리액트 (0) | 2023.01.27 |