Skip to content

Commit 6af9b81

Browse files
committed
post: 20251218 EDA 포스팅
1 parent 366a1e9 commit 6af9b81

File tree

5 files changed

+241
-0
lines changed

5 files changed

+241
-0
lines changed

posts/20251218.mdx

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
---
2+
title: "이벤트 기반 아키텍처 (EDA) 활용해보기"
3+
date: "2025-12-18"
4+
slug: "20251218"
5+
tag: "Frontend"
6+
category: "Frontend"
7+
description: "클릭 이벤트부터 컴포넌트 간 느슨한 결합까지, 복잡한 의존성을 해결하는 방법을 장바구니 예시로 알아봅니다."
8+
---
9+
10+
# 들어가며: "정해진 시간" vs "사건이 발생한 순간"
11+
개발을 하다 보면 두 가지 방식의 일 처리를 마주하게 됩니다.
12+
13+
- **Batch/Cron 방식**
14+
"매일 밤 12시에 오늘 가입한 회원 목록을 정리해."
15+
정해진 시간에 능동적으로 데이터를 확인(Polling) 합니다.
16+
17+
- **Event-Driven 방식**
18+
"회원가입 버튼이 **눌리는 순간**, 웰컴 메일을 보내고 통계에 반영해."
19+
사건이 발생한 시점에 수동적으로 반응합니다.
20+
21+
22+
우리가 만드는 모던 웹 애플리케이션은 후자에 가깝습니다. 사용자의 행동에 즉각 반응해야 합니다.
23+
즉, 버튼을 클릭하면 바로 피드백이 와야 하고, 데이터가 변경되면 화면이 즉시 업데이트되어야 합니다.
24+
25+
이러한 **EDA(Event-Driven Architecture)** 적 사고가 단순히 백엔드뿐만 아니라 프론트엔드 아키텍처의 핵심임을 깨닫게 되었습니다.
26+
27+
---
28+
29+
## 1. 프론트엔드는 태생부터 EDA였다.
30+
### 브라우저는 거대한 이벤트 루프다.
31+
32+
프론트엔드 개발자라면 누구나 아는 `addEventListener`
33+
사실 이것이 EDA의 가장 원시적이고 완벽한 형태입니다.
34+
35+
```typescript
36+
const button = document.querySelector('button');
37+
38+
// Producer: 버튼 (클릭 발생)
39+
// Consumer: 콜백 함수 (클릭 처리)
40+
button.addEventListener('click', () => {
41+
console.log('클릭됨!');
42+
});
43+
```
44+
45+
여기서 중요한 점은 버튼(Producer)은 그저 "나 눌렸어!"라고 브라우저(Channel)에 알릴 뿐, 누가 자신의 클릭을 listen을 가지고 있는지 전혀 모르고 있다는 점 입니다.
46+
47+
> 이것이 바로 **느슨한 결합** 의 시작입니다.
48+
49+
<br />
50+
51+
52+
### React의 `useEffect`도 결국 구독이다.
53+
React의 `useEffect`도 EDA 관점에서 해석할 수 있습니다.
54+
55+
```
56+
const [count, setCount] = useState(0);
57+
58+
useEffect(() => {
59+
console.log(`count가 ${count}로 변경됨`);
60+
}, [count]);
61+
```
62+
63+
- `count` 상태 변경은 이벤트.
64+
- `Dependency Array > [count]` 는 구독(Subscribe) 행위.
65+
- `useEffect` 내부 로직은 이벤트를 처리하는 Consumer
66+
67+
우리는 알게 모르게 이미 이벤트 기반으로 사고하고 있었어요. 👀
68+
69+
<br />
70+
71+
## 2. 아키텍처로 확장하기: "장바구니 담기"의 딜레마
72+
조금 더 들어가서, "장바구니 담기" 기능을 구현한다고 가정 하겠습니다.
73+
74+
### **전통적인 방식 (강한 결합)**
75+
사용자가 '담기' 버튼을 눌렀다고 가정하면 보통 이런 로직으로 담을 수 있습니다.
76+
```
77+
// ProductCard.tsx
78+
const handleAddToCart = async () => {
79+
await addToCartApi(); // 1. API 호출
80+
headerBadge.update(); // 2. 헤더 숫자 갱신
81+
toast.show("담겼습니다"); // 3. 토스트 알림
82+
gtm.push("add_to_cart"); // 4. GA 태그 전송
83+
};
84+
```
85+
86+
![구시대적인 플로우차트](/img/posts/20251218/i1amg6.png)
87+
88+
- `ProductCard` 컴포넌트가 `Header`, `Toast`, `GTM` 로직을 전부 알고 있어야 합니다.
89+
- 만약 "최근 본 상품에 추가"라는 기능이 또 생긴다면?
90+
- `handleAddToCart` 함수를 또 수정해야 합니다.
91+
- 컴포넌트 간 의존성이 복잡하게 얽히기 시작해요.
92+
93+
<br />
94+
95+
### **EDA 방식 (느슨한 결합)**
96+
그렇다면, 이걸 이벤트 기반으로 풀면 어떻게 될까요?
97+
98+
![프론트엔드 EDA 흐름도](/img/posts/20251218/flow-chart.png)
99+
100+
`ProductCard`**"장바구니에 담겼음(ADD_TO_CART)"** 이라는 이벤트만 발행하고 끝냅니다.
101+
`Header`, `Toast`, `GA` 모듈은 각자 알아서 그 이벤트를 구독하고 처리하죠.
102+
103+
이렇게 하면 `ProductCard`는 다른 컴포넌트의 존재를 알 필요가 없습니다.
104+
기능이 추가되어도 상품 카드 코드는 수정되지 않게됩니다.
105+
106+
> 바로, 이것이 우리가 추구해야 할 확장 가능한 프론트엔드 아키텍처 입니다!
107+
108+
<br />
109+
110+
## 3. CustomEvent 활용
111+
이 개념을 실제로 구현할 때, 복잡한 라이브러리 없이 브라우저 내장 API인 `CustomEvent`만으로도 충분히 구현 가능합니다.
112+
113+
```
114+
// hooks/useCustomEvent.ts
115+
import { useEffect } from 'react';
116+
117+
export const useCustomEvent = <T>(eventName: string, handler: (data: T) => void) => {
118+
useEffect(() => {
119+
const listener = (e: Event) => {
120+
const customEvent = e as CustomEvent<T>;
121+
handler(customEvent.detail);
122+
};
123+
124+
window.addEventListener(eventName, listener);
125+
return () => window.removeEventListener(eventName, listener);
126+
}, [eventName, handler]);
127+
};
128+
129+
export const emitCustomEvent = <T>(eventName: string, data: T) => {
130+
window.dispatchEvent(new CustomEvent(eventName, { detail: data }));
131+
};
132+
```
133+
134+
1. Producer (상품 카드)
135+
```
136+
const ProductCard = ({ product }) => {
137+
const handleAddToCart = () => {
138+
// API 호출 후 이벤트만 던진다. 누가 듣는지는 신경 안 쓴다.
139+
emitCustomEvent('CART_UPDATED', { count: 1 });
140+
};
141+
142+
return <button onClick={handleAddToCart}>담기</button>;
143+
};
144+
```
145+
146+
2. Consumer (헤더, 토스트 등)
147+
```
148+
// Header.tsx (전혀 다른 파일)
149+
const Header = () => {
150+
const [count, setCount] = useState(0);
151+
152+
// 이벤트를 구독하고 있다가 반응한다.
153+
useCustomEvent('CART_UPDATED', (data) => {
154+
setCount(prev => prev + data.count);
155+
});
156+
157+
return <header>장바구니: {count}</header>;
158+
};
159+
```
160+
161+
이 방식은 Redux, Zustand 같은 전역 상태 관리 라이브러리의 내부 동작 원리 와도 일맥상통합니다.
162+
상태 관리 라이브러리 역시 거대한 이벤트 버스 역할을 수행하기 때문입니다.
163+
164+
## 마치며: "느슨한 결합"의 미학
165+
<aside data-type="note">
166+
코드를 잘 짜는 것보다 **'고치기 쉽게 짜는 것'** 이 훨씬 중요하다고 생각합니다.
167+
</aside>
168+
169+
EDA의 핵심 가치는 **느슨한 결합** 입니다.
170+
- 컴포넌트 간 직접 의존성을 제거한다.
171+
- 한 부분을 수정해도 다른 부분에 영향이 적다.
172+
- 기능 확장이 유연해진다.
173+
174+
물론 모든 로직을 이벤트로 처리하면 흐름을 파악하기 어려워지는(디버깅의 어려움) 트레이드오프가 존재하기도 합니다.
175+
하지만 서로 관련 없는 컴포넌트 간의 통신이나, 확장성이 중요한 로직에서는 EDA가 강력한 무기가 되어줄 것 입니다.
176+
177+
---
178+
179+
### 핵심 정리
180+
181+
- 우리는 이미 EDA를 쓰고 있다.
182+
- `addEventListener`, `useEffect`, 상태 관리 라이브러리 — 전부 이벤트 기반
183+
- 브라우저는 거대한 이벤트 루프다.
184+
- 클릭, 네트워크, 렌더링 모두 이벤트로 처리
185+
- 핵심은 느슨한 결합.
186+
- Producer는 Consumer를 모른다.
187+
- 그래서 독립적으로 수정/확장이 가능하다.
188+
- CustomEvent로 직접 구현 가능
189+
- 관련 없는 컴포넌트 간 통신에 유용하다.
190+
- 적절히 사용하자.
191+
- 디버깅 복잡도와 트레이드오프를 고려해야 한다.
192+
193+
194+
---
195+
### 📝 참고한 여러 레퍼런스 자료들
196+
197+
> "액션을 디스패치하는 것을 애플리케이션에서 **'이벤트를 트리거'** 하는 것으로 생각할 수 있습니다.
198+
무언가 일어났고, 스토어에 알려주고 싶은 것입니다. 리듀서는 이벤트 리스너처럼 동작합니다."
199+
— Redux Fundamentals
200+
201+
- Zustand
202+
```
203+
// Zustand 내부 구현 (단순화)
204+
const createStore = (createState) => {
205+
let state;
206+
const listeners = new Set();
207+
208+
const setState = (newState) => {
209+
state = newState;
210+
// 모든 리스너에게 알림 (Pub/Sub!)
211+
listeners.forEach(listener => listener(state));
212+
};
213+
214+
const subscribe = (listener) => {
215+
listeners.add(listener);
216+
return () => listeners.delete(listener);
217+
};
218+
219+
return { setState, getState, subscribe };
220+
};
221+
```
222+
223+
> 상태 관리 = Pub/Sub 패턴 = EDA의 일종이다.
224+
225+
226+
> "느슨한 결합은 컴포넌트 간의 상호 의존도를 줄이는 것을 목표로 합니다. 그러나 완전히 분리하지는 않습니다."
227+
— Red Hat - What is EDA?
228+
229+
> "Event Notification은 낮은 결합도를 제공하지만, 여러 이벤트에 걸친 논리적 흐름이 있을 때 문제가 될 수 있습니다. 그런 흐름은 어떤 프로그램 코드에도 명시적으로 드러나지 않습니다."
230+
> — Martin Fowler - Event-Driven
231+
232+
---
233+
### Ref
234+
- Event Loop: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop
235+
- CustomEvent: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
236+
- useEffect: https://react.dev/reference/react/useEffect
237+
- Fowler Event-Driven: https://martinfowler.com/articles/201701-event-driven.html
238+
- Redux 개념: https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow
239+
- Zustand 소스코드: https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts
240+
168 KB
Loading
134 KB
Loading
970 KB
Loading

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"isolatedModules": true,
1414
"jsx": "preserve",
1515
"incremental": true,
16+
"forceConsistentCasingInFileNames": true,
1617
"plugins": [
1718
{
1819
"name": "next"

0 commit comments

Comments
 (0)