수많은 상태(State)와 Props 드릴링으로 뒤엉킨 컴포넌트 계층 구조를 보며 한숨을 내쉰 적이 한두 번이 아닐 거예요. 프로젝트 규모가 커질수록 부모-자식 관계를 넘어선 컴포넌트 간의 데이터 전달은 개발자의 생산성을 갉아먹는 주범이 되곤 하죠. 우리는 그동안 이를 해결하기 위해 중앙 집중식 상태 관리 라이브러리에 의존해 왔지만, 모든 데이터가 전역 상태에 머무는 것이 정답은 아니라는 사실을 깨닫고 있습니다.
중앙 집중식 상태 관리의 함정에서 벗어나기
우리는 관습적으로 프로젝트가 복잡해지면 Redux, Recoil, 혹은 최신의 Zustand와 같은 도구부터 꺼내 들곤 합니다. 하지만 전역 상태가 비대해질수록 예기치 못한 사이드 이펙트와 성능 저하, 그리고 무엇보다 컴포넌트 간의 강한 결합(Tight Coupling)이라는 문제에 직면하게 돼요.
특정 UI 요소의 변화가 전혀 상관없는 다른 페이지의 컴포넌트에 영향을 주거나, 작은 수정에도 수많은 파일을 넘나들며 로직을 수정해야 했던 경험이 있으시죠? 이제는 ‘데이터를 어디에 저장할까’라는 고민에서 벗어나, ‘컴포넌트들이 어떻게 유연하게 대화할 수 있을까’에 집중해야 할 때입니다.
이벤트 드리븐(Event-driven) UI 패턴의 등장 배경
최근 프론트엔드 아키텍처의 핵심 화두는 디커플링(Decoupling)입니다. 컴포넌트가 서로의 내부 구현을 몰라도, 필요한 시점에 적절한 메시지를 주고받는 방식이죠. 마치 우리가 현실 세계에서 알림 설정을 해두면 이벤트가 발생했을 때만 반응하는 것과 같습니다.
이벤트 드리븐 패턴은 브라우저의 기본 기능인 CustomEvent나 경량화된 Event Emitter를 활용하여, 컴포넌트가 독립적으로 존재하면서도 필요한 정보를 선언적으로 수신할 수 있게 합니다. 이는 특히 마이크로 프론트엔드(MFE) 환경이나 복잡한 대시보드 UI를 구성할 때 빛을 발합니다.
왜 이벤트 드리븐인가요?
- 컴포넌트 독립성 보장: 특정 컴포넌트가 사라져도 전체 시스템의 이벤트 흐름은 깨지지 않습니다.
- 재사용성 극대화: Props 구조에 얽매이지 않으므로 컴포넌트를 어디든 배치할 수 있습니다.
- 유지보수의 용이성: 데이터의 흐름이 중앙 상태가 아닌 ‘사건(Event)’ 중심으로 정의되어 추적이 직관적입니다.
실전 적용: Pub/Sub 모델을 통한 선언적 통신
이 패턴을 가장 쉽게 적용하는 방법은 발행-구독(Publish-Subscribe) 모델을 구축하는 것입니다. 예를 들어, 사용자가 상품을 장바구니에 담았을 때 상단 네비게이션 바와 우측 퀵 메뉴가 동시에 업데이트되어야 한다고 가정해 볼게요.
기본적인 방식이라면 장바구니 전역 상태를 업데이트하고 두 컴포넌트가 이를 리렌더링하며 반응하겠지만, 이벤트 드리븐 방식은 다음과 같이 동작합니다.
- 발행(Publish): 장바구니 버튼 컴포넌트가
cart:add이벤트를 발생시킵니다. - 구독(Subscribe): 네비게이션과 퀵 메뉴는 마운트 시점에 해당 이벤트를 리스닝합니다.
- 반응: 이벤트가 수신되면 각자 필요한 로직(애니메이션 실행, 로컬 카운트 업데이트 등)을 수행합니다.
이 방식의 묘미는 관심사의 분리에 있습니다. 장바구니 버튼은 누가 내 신호를 듣는지 알 필요가 없고, 구독자들 또한 누가 신호를 보냈는지 상관하지 않습니다. 그저 ‘신호가 왔다’는 사실에만 집중하면 되죠.
디자인 시스템과 이벤트 패턴의 결합
디자인 시스템을 구축할 때도 이 패턴은 강력한 힘을 발휘합니다. 아토믹 디자인 패턴으로 쪼개진 작은 단위의 컴포넌트들이 복잡한 비즈니스 로직을 포함하지 않으면서도, 시스템 전체와 상호작용할 수 있게 만들어주기 때문이에요.
💡 핵심 팁: 이벤트 이름을 설계할 때는
domain:action:status와 같이 네임스페이스를 활용하세요. 예를 들어order:payment:success와 같은 명확한 네이밍은 디버깅 시 이벤트 흐름을 한눈에 파악하게 도와줍니다.
또한, React의 useSyncExternalStore와 같은 훅을 활용하면 이러한 외부 이벤트 시스템을 React의 렌더링 사이클과 안전하게 동기화할 수 있습니다. 이는 프레임워크에 종속되지 않는 비즈니스 로직 레이어를 구축하는 데 큰 도움이 됩니다.
성능 최적화: 이벤트 버스의 오남용 방지
물론 모든 것을 이벤트로 처리하려는 시도는 위험합니다. 이벤트가 너무 많아지면 소위 ‘이벤트 지옥’에 빠져 데이터 흐름을 추적하기 어려워질 수 있거든요. 이를 방지하기 위한 몇 가지 가이드를 제안합니다.
- 생명주기 관리: 컴포넌트가 언마운트될 때 반드시 리스너를 제거(
removeEventListener)해야 메모리 누수를 방지할 수 있습니다. - 데이터 페이로드 최소화: 이벤트 객체에는 가급적 최소한의 식별자(ID 등)만 담고, 상세 데이터는 필요한 곳에서 직접 가져오도록 설계하세요.
- 단방향성 유지: 이벤트가 또 다른 이벤트를 무한히 발생시키는 순환 참조 구조를 경계해야 합니다.
요약 및 결론
프론트엔드 개발의 본질은 결국 복잡도를 어떻게 관리하느냐에 달려 있습니다. 전역 상태 관리가 주는 편리함도 크지만, 때로는 컴포넌트 간의 느슨한 결합을 지원하는 이벤트 드리븐 UI 패턴이 더 우아한 해결책이 될 수 있습니다.
- Props 드릴링과 비대해진 전역 상태의 대안으로 이벤트 기반 통신을 고려해 보세요.
- CustomEvent나 Pub/Sub 패턴을 활용해 컴포넌트 간의 의존성을 끊어내세요.
- 디자인 시스템과 결합하여 유연하고 확장 가능한 UI 구조를 완성하세요.
단순히 기능을 구현하는 것을 넘어, 미래의 내가 (혹은 동료가) 코드를 읽었을 때 의도가 명확히 전달되는 구조를 만드는 것이 진정한 시니어 개발자의 역량이라고 생각해요. 오늘 알려드린 패턴을 작은 기능부터 하나씩 적용해 보며 우리 코드의 숨통을 틔워주는 건 어떨까요? 😊