Skip to content

04. 이벤트 플로우와 위임 패턴

이벤트는 타깃에서 갑자기 튀어나오는 것이 아니라 DOM 트리를 따라 이동한다. 이 흐름을 이해해야 왜 여기서 실행됐는가왜 막혔는가를 설명할 수 있다.

학습 목표

  1. 캡처, 타깃, 버블 단계의 차이를 설명할 수 있다.
  2. event.target, event.currentTarget, event.eventPhase를 구분할 수 있다.
  3. stopPropagation, stopImmediatePropagation, preventDefault의 역할을 분리해서 이해한다.
  4. closest() 기반 이벤트 위임과 CustomEvent 사용 맥락을 설명할 수 있다.

1. 이벤트는 어떻게 이동할까?

단계로 보기

DOM 이벤트는 내려갔다가 타깃을 찍고 다시 올라간다

1 / 3캡처 단계

이벤트는 Window에서 시작해 Document, Parent를 거쳐 실제 타깃 쪽으로 내려간다.

이 흐름을 세 단계로 나누면 다음과 같다.

  1. 캡처 단계: 위에서 아래로 내려온다
  2. 타깃 단계: 실제 이벤트가 발생한 요소에 도달한다
  3. 버블 단계: 다시 아래에서 위로 올라간다

대부분의 앱 코드는 기본적으로 버블 단계 리스너를 사용한다.

스스로 확인

이벤트 위임에서 부모 리스너가 실제로 클릭된 자식을 찾을 때 어떤 값을 기준으로 삼아야 할까?


2. targetcurrentTarget을 헷갈리지 말자

이벤트 위임을 처음 배울 때 가장 많이 헷갈리는 부분이다.

프로퍼티의미
event.target실제로 이벤트가 시작된 가장 안쪽 요소
event.currentTarget현재 리스너가 붙어 있는 요소

예를 들어 ul에 클릭 리스너를 붙이고 내부 button을 눌렀다면:

  • targetbutton
  • currentTargetul

이 차이를 알아야 부모 하나에서 자식 여러 개의 동작을 분기할 수 있다.


3. 이벤트 위임은 왜 중요한가?

여러 버튼이나 리스트 아이템에 각각 리스너를 붙이는 대신, 공통 부모 하나에 붙이고 target을 해석하는 방식이 이벤트 위임이다.

장점은 다음과 같다.

  • 동적으로 추가된 자식도 별도 등록 없이 처리 가능
  • 리스너 수가 줄어든다
  • 구조가 바뀌어도 부모 중심으로 제어하기 쉽다

현대적인 패턴은 matches()보다 closest()를 자주 쓴다.

js
list.addEventListener('click', (event) => {
  const button = event.target.closest('[data-action]')
  if (!button || !list.contains(button)) return

  const action = button.dataset.action
  if (action === 'delete') removeItem(button)
})

4. 기본 동작 방지와 전파 중단은 다르다

원문도 이 부분을 중요하게 다루는데, 지금도 정확히 구분해야 한다.

메서드막는 것
preventDefault()브라우저 기본 동작
stopPropagation()상위/하위 단계로의 이벤트 전파
stopImmediatePropagation()같은 요소의 다른 리스너 실행까지 포함한 즉시 중단

예를 들면:

  • 링크 이동을 막고 싶다 -> preventDefault()
  • 부모 카드 클릭 핸들러까지 올라가지 않게 하고 싶다 -> stopPropagation()
  • 같은 요소에 붙은 다른 클릭 리스너도 멈추고 싶다 -> stopImmediatePropagation()

이 셋을 한 덩어리로 외우면 디버깅이 꼬인다.


5. 이벤트 위임 실전에서 자주 보는 패턴

메뉴, 리스트, 테이블 액션

  • 버튼마다 리스너를 붙이지 않고 부모에 하나만 등록
  • data-actiondataset으로 분기

토글 UI

  • 카드 전체는 열고 닫기
  • 내부 닫기 버튼은 상위 클릭까지 전파되지 않게 제어

동적 렌더링 목록

  • 서버 응답 후 아이템이 계속 늘어남
  • 새 노드마다 리스너 등록하는 비용을 줄임

6. 캡처 단계는 언제 쓸까?

평소에는 버블 단계로 충분하지만, 아래 경우는 캡처를 고려할 수 있다.

  • 상위 컨테이너가 먼저 로깅이나 추적을 해야 할 때
  • 특정 이벤트를 아래 요소보다 먼저 가로채야 할 때
  • Shadow DOM 경계나 라이브러리 내부 리스너보다 먼저 반응해야 할 때

다만 무조건 캡처를 쓰면 디버깅 난이도만 올라갈 수 있으니, 이유가 분명한 경우에만 쓰는 편이 좋다.


7. CustomEventdispatchEvent()

DOM 이벤트 모델은 브라우저 기본 이벤트만 위한 것이 아니다.
컴포넌트끼리 의미 있는 사건을 전달할 때도 쓸 수 있다.

js
card.dispatchEvent(new CustomEvent('item:remove', {
  bubbles: true,
  detail: { id: 42 },
}))

이 패턴이 유용한 상황:

  • 커스텀 엘리먼트가 외부에 상태 변화를 알릴 때
  • 특정 UI 단위가 "무슨 일이 일어났는지"를 의미 중심으로 알릴 때

이때 detail에는 필요한 최소 데이터만 넣는 편이 좋다.


8. Shadow DOM과 이벤트 경계

Web Components 장에서 더 자세히 다루지만, 여기서 핵심만 먼저 잡자.

  • 어떤 이벤트는 Shadow DOM 경계를 넘어 전파된다
  • 이때 event.composedevent.composedPath()가 중요하다
  • click 같은 이벤트는 경계를 넘어오는 경우가 많다

즉, 이벤트 위임을 Shadow DOM 바깥에서 할 때는 단순한 target만으로 충분하지 않을 수 있다.


9. 흔한 안티패턴

안티패턴문제점더 나은 방식
자식마다 리스너 남발동적 UI에서 추적이 어려움부모 하나 + 위임
preventDefault()와 전파 중단을 혼용버그 원인 파악이 어려움목적별로 분리
event.target만 믿고 로직 실행내부 아이콘 클릭 같은 경우 오작동closest()로 의도한 요소를 찾기
불필요한 캡처 사용디버깅 난이도 상승기본은 버블, 이유 있을 때만 캡처

10. PR 리뷰 체크리스트

  • 이벤트가 어떤 단계에서 실행되는지 설명 가능한가
  • targetcurrentTarget을 혼동하고 있지 않은가
  • 전파 중단과 기본 동작 방지가 목적에 맞게 쓰였는가
  • 여러 자식 요소를 처리할 때 이벤트 위임으로 단순화할 수 있는가
  • 커스텀 이벤트 이름과 detail 구조가 의미 중심으로 설계되었는가

핵심 정리

  • 이벤트는 캡처, 타깃, 버블 흐름을 따라 움직인다
  • 이벤트 위임은 동적 UI를 단순하게 관리하는 핵심 패턴이다
  • preventDefault()와 전파 중단은 서로 다른 도구이며, Shadow DOM까지 가면 composedPath() 이해가 추가로 필요하다