JavaScript

[JavaScript] 이벤트 버블링/캡쳐링/ 위임

devSoo 2022. 10. 10. 17:07

오늘은 자주 접했지만 자세히 알지 못했던 이벤트 버블링, 캡쳐링, 위임에 대해서 알아보려고 한다. 

 

1. 목표

이벤트 버블링, 캡쳐링, 위임에 대한 개념을 이해한다.
이벤트 위임의 패턴 및 사용하는 이유에 대해 이해한다. 


2. 이벤트 버블링/ 이벤트 캡쳐링

이벤트 위임에 들어가기 앞서 이벤트 버블링과 캡쳐링에 대한 개념을 먼저 알아야 한다.

2-1) 이벤트 버블링 

특정 엘리먼트의 이벤트 발생 시 조상까지 이벤트가 전달되는 현상

<!DOCTYPE html>
<body>
    body
    <div>
        div
        <p>p</p>
    </div>
    <script src="app.js"></script>
</body>
</html>
const $p = document.querySelector('p')
const $div = document.querySelector('div')
const $body = document.querySelector('body')

function Alert(message) {
  return function() {
    alert(message)
  }
}

$p.addEventListener('click', Alert('p tag event'))
$div.addEventListener('click', Alert('div tag event'))
$body.addEventListener('click', Alert('body tag event'))

여기서 p 태그 클릭 시 'p tag event' -> 'div tag event' -> 'body tag event'의 얼러트 창이 띄어지는 것을 볼 수 있다.

즉 타켓 엘리먼트의 이벤트가 그 위의 조상 엘리먼트까지 버블링 되는 것이다. 

 

멈추는 법 ?

버블링은 html의 태그까지 올라가므로, 타켓 엘리먼트의 엘리먼트에 `event.stopPropagation()` 라는 API를 추가해야 한다. 

만약 하나의 이벤트에 여러 개의 핸들러가 있을 때 하나의 핸들러에 stopPropagation를 추가해도 다른 버블링은 계속 발생한다. 

이때는 모든 버블링은 멈추기 위해서는 `event.stopImmediatePropagation()`라는 API를 사용하면 된다.

 

2-2) 이벤트 캡쳐링

그렇다면 캡쳐링은 무엇인가? 

캡쳐링은 버블링과 반대로 조상으로부터 이벤트가 내려오는 현상이다. 

캡쳐링을 작동하기 위해서는 캡쳐링 옵션에 {capture: true} 혹은 true로 캡쳐링 옵션을 추가해야 한다. (디폴트 false)

 

위의 예시에서 아래와 같이 이벤트 리스너를 등록해보자. 

const elements = document.querySelectorAll('*')

for (let elem of elements) {
  elem.addEventListener('click', e => alert(`캡쳐링: ${elem.tagName}`), true)
}

 

<p>태그 클릭 시 html태그 부터 캡쳐링: html -> 캡쳐링: body -> --- -> 캡쳐링: P 까지 내려오는 것을 볼 수 있다. 

 

멈추는 법 ?

캡쳐링을 멈추는 방법은 버블링과 같이 `event.stopPropagation()` 라는 API를 추가하면 된다.

타켓 엘리먼트 기준으로 최상단의 엘리먼트에만 이벤트가 발생하도록 해준다. 

 

2-3) 버블링과 캡쳐링이 동시에?

그렇다면 버블링과 캡쳐링이 함께 일어날 수 있을까? 

이벤트가 특정 엘리먼트에 일어날 경우 해당 이벤트는 아래와 같은 3단계의 과정을 거친다.

1단계 - 캡쳐링 단계: window로 부터 타켓 엘리먼트까지 이벤트가 아래로 전달 
2단계- 타켓 단계: 이벤트가 타겟 엘리먼트에 도달
3단계 - 버블링 단계 : 이벤트가 타켓 엘리먼트로부터 부모 엘리먼트에 전달 

3. 이벤트 위임

이벤트 위임은 위에서 배운 버블링과 캡쳐링을 이용하여 각각의 엘리먼트에 이벤트를 할당하지 않고 공통되는 부모에 이벤트를 할당하여 이벤트를 관리하는 방식이다. 

 

이벤트 위임이 어떻게 쓰이는 지 아래의 두 가지 경우를 살펴보자.

3 -1 ) 여러 개의 자식 엘리먼트 이벤트 관리하기 

<body>
    <div id="Menu">
        <button data-action="save">저장하기</button>
        <button data-action="reset">초기화 하기</button>
        <button data-action="load">불러오기</button>
    </div>
    <script src="app.js"></script>
</body>

위 와 같이 여러개의 자식 엘리먼트에 특정 액션을 취하고 싶다면 각 각의 엘리먼트 마다 이벤트를 등록 해주어야 한다. 

하지만 위와 같은 엘리먼트가 엄청나게 많다면? 관리하기가 굉장히 힘들어 진다.

 

이때 공용 엘리먼트인 Menu에 이벤트를 위임하여 action에 따라 다른 함수가 호출되게 만들 수 있다. 아래를 보자

const $Menu = document.getElementById('Menu')

const ActionFunctions = {
  save: () => alert('저장하기'),
  reset: () => alert('초기화하기'),
  load: () => alert('불러오기'),
}

$Menu.addEventListener('click', e => {
  const action = e.target.dataset.action
  if (action) {
    ActionFunctions[action]()
  }
})

3 - 2 ) 동적 엘리먼트에 대한 이벤트 관리하기 

동적으로 추가되거나 삭제되는 엘리먼트의 경우에도 이벤트 위임이 쓰일 수 있다. 

아래의 예시를 보자 

<body>
    <form>
        <input type="text"/>
        <button type="submit">등록하기</button>
    </form>
    <ul></ul>
    <script src="app.js"></script>
</body>

등록하기 버튼 클릭 시 ul 태그 아래 li 태그가 생기고 그 li를 클릭 시 li에 쓰여진 text를 alert창으로 보여주는 이벤트를 등록하려고 한다.

이때 각 각의 li마다 이벤트를 등록한다면 코드의 효율성도 떨어질 뿐만 아니라 제대로 리스너가 삭제가 안된다면 메모리 누수의 문제까지 발생하게 된다. 

 

이때, 아래와 같이 

const $form = document.querySelector('form')
const $input = document.querySelector('input')
const $ul = document.querySelector('ul')

$form.addEventListener('submit', e => {
  e.preventDefault()
  const li = document.createElement('li')
  li.innerText = $input.value
  $ul.appendChild(li) // li 엘리먼트 추가
  $input.value = ''
})

$ul.addEventListener('click', e => {
  // 이벤트 위임
  alert(e.target.innerText)
})

ul 태그에 이벤트를 위임하여 이벤트를 효율적으로 관리할 수 있게 된다.  

 

결국 이벤트 위임의 이점은 아래와 같이 요약할 수 있다. 

1. 이벤트 핸들러 관리의 효율성
2. 성능 개선 
3. 메모리 누수 가능성 방지 

4. React에서의 이벤트 위임

 그렇다면 이렇게 성능상의 이점이 있는 이벤트 위임을 React에서는 어떻게 처리해야 할까?

그에 대한 답변은 애초에 React에서는 모든 이벤트가 이벤트 위임으로 처리되고 있다고 한다. 

즉 실제로 모든 이벤트 핸들러는 해당 엘리먼트에 붙여지는 것이 아니라 document 레벨에서 모든 이벤트들이 처리되고 있다는 말이다. 

 

따라서 설사 ref를 통해 하나 하나 접근해서 이벤트 위임을 어렵게 구현했다고 하더라도 성능상의 이점을 기대하기 힘들다.


5. 요약

1. 이벤트 전파 과정은 캡쳐링 단계 -> 타켓 단계 -> 버블링 단계를 거쳐 전파된다. 

2. 이벤트 버블링은 특정 엘리먼트에 이벤트 발생시 그 엘리먼트의 조상들에게 이벤트가 전파되는 것을 뜻하며 이벤트 캡쳐링은 그 엘리먼트로 부터 하위요소로 전파되는 방식을 뜻한다. 

3. 이벤트 버블링과 캡쳐링을 중단하는 방법은 event.stopPropagation() API를 사용해서 할 수 있다.

4. 이벤트 위임은 하위 요소마다 이벤트 핸들러를 할당하지 않고 상위 요소에서 하위 요소의 이벤트를 제어하는 방식이다.

5. 이벤트 위임은 이벤트 관리를 효율적으로 도와줘 성능상의 이점(메모리 절약)과 메모리 누수 가능성을 방지한다. 

6. 리액트는 자체적으로 이벤트 위임을 통해 이벤트를 처리하고 있으므로 별도로 할 필요는 없다. 

 

참고자료)

1. [JavaScript] 이벤트 위임