DOM 이벤트는 대상 요소에서만 끝나지 않고 트리를 따라 전파된다. 루트에서 대상까지 내려가는 캡처링(Capturing Phase), 대상에서 처리되는 타깃 단계(Target Phase), 대상에서 루트로 올라가는 버블링(Bubbling Phase) 순서다.
addEventListener는 기본으로 버블링 단계에서 실행된다. 세 번째 인자로true나{ capture: true }를 주면 캡처링 단계에서 먼저 실행된다event.stopPropagation()은 전파를 멈추고,event.preventDefault()는 링크 이동·폼 제출 같은 기본 동작만 막는다. 둘은 별개다focus,blur,mouseenter처럼 버블링하지 않는 이벤트도 있다(대신focusin·focusout은 버블링한다)event.target은 실제로 이벤트가 일어난 요소,event.currentTarget은 지금 핸들러가 붙은 요소다
sequenceDiagram participant W as window participant D as document participant P as 부모 participant T as 타깃 요소 Note over W,T: 캡처링 단계 (루트에서 대상으로) W->>D: 전파 D->>P: 전파 P->>T: 전파 Note over T: 타깃 단계 Note over W,T: 버블링 단계 (대상에서 루트로) T->>P: 전파 P->>D: 전파 D->>W: 전파
parent.addEventListener('click', () => log('부모(캡처)'), true)
child.addEventListener('click', () => log('자식'))
parent.addEventListener('click', () => log('부모(버블)'))
// 자식 클릭: 부모(캡처) → 자식 → 부모(버블)이벤트 위임(Event Delegation)
자식마다 핸들러를 붙이지 않고, 부모 하나에 붙여 버블링으로 올라온 이벤트를 target으로 구분하는 기법. 핸들러 수가 줄고, 나중에 추가된 자식도 따로 등록할 필요가 없다.
list.addEventListener('click', e => {
const btn = e.target.closest('button[data-id]')
if (btn) remove(btn.dataset.id)
})React는 이벤트 위임을 기본으로 쓴다. 각 요소가 아니라 루트 컨테이너에 리스너를 달고, 자체 합성 이벤트(Synthetic Event)로 전파를 흉내 낸다. 그래서 포털 안의 이벤트도 DOM 위치가 아니라 React 트리를 따라 버블링한다. 이 구조는 한 곳에서 구독자에게 알리는 옵저버 패턴과도 닮았다.