이번 우아콘에서 이벤트 버스 패턴을 처음 접했을 때, “이렇게 간단한 구조로도 컴포넌트 간 통신이 가능하구나”라는 점이 가장 인상적이었습니다. 기존에는 상태 관리 도구를 통해서만 해결할 수 있다고 생각했던 부분들을 이벤트 버스를 통해 훨씬 가볍게 구현할 수 있다는 점이 새롭게 다가왔습니다. 특히 발행/구독(Pub-Sub) 모델의 단순함 속에서 느껴지는 구조적 유연함이 좋았지만, 반면 구독 해제를 잊거나 이벤트 이름을 잘못 관리하면 예기치 않은 버그로 이어질 수 있음을 주의해야겠다는 생각이 들었습니다. 학습을 통해 도구보다 패턴의 본질적 의도를 이해하는 것이 얼마나 중요한지를 배웠고, 앞으로는 상태 관리 라이브러리를 무조건 도입하기보다, 문제의 복잡도에 맞는 패턴을 선택하는 사고 방식을 더 의식적으로 가져가야겠다고 느꼈습니다.
이벤트 버스 패턴(Event Bus Pattern)으로 컴포넌트 간 통신하기
프론트엔드 개발을 하다 보면 컴포넌트 간에 데이터를 주고받거나 상태를 공유해야 하는 순간이 자주 있습니다. 특히 React나 Vue처럼 컴포넌트 기반 구조를 사용하는 경우, 부모-자식이 아닌 형제나 깊게 중첩된 컴포넌트 간의 통신은 생각보다 까다롭습니다.
일반적으로 이런 문제를 해결하기 위해 Redux, Zustand, Pinia 같은 전역 상태 관리 라이브러리를 도입하지만, 가벼운 이벤트 전달 하나를 위해 별도의 상태 관리 도구를 쓰는 것은 오버엔지니어링이 될 때가 있습니다.
이럴 때 유용하게 쓸 수 있는 패턴이 바로 이벤트 버스(Event Bus) 입니다.
1. 이벤트 버스란 무엇인가요?
이벤트 버스(Event Bus) 패턴은 발행/구독(Publish–Subscribe) 모델을 기반으로 한 통신 방식입니다. 쉽게 말해, ‘중간 다리’ 역할을 하는 객체를 통해 이벤트를 주고받는 구조입니다.
예를 들어 라디오 방송국을 생각해보면,
- DJ(Publisher)는 방송국(Event Bus)에 특정 주제(Event Name)로 방송을 송출합니다.
- 청취자(Subscriber)는 자신이 관심 있는 주파수(Event Name)에 맞춰 라디오를 켜두고 방송을 듣습니다.
- DJ와 청취자는 서로의 존재를 전혀 알 필요가 없습니다. 오직 '방송국'이라는 중간 매개체만 알면 됩니다.
이처럼 이벤트 버스는 어플리케이션 전역에 단 하나의 통신 채널(Bus)을 두고, 이 버스를 통해 이벤트를 주고받는 방식입니다.
- Publisher (발행자): 이벤트를 '발행'하는(보내는) 주체입니다.
- Subscriber (구독자): 이벤트를 '구독'하는(받는) 주체입니다.
- Event Bus: 발행자와 구독자 사이를 연결하는 중간 다리입니다.
2. 간단한 자바스크립트로 이벤트 버스 구현하기
이벤트 버스의 핵심은 이벤트 이름(eventName) 을 기준으로 콜백을 저장하고, 이후 해당 이벤트가 발생했을 때 연결된 콜백을 한 번에 실행하는 구조입니다.
class EventBus {
constructor() {
// 이벤트 이름(key)과 콜백 함수 배열(value)을 저장할 객체
this.listeners = {};
}
/**
* 이벤트 구독 (듣기)
* @param {string} eventName - 구독할 이벤트 이름
* @param {function} callback - 이벤트 발생 시 실행할 콜백 함수
*/
on(eventName, callback) {
if (!this.listeners[eventName]) {
this.listeners[eventName] = [];
}
this.listeners[eventName].push(callback);
}
/**
* 이벤트 발행 (보내기)
* @param {string} eventName - 발행할 이벤트 이름
* @param {*} data - 함께 전송할 데이터
*/
emit(eventName, data) {
if (this.listeners[eventName]) {
// 해당 이벤트를 구독 중인 모든 콜백 함수 실행
this.listeners[eventName].forEach(callback => {
callback(data);
});
}
}
/**
* 이벤트 구독 해제 (더 이상 듣지 않기)
* @param {string} eventName - 해제할 이벤트 이름
* @param {function} callback - 해제할 특정 콜백 함수
*/
off(eventName, callback) {
if (this.listeners[eventName]) {
// 해제하려는 콜백 함수를 리스너 배열에서 제거
this.listeners[eventName] = this.listeners[eventName].filter(
cb => cb !== callback
);
}
}
}
// 싱글톤(Singleton) 인스턴스로 생성하여 전역에서 사용
export const eventBus = new EventBus();
왜 이렇게 설계할까?
- listeners 객체
→ 여러 컴포넌트가 같은 이벤트를 동시에 구독할 수 있습니다.
→ 각 이벤트 이름을 key로, 콜백 배열을 value로 관리합니다. - emit()에서 forEach 호출
→ 이를 통해 1:N, N:N 구조의 통신이 가능합니다.
→ 발행 시, 등록된 모든 구독자 콜백이 호출됩니다. - off() 메서드→ 컴포넌트가 언마운트될 때 반드시 호출해야 합니다.
→ 메모리 누수를 막기 위해 필수입니다.
3. 장단점 정리
장점
- 강력한 디커플링
컴포넌트 간 의존성이 사라집니다. 서로의 존재를 몰라도 오직 eventBus 객체만 알면 됩니다. - 구조 단순성
복잡한 prop drilling이나 상태 끌어올리기 없이 간단하게 데이터 전달이 가능합니다. - 유연한 통신 구조
1:N, N:N 통신이 자유롭고, 특정 이벤트만 구독하는 것도 가능합니다.
단점
- 디버깅 어려움
이벤트가 어디서 발행됐고, 누가 구독 중인지 추적하기 어렵습니다. - 암묵적 의존성→ TypeScript enum으로 관리하는 것이 안전합니다.
이벤트 이름이 문자열 기반이라 오타(themeChange vs themeCange)에 취약합니다. - 메모리 누수 가능성
off()를 제대로 호출하지 않으면 컴포넌트가 사라져도 리스너는 남습니다. - 전역 남용 위험
모든 데이터를 eventBus로 관리하면 애플리케이션 흐름이 불투명해집니다.
4. 옵저버 패턴 vs 이벤트 버스
즉, 이벤트 버스는 옵저버 패턴보다 한 단계 더 느슨한 연결을 제공합니다.
모듈 간 독립성을 극대화하고 싶을 때 적합합니다.
| 구분 | 옵저버 패턴 | 이벤트 버스 패턴 |
| 구조 | 주체(Subject)가 관찰자(Observer)를 직접 관리 | 중간 매개체(Bus)가 모든 이벤트를 중앙 관리 |
| 관계 | 1:N | 1:N, N:N |
| 의존성 | 주체 ↔ 관찰자 연결 | 완전한 디커플링 |
| 예시 | React의 useEffect, DOM 이벤트 리스너 | mitt.js, Node.js EventEmitter |
5. 언제 사용하면 좋을까?
이벤트 버스는 전역 상태 관리 도구의 대체제가 아니라 오히려 간단한 알림을 전달할 때 가장 효율적인 패턴입니다. 예를 들어, 사용자의 로그인 만료나 로그아웃처럼 애플리케이션 전역에 알려야 하는 상황이 있을 때, 이벤트 버스를 통해 한 번의 신호로 여러 컴포넌트에 알림을 전달할 수 있습니다.
또한, 새로운 상태 관리 도구를 설치하기에는 규모가 작거나 단순히 특정 이벤트만 전달하고 싶은 경우에도 이벤트 버스는 좋은 선택입니다. 가벼운 이벤트 전파에 특화되어 있기 때문에, 작은 프로젝트나 독립적인 모듈 간 통신에도 부담 없이 적용할 수 있습니다.
반대로, 이벤트 버스를 핵심 데이터 흐름의 중심축으로 사용하는 것은 권장되지 않습니다. 상태가 여러 컴포넌트를 거치며 변하거나, 데이터 변경에 따라 로직이 복잡하게 얽히는 경우에는 Context API, Redux, Zustand 같은 전용 상태 관리 도구를 사용하는 것이 훨씬 안정적입니다.
정리하자면, 이벤트 버스는 복잡한 상태를 관리하기 위한 수단이 아니라, “상태 변화가 아닌 이벤트 알림”을 전달하는 신호 전달자(signal messenger) 로 사용할 때 가장 효과적입니다. 적재적소에 가볍게 활용하면, 애플리케이션의 의존성을 줄이면서도 컴포넌트 간의 소통을 유연하게 만들어줍니다.
'📚 CS > Basic' 카테고리의 다른 글
| [CS] 디자인 패턴 알아보기 (0) | 2025.10.18 |
|---|---|
| [CS] CORS(Cross-Origin Resource Sharing)는 왜 필요할까요? (프록시 서버로 우회하기) (0) | 2025.10.11 |
| [CS] API와 아키텍처 (1) | 2025.08.26 |
| [CS] 컴퓨터 네트워크 기초 (2) | 2025.08.25 |