타입스크립트에서 이벤트 처리기 구현하기

타입스크립트에서 이벤트 처리기 구현하기

이벤트 처리

C#을 할 땐 이벤트 처리가 굉장히 간편했다. 언어 자체에서 event-driven 방식을 지원하기 때문이다. 자바스크립트(및 타입스크립트)를 처음 접했을 땐 당연히 자바스크립트에서도 이런 방식으로 손쉽게 이벤트 처리를 할 수 있을 줄 알았다. 그도 그럴 것이 NodeJS가 출시되지도 않았던 시절에 자바스크립트 하면 프론트엔드 개발에서 접할 수밖에 없었고, element.addEventListener와 같은 메서드가 기본적으로 제공되었기 때문이다.

그러나 이런 추측은 반은 맞고 반은 틀렸다. NodeJS 이끌어가는 현 시대 자바스크립트 생태계에서 EventEmitter 클래스를 상속하여 손쉽게 이벤트 기반 클래스를 작성할 수 있기는 하다. 마찬가지로, 브라우저 단에서도 DOM 기반 클래스를 상속하면 대게 이벤트 처리가 가능하다. 여기까지 반이 맞고.

사용자 정의 프론트엔드 클래스에서 이벤트 핸들링

웹팩이나 Vite 등을 사용해 번들링된 프론트엔드 코드가 있다고 치자. 물론, 바닐라 코드여도 마찬가지다. 아무튼 프론트엔드에서 특정 용도로 제작한 어떠한 클래스가 이벤트 기반으로 동작하게 하려면 어떻게 해야 할까? 여기가 반이 틀린 부분이다.

이런 경우에는 직접 EventEmitter 클래스를 제작하여 사용해야 한다. addEventListener, on, off, emit 등을 사용할 수 있게 만들기 위해 다음과 같은 코드를 짜서 상속해야 한다.

export type IDisposable = {
    dispose(): void;
};

export type IListener<TEvent> = (event: TEvent) => void;

export type IEventEmitter<TEvent> = {
    emit: <TKey extends keyof TEvent>(key: TKey, event: TEvent[TKey]) => void;
    on: <TKey extends keyof TEvent>(key: TKey, listener: IListener<TEvent[TKey]>) => void;
    off: <TKey extends keyof TEvent>(key: TKey, listener: IListener<TEvent[TKey]>) => void;
};

export class EventEmitter<TEvent> implements IEventEmitter<TEvent> {
    private _listeners: Map<keyof TEvent, IListener<any>[]> = new Map();

    public emit<TKey extends keyof TEvent>(event: TKey, data: TEvent[TKey]): void {
        const registeredListeners = this._listeners.get(event);

        if (!registeredListeners) {
            return;
        }

        for (const listener of registeredListeners) {
            listener(data);
        }
    }

    public off<TKey extends keyof TEvent>(event: TKey, listener: IListener<TEvent[TKey]>): void {
        const registeredListeners = this._listeners.get(event);

        if (!registeredListeners) {
            return;
        }

        const index = registeredListeners.indexOf(listener);

        if (index === -1) {
            return;
        }

        registeredListeners.splice(index, 1);
    }

    public on<TKey extends keyof TEvent>(event: TKey, listener: IListener<TEvent[TKey]>): IDisposable {
        const registeredListeners = this._listeners.get(event);

        if (!registeredListeners) {
            throw new Error(`No listeners registered for event ${String(event)}`);
        }

        registeredListeners.push(listener);

        return {
            dispose: () => {
                this.off(event, listener);
            },
        };
    }
}

위 코드는 타입스크립트 기준이라 복잡해 보일 수도 있으나, 자바스크립트라면 제네릭 타입 부분을 없애고 사용하면 된다.

사용 부분은 다음과 같다.

export type Info = {
    [url: string]: {
        site: SupportedSite;
        pageType: PageType;
        queuedAt?: Date;
    };
};

export type BackgroundWorkerEvents = {
    enqueued: Info[];
    dequeued: Info;
};

export default class BackgroundWorker extends EventEmitter<BackgroundWorkerEvents> {
    private static _instance?: BackgroundWorker;

    private static readonly _queueInfo: Info[] = [];

    private constructor() {
        super();
    }

    public static get instance(): BackgroundWorker {
        if (!this._instance) {
            this._instance = new BackgroundWorker();
        }

        return this._instance;
    }

    public dequeue(): Info | undefined {
        const shifted = BackgroundWorker._queueInfo.shift();

        if (shifted) {
            this.emit("dequeued", shifted);
        }

        return shifted;
    }

    public enqueue(infos: Info[]) {
        BackgroundWorker._queueInfo.push(..infos);

        this.emit("enqueued", infos);
    }
}

이렇게 하면 typescript 기준으로 타입 체크된 이벤트 기반 클래스를 작성할 수 있다.

댓글

이 블로그의 인기 게시물

C# 남아도는 메모리에도 불구하고 OutOfMemoryException이 발생한다면?

USB를 뒤는 괜찮은데 앞에 꽂으면 인식이 힘들다?

MySQL 데이터 타입과 Java 데이터 타입 비교/매칭