이벤트 기반 아키텍처 (EDA) 활용해보기
클릭 이벤트부터 컴포넌트 간 느슨한 결합까지, 복잡한 의존성을 해결하는 방법을 장바구니 예시로 알아봅니다.
들어가며: “정해진 시간” vs “사건이 발생한 순간”
개발을 하다 보면 두 가지 방식의 일 처리를 마주하게 됩니다.
-
Batch/Cron 방식
”매일 밤 12시에 오늘 가입한 회원 목록을 정리해.”
정해진 시간에 능동적으로 데이터를 확인(Polling) 합니다. -
Event-Driven 방식
”회원가입 버튼이 눌리는 순간, 웰컴 메일을 보내고 통계에 반영해.”
사건이 발생한 시점에 수동적으로 반응합니다.
우리가 만드는 모던 웹 애플리케이션은 후자에 가깝습니다. 사용자의 행동에 즉각 반응해야 합니다.
즉, 버튼을 클릭하면 바로 피드백이 와야 하고, 데이터가 변경되면 화면이 즉시 업데이트되어야 합니다.
이러한 EDA(Event-Driven Architecture) 적 사고가 단순히 백엔드뿐만 아니라 프론트엔드 아키텍처의 핵심임을 깨닫게 되었습니다.
1. 프론트엔드는 태생부터 EDA였다.
브라우저는 거대한 이벤트 루프다.
프론트엔드 개발자라면 누구나 아는 addEventListener
사실 이것이 EDA의 가장 원시적이고 완벽한 형태입니다.
const button = document.querySelector('button');
// Producer: 버튼 (클릭 발생)
// Consumer: 콜백 함수 (클릭 처리)
button.addEventListener('click', () => {
console.log('클릭됨!');
});여기서 중요한 점은 버튼(Producer)은 그저 “나 눌렸어!”라고 브라우저(Channel)에 알릴 뿐, 누가 자신의 클릭을 listen을 가지고 있는지 전혀 모르고 있다는 점 입니다.
이것이 바로 느슨한 결합 의 시작입니다.
React의 useEffect도 결국 구독이다.
React의 useEffect도 EDA 관점에서 해석할 수 있습니다.
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`count가 ${count}로 변경됨`);
}, [count]);count상태 변경은 이벤트.Dependency Array > [count]는 구독(Subscribe) 행위.useEffect내부 로직은 이벤트를 처리하는 Consumer
우리는 알게 모르게 이미 이벤트 기반으로 사고하고 있었어요. 👀
2. 아키텍처로 확장하기: “장바구니 담기”의 딜레마
조금 더 들어가서, “장바구니 담기” 기능을 구현한다고 가정 하겠습니다.
전통적인 방식 (강한 결합)
사용자가 ‘담기’ 버튼을 눌렀다고 가정하면 보통 이런 로직으로 담을 수 있습니다.
// ProductCard.tsx
const handleAddToCart = async () => {
await addToCartApi(); // 1. API 호출
headerBadge.update(); // 2. 헤더 숫자 갱신
toast.show("담겼습니다"); // 3. 토스트 알림
gtm.push("add_to_cart"); // 4. GA 태그 전송
};
ProductCard컴포넌트가Header,Toast,GTM로직을 전부 알고 있어야 합니다.- 만약 “최근 본 상품에 추가”라는 기능이 또 생긴다면?
handleAddToCart함수를 또 수정해야 합니다.
- 컴포넌트 간 의존성이 복잡하게 얽히기 시작해요.
EDA 방식 (느슨한 결합)
그렇다면, 이걸 이벤트 기반으로 풀면 어떻게 될까요?

ProductCard는 “장바구니에 담겼음(ADD_TO_CART)” 이라는 이벤트만 발행하고 끝냅니다.
Header, Toast, GA 모듈은 각자 알아서 그 이벤트를 구독하고 처리하죠.
이렇게 하면 ProductCard는 다른 컴포넌트의 존재를 알 필요가 없습니다.
기능이 추가되어도 상품 카드 코드는 수정되지 않게됩니다.
바로, 이것이 우리가 추구해야 할 확장 가능한 프론트엔드 아키텍처 입니다!
3. CustomEvent 활용
이 개념을 실제로 구현할 때, 복잡한 라이브러리 없이 브라우저 내장 API인 CustomEvent만으로도 충분히 구현 가능합니다.
// hooks/useCustomEvent.ts
import { useEffect } from 'react';
export const useCustomEvent = <T>(eventName: string, handler: (data: T) => void) => {
useEffect(() => {
const listener = (e: Event) => {
const customEvent = e as CustomEvent<T>;
handler(customEvent.detail);
};
window.addEventListener(eventName, listener);
return () => window.removeEventListener(eventName, listener);
}, [eventName, handler]);
};
export const emitCustomEvent = <T>(eventName: string, data: T) => {
window.dispatchEvent(new CustomEvent(eventName, { detail: data }));
};- Producer (상품 카드)
const ProductCard = ({ product }) => {
const handleAddToCart = () => {
// API 호출 후 이벤트만 던진다. 누가 듣는지는 신경 안 쓴다.
emitCustomEvent('CART_UPDATED', { count: 1 });
};
return <button onClick={handleAddToCart}>담기</button>;
};- Consumer (헤더, 토스트 등)
// Header.tsx (전혀 다른 파일)
const Header = () => {
const [count, setCount] = useState(0);
// 이벤트를 구독하고 있다가 반응한다.
useCustomEvent('CART_UPDATED', (data) => {
setCount(prev => prev + data.count);
});
return <header>장바구니: {count}</header>;
};이 방식은 Redux, Zustand 같은 전역 상태 관리 라이브러리의 내부 동작 원리 와도 일맥상통합니다.
상태 관리 라이브러리 역시 거대한 이벤트 버스 역할을 수행하기 때문입니다.
마치며: “느슨한 결합”의 미학
EDA의 핵심 가치는 느슨한 결합 입니다.
- 컴포넌트 간 직접 의존성을 제거한다.
- 한 부분을 수정해도 다른 부분에 영향이 적다.
- 기능 확장이 유연해진다.
물론 모든 로직을 이벤트로 처리하면 흐름을 파악하기 어려워지는(디버깅의 어려움) 트레이드오프가 존재하기도 합니다.
하지만 서로 관련 없는 컴포넌트 간의 통신이나, 확장성이 중요한 로직에서는 EDA가 강력한 무기가 되어줄 것 입니다.
핵심 정리
- 우리는 이미 EDA를 쓰고 있다.
addEventListener,useEffect, 상태 관리 라이브러리 — 전부 이벤트 기반
- 브라우저는 거대한 이벤트 루프다.
- 클릭, 네트워크, 렌더링 모두 이벤트로 처리
- 핵심은 느슨한 결합.
- Producer는 Consumer를 모른다.
- 그래서 독립적으로 수정/확장이 가능하다.
- CustomEvent로 직접 구현 가능
- 관련 없는 컴포넌트 간 통신에 유용하다.
- 적절히 사용하자.
- 디버깅 복잡도와 트레이드오프를 고려해야 한다.
📝 참고한 여러 레퍼런스 자료들
“액션을 디스패치하는 것을 애플리케이션에서 ‘이벤트를 트리거’ 하는 것으로 생각할 수 있습니다.
무언가 일어났고, 스토어에 알려주고 싶은 것입니다. 리듀서는 이벤트 리스너처럼 동작합니다.”
— Redux Fundamentals
- Zustand
// Zustand 내부 구현 (단순화)
const createStore = (createState) => {
let state;
const listeners = new Set();
const setState = (newState) => {
state = newState;
// 모든 리스너에게 알림 (Pub/Sub!)
listeners.forEach(listener => listener(state));
};
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
return { setState, getState, subscribe };
};상태 관리 = Pub/Sub 패턴 = EDA의 일종이다.
“느슨한 결합은 컴포넌트 간의 상호 의존도를 줄이는 것을 목표로 합니다. 그러나 완전히 분리하지는 않습니다.”
— Red Hat - What is EDA?
“Event Notification은 낮은 결합도를 제공하지만, 여러 이벤트에 걸친 논리적 흐름이 있을 때 문제가 될 수 있습니다. 그런 흐름은 어떤 프로그램 코드에도 명시적으로 드러나지 않습니다.”
— Martin Fowler - Event-Driven