이벤트 캡처링, 버블링, 위임

2020년 05월 24일

이벤트 전파

사용자 입력에 대한 동작을 위해 이벤트 핸들러를 등록한다. 예를 들어 사용자가 클릭하면 alert를 띄워주고 싶을 때에 그 엘리먼트에 클릭 이벤트 핸들러를 등록하는 식이다. 이 포스팅에서는 이벤트 핸들러가 어떻게 호출되는지를 설명하기보다는 이벤트가 전파되는 과정을 설명한다.

1

예를 들어 form 태그 안에 p 태그가 있었다고 하자. 사용자가 p 태그를 클릭했다. 그러면 p 태그만 클릭했다고 봐야 할까? 아니면 중첩되어 있는 부모 form도 같이 클릭했다고 볼 수 있을까? 더 상위 부모인 body나 html도 클릭했다고 볼 수 있을까? 만약 p 태그와 form 태그 모두에게 각각 다른 클릭 이벤트 핸들러가 달려 있었다면 어떤 핸들러가 먼저 호출되었을까?

이런 질문들에 대한 대답을 이벤트 전파 과정이 설명해줄 수 있다. 말 그대로 이벤트는 전파된다. p 태그를 클릭했을 때 그 클릭 이벤트가 어떤 순서에 따라 전파되는 것이다. 이러한 이벤트 전파 과정은 3단계로 나타낸다.

이벤트 캡처링 -> 타깃 객체의 이벤트 핸들러 호출 -> 이벤트 버블링

편의상 마지막 단계인 버블링부터 보도록 하자.

이벤트 버블링(Event Bubbling)

타깃 객체에 등록된 이벤트 핸들러가 실행된 후에, 대부분의 이벤트 는 DOM 계층 구조를 따라서 타고 올라간다. 이를 버블링이라 한다.

<div id="one" onclick="alert('one')">
  <div id="two" onclick="alert('two')">
    <div id="three" onclick="alert('three')"></div>
  </div>
</div>

id가 three 인 div를 클릭하면, 그 클릭 이벤트는 DOM 구조를 따라서 위로 전파된다. 그러므로 우선 타깃 객체에 등록된 이벤트 핸들러가 실행되고, 그 다음 그의 부모인 two 에 등록된 이벤트 핸들러가 실행된다. 그 다음 two의 부모인 one의 이벤트 핸들러가 실행된다. 이런 식으로 이벤트가 DOM 계층을 따라 올라가며 전파되는 현상을 이벤트 버블링이라 하는 것이다. (즉 three -> two -> one의 순서로 alert한다)

몇몇 이벤트(e.g. focus, blur, scroll)를 제외하고 이벤트는 버블링되는데, 이는 Document 객체를 지나 Window 객체까지 이어진다. (단 load 이벤트의 경우 Window 객체까지 전파되지는 않는다)

이벤트 캡처링(Event Capturing)

이벤트 캡처링은 이벤트 핸들러가 캡처링될 때까지 DOM 계층 구조를 타고 내려가는 것이다. Window 객체의 캡처링 핸들러가 맨 처음 호출되고, 그 다음 Document 객체의 캡처링 핸들러가 호출되며, 그 다음은 body 객체가 호출되는 식이다.

<div id="one">
  <div id="two">
    <div id="three"></div>
  </div>
</div>

<script>
  for (let el of document.querySelectorAll('div')) {
    el.addEventListener('click', e => alert(`Capturing: ${el.id}`), true)
    el.addEventListener('click', e => alert(`Bubbling: ${el.id}`))
  }
</script>

위 코드에서는 addEventListener 로 이벤트가 등록돼있다. 이벤트 캡처링은 addEventListener의 세번째 인자를 true로 설정할 때에만 작동하기 때문이다. false로 설정하거나 인자를 넘겨주지 않으면 이벤트 버블링이 작동한다.

이 상황에서 만약 최하위 객체인 three를 클릭했다고 했을 때, 우선 타깃으로 지정된 three의 이벤트 핸들러를 캡처링할 때까지 계속 DOM구조를 타고 내려가며 핸들러가 호출된다. 따라서 이렇게 alert한다.

Capturing one -> Capturing two -> Capturing three

그 다음은 버블링이 일어나면서 DOM 구조를 타고 올라가며 이벤트 핸들러가 호출되므로 이 순서대로 alert한다.

Bubbling three -> Bubbling two -> Bubbling one

이벤트 위임(Event Delegation)

이벤트 위임은 이벤트 버블링을 이용한 방법이다. ul 태그 안에 아주 많은 li 태그가 있다고 하자. 만약 각 list item을 클릭하면 해당 list item의 id를 alert 해주고 싶다고 하자. 이럴 때에 모든 list item에 일일이 이벤트 리스너를 등록하는 것보다 공통 부모인 ul 태그에 클릭 이벤트 리스너를 달아주면 된다.

자식인 list item에서 클릭 이벤트가 발생했을 때 그 이벤트가 버블링되면서 부모에게까지 전달될 것이므로, 공통 부모에게 이벤트 리스너를 달아놓는 방법으로 이벤트를 위임하는 것이다.

document.querySelector('ul').addEventListener('click', function(e) {
  alert(e.target.id)
})

이벤트 위임은 아래와 같은 장점이 있다

  • 관리해야 하는 이벤트 리스너의 수가 줄어들기 때문에 관리가 용이하다
  • 하위 엘리먼트를 추가하거나 삭제할 때 일일이 이벤트 리스너를 달아주지 않아도 된다
  • 등록되는 이벤트 리스너의 수가 줄어들기 때문에 메모리 사용량이 줄어든다

React 에서는?

내가 주로 사용하고 있는 리액트에서는 어떻게 이벤트 전파를 처리하고 있는지 궁금해졌다. 리액트에서는 보통 JSX 태그에 onClick 을 달아주는 식으로 이벤트 리스너를 등록하는데, 기본적으로 이벤트 버블링이 작동한다고 보면 된다. addEventListener의 세번째 인자로 아무것도 넘겨주지 않는 것과 같다. 만약 캡처링을 하고 싶다면 onClickCapture 라는 속성을 등록한다. 이런 속성이 있는지 처음 알았다…👀 사실 캡처링은 일반적으로 잘 쓰이지는 않고 모든 브라우저가 지원하는 것도 아니다. 캡처링 이벤트 핸들러는 타깃 객체에 이벤트를 전달하기 전에 이벤트를 확인하여 디버깅하거나, 사전에 걸러내 호출되지 않게끔 하는 이벤트 취소에 사용되기도 한다.

Ref