이벤트 버스 단순 예시

이 방법은 옵저버 패턴으로 데이터를 공유하는 방법입니다. 프론트엔드 프레임워크를 사용하고 있으면 프레임워크에서 자주 사용하는 라이브러리를 사용할 것을 권장합니다.

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개의 객체이지만 ClassBClassAtrigger가 실행할 dispatchEvent로 구독하게 만들 수 있습니다. EventBus를 싱글튼 패턴으로 감싸고 "A:done" 같은 문자열을 상수 혹은 키로 관리를 하면 전역상태를 대체하게 만드는 것도 가능합니다. 저는 이런 로직 구현은 복잡한 랜더링 태스크 매니저1가 없는 경우 한정해서 괜찮을 것 같습니다.

이벤트 버스 응용

실제 응용한 링크 전체 코드를 보고 싶다면 다음 3개의 링크를 보면 됩니다.

// 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

  1. vue의 tick 사이클 비슷합니다.