이벤트 버스 단순 예시
이 방법은 옵저버 패턴으로 데이터를 공유하는 방법입니다. 프론트엔드 프레임워크를 사용하고 있으면 프레임워크에서 자주 사용하는 라이브러리를 사용할 것을 권장합니다.
const EventBus = new EventTarget();
class ClassA {
trigger() {
console.log('A 동작');
EventBus.dispatchEvent(new CustomEvent('A:done', { detail: 'A 실행 완료' }));
}
}
class ClassB {
constructor() {
EventBus.addEventListener('A:done', (e) => {
console.log('B: A 완료 이벤트 받음 →', e.detail);
});
}
}
const a = new ClassA();
const b = new ClassB();
a.trigger(); // B: A 완료 이벤트 받음 →A 실행 완료
EventTarget을 응용해서 만든 가장 단순한 예시입니다. 별도의 3개의 객체이지만 ClassB는 ClassA의 trigger가 실행할 dispatchEvent로 구독하게 만들 수 있습니다. EventBus를 싱글튼 패턴으로 감싸고 "A:done" 같은 문자열을 상수 혹은 키로 관리를 하면 전역상태를 대체하게 만드는 것도 가능합니다. 저는 이런 로직 구현은 복잡한 랜더링 태스크 매니저1가 없는 경우 한정해서 괜찮을 것 같습니다.
이벤트 버스 응용
실제 응용한 링크 전체 코드를 보고 싶다면 다음 3개의 링크를 보면 됩니다.
- 이런 구현을 해야 하는 상황은 2026년 대부분의 프론트엔드 개발자에게 해당하지 않을 것입니다.
// src/search-popup-bus.ts
type Events = 'click';
class PopupBus {
private static instance: PopupBus; // 싱글톤 인스턴스
private EventBus: EventTarget;
/**
* 싱글튼이라 접근이 불가능해야 함.
* 외부에서 new 불가능
*/
private constructor() {
const EventBus = new EventTarget();
this.EventBus = EventBus;
}
// 항상 같은 인스턴스 반환
public static getInstance(): PopupBus {
if (!PopupBus.instance) {
PopupBus.instance = new PopupBus();
}
return PopupBus.instance;
}
// 이벤트 발생 및 데이터 송신
public emit<T>(event: Events, detail: T): void {
const customEvent = new CustomEvent<T>(event, { detail });
this.EventBus.dispatchEvent(customEvent);
}
// 이벤트 구독 및 데이터 수신
public on<T>(event: Events, handler: (detail: T) => void): void {
this.EventBus.addEventListener(event, (e) => {
handler((e as CustomEvent<T>).detail);
});
}
}
export default PopupBus;
여기서 코드를 보면 먼저 파악해야 할 것은 싱글튼 패턴을 사용한다는 것입니다. 싱글튼 패턴을 굳이 사용하는 이유(항상 같은 메모리 주소를 보장해야 하는 이유)가 있습니다. 이 인스턴스는 각각 파일 2개에서 접근해야 합니다.
// src/nav.ts
import PopupBus from './search-popup-bus';
/**
* nav를 동적으로 만들어서 다른 페이지 이동할 때 쿼리파라미터를 유지함.
*/
const nav = () => {
const popupBus = PopupBus.getInstance();
// ... (생략)
popupBtn.addEventListener('click', () => {
popupBus.emit('click', () => {});
});
// ... (생략)
};
export default nav;
Nav에 팝업을 활성화 할 수 있는 버튼입니다. popupBtn은 동적으로 생성한 DOM 요소입니다. 팝업을 활성화 하기위한 버튼입니다. 이 버튼을 누르는 이벤트는 pub sub 패턴으로 생각할 때 publish를 하는 부분입니다. addEventListener로 클릭이라는 이벤트가 발생하면 emit을 통해 이벤트가 발생했다는 사실을 전달합니다. 지금 예시의 경우 데이터를 전달해야 하는 상황이 아닙니다.
// src/search.ts
/**
* @fileoverview ctrl + k로 볼 수 있는 창을 다룸
*/
import PopupBus from './search-popup-bus';
// ... (생략)
const search = async (data: Data) => {
// ... (생략)
const popupBus = PopupBus.getInstance();
popupBus.on('click', () => {
searchElem.appendChild(createPopup());
searchElem.appendChild(createOverlay());
focusSearchInput();
// url에 search open 추가
const url = new URL(window.location.href);
url.searchParams.set('search', 'open');
window.history.pushState({}, '', url);
popupType = 'search';
});
// ... (생략)
};
export default search;
검색 팝업을 초기화하는 부분입니다. 여기서 pub sub 패턴으로 생각해보면 여기는 subscribe하는 곳입니다. 실제 팝업을 활성화하는 로직들이 있습니다. DOM 요소를 생성하고 url을 갱신합니다.
생각
- 제어흐름 자체는 문서화만 잘 하면 명확합니다.
- 타입 선언을 잘하면 자동완성으로 확장할 수 있습니다.
- 데이터는 어떻게 보관하게 만들지는 고민입니다. 순수하게 이벤트만 전달해주고 있습니다. 데이터를 전달하고 그 데이터를 인스턴스에 저장을 해야 한다면 어떻게 설계할지는 고민이 됩니다.
Footnotes
-
vue의 tick 사이클 비슷합니다. ↩