자바스크립트와 타입스크립트에서 이벤트 버스 구현
이벤트 버스(Event Bus)는 여러 모듈간의 통신 메커니즘에 사용한다. 상태를 관리하기 위해 모듈마다 이벤트를 작성하면 프로젝트가 커질수록 유지관리 측면에 애로사항이 생긴다.
이 문서에서는 특정 모듈에서 이벤트를 발생시키면, 이벤트 관리 센터에서 연관된 모든 모듈에 전파하는 발행 – 구독 패턴을 사용하였다. 구현 자체는 이벤트 핸들러가 아닌, 클래스를 사용하였다.
React, Vue 같은 프레임워크를 사용하지 않거나, 프레임워크 도입 전에 학습 차원에서 작동 원리를 알아보고자 한다면, 자바스크립트와 타입스크립트로 직접 이벤트 버스를 구현해 보자.
- Vue 공식문서에서
대형 프로젝트가 아닌 이상, 상태관리의 많은 사례 중 대부분은 전역 이벤트 버스만으로도 충분히 해결할 수 있다.
- 참고문헌
원리
이벤트 버스는 기본적으로 발표-구독 디자인 모델을 채택했다. 예를 들어, 여러 모듈A, B, C가 하나의 이벤트 EventX를 구독한다.
특정 모듈X가 이벤트 버스에 EventX를 발행하면 이벤트 버스는 모든 구독자에 매개변수와 함께 이벤트를 통지하고, 구독자 모듈 A, B, C는 이를 수신한다.
![event-bus 이벤트 버스 원리](https://newstoday.io/wp-content/uploads/2022/05/event-bus.png)
구현 - 자바스크립트
자바스크립트와 타입스크립트에서 이벤트 버스 구현 방법은 다음과 같다.
- 먼저 이벤트 버스 클래스를 구성하고, 모든 이벤트를 저장 할 빈 객체를 생성하고 초기화한다.
- 이벤트를 구독할 때, 이벤트 이름을 키, 받은 메시지를 값으로 실행하는 콜백 함수를 저장할 배열을 생성한다. 하나의 이벤트에 여러 명의 구독자가 있을 수 있기 때문에, 이곳의 콜백함수는 배열로 저장해야 한다.
- 이벤트를 발행할 때, 이벤트 배열에 저장한 이름에 대응하는 모든 콜백함수를 순서대로 실행한다.
아래는 브라우저 개발자 도구의 콘솔에 붙여넣기하여 직접 결과값을 확인할 수 있는 코드다.
이벤트가 발행되면 해당 이벤트를 구독하는 모든 모듈의 콜백함수가 실행되는 기본적인 작동이 구현되었다. 실제 프로덕션 단계에서는 좀 더 높은 요구사항을 가진 이벤트 버스가 필요할테니 이를 더 개선해보자.
개선- 매개변수 전달
발행자는 이벤트 이름 외에 추가 매개변수를 이벤트 버스에 전달하고, 콜백함수를 실행할 때 이 매개변수를 함께 전달한다.
개선 - 구독 해지
어느 지점부터는 구독자가 이벤트 수신을 중단해야 하는 경우가 있다. 이것은 구독 취소 기능과 관련이 있기 때문에 코드를 리팩토링 한다.
우선, 구독할 때마다 유일한 id를 가진 구독 취소 내부 함수를 생성하고, 구독 취소를 원하는 지점에서 호출하여 현재 구독중인 콜백함수를 삭제한다.
// 매번 이벤트를 구독할 때마다 구독 취소를 위한 내부 함수 생성
const unSubscribe = () => {
// 이 구독자의 콜백함수 제거
delete this.eventObject[eventName][id];
};
다음으로, 구독한 콜백함수 목록은 객체에 저장하고. 모든 구독 취소 함수에 유일한 id를 설정해서 구독 취소 시 삭제 효율을 높인다.
콜백함수 목록을 기존 그대로 배열에 저장하면 split 메서드를 사용해서 삭제해야 하는데, 이것은 delete 메서드를 사용하는 것보다 효율이 낮다.
개선 - 한번만 구독
이벤트를 한번만 받는 경우를 위해 subscribeOnce 메서드를 추가할 것인데, 내부 실현은 subscribe 메서드와 거의 같고 야주 약간의 차이만 있다.
callbackId 전에 문자 “d“를 추가해서 이것이 실행 후 삭제해야 할 구독임을 나타낸다.
// 한번만 구독할 콜백함수 마크
const id = "d" + this.callbackId++;
그리고 발행자는 지금 실행한 콜백함수의 id에 문자 “d”가 포함되었는지 살핀 후 해당 콜백함수를 제거할지 결정한다.
// 한번만 구독한 경우 해당 콜백함수를 제거
if (id[0] === "d") {
delete callbackObject[id];
}
개선 - 이벤트 제거
이번에는 clear 메서드를 통해 지정한 구독의 일부, 혹은 모든 구독을 제거하고자 한다. 구독 취소 로직과 비슷하지만 여기서는 훨씬 간결하게 처리한다.
// 이벤트 제거
clear(eventName){
// 매개변수로 이름이 제공되지 않으면 모든 이벤트 제거
if(!eventName){
this.eventObject = {}
return
}
// 지정된 이름의 이벤트 제거
delete this.eventObject[eventName]
}
타입스크립트 개선 - 싱글톤 패턴
통상적으로 이벤트 버스는 하나만 있으면 되는데. 여기에는 두 가지 상황이 있다. 하나는 높은 우위에 있는 싱글톤 인스턴스를 유지하는 것과, 전역 이벤트 버스다.
상부 인스턴스로 싱글톤 유지
이벤트 버스를 상부 인스턴스로 가져오려면 이벤트 버스 생성자 함수의 인스턴스가 하나만 있는지 확인하면 된다.
상부 인스턴스가 여러 개 있다면 이벤트 버스 역시 여러 개 있다는 것이며, 각각의 상부 인스턴스는 개별적으로 자체 이벤트 버스를 제어한다.
먼저, 기존 이벤트 버스의 인터페이스를 상속하는 새로운 클래스를 생성한다.
상부 인스턴스에 변수를 설정하고, 처음 사용할 때만 초기화 되는 이벤트 버스를 저장하고, 다른 모듈에서 이벤트 버스를 사용할 때는 새로운 인스턴스를 생성하지 않고 기존 인스턴스를 직접 사용한다.
// 상부 인스턴스
class LWebApp {
private _eventBus?: EventBus;
constructor() {}
public getEventBus() {
// 다른 인스턴스 확인 후 초기화
if (this._eventBus == undefined) {
this._eventBus = new EventBus();
}
// 인스턴스를 호출할 때마다 하나의 인스턴스만 가져와 LWebApp의 단일 인스턴스로 유지
return this._eventBus;
}
}
// 용례
const eventBus = new LWebApp().getEventBus();
전역 싱글톤(전역 이벤트 버스)
어떤 경우, 사실 대다수의 경우 모든 모듈이 동일한 이벤트 버스를 공유하는 전역 이벤트 버스를 필요로 한다. 이 디자인은 많은 이벤트를 중앙집중 방식으로 더 쉽게 관리할 수 있는 장점이 있다.
내부 실현은 앞서 살펴본 방식과 유사하며, _eventBus 및 getEventBus를 정적 프로퍼티와 정적 메서드로 바꾼다는 차이점만 있다.
따라서 EventBusTool 클래스를 인스턴스화 할 필요없이 정적 메서드를 그대로 사용하면 된다.
class EventBusTool {
private static _eventBus?: EventBus;
constructor() {}
public static getEventBus(): EventBus {
if (this._eventBus == undefined) {
this._eventBus = new EventBus();
}
return this._eventBus;
}
}
// 용례
const eventBus = EventBusTool.getEventBus();
마치며
이로써 이벤트 버스 구현에 필요한 기본적인 기능을 구현했다. 점진적으로 필요한 기능을 추가해 나가는 원문 저작자의 세심한 배려에 감사함을 표한다.
스스로 발행-구독 모델을 실현해 봄으로서 클래식 디자인 모델에 대한 깊이있는 이해해 도움이 됐길 바란다.