이벤트를 간편하게 걸고 싶은 당신을 위한 ”이벤트 버블링,캡처링, 위임”

여러분은 여러개의 버튼을 반복문을 통해 만들었고, 각 버튼에 이벤트를 추가해야한다면 어떤 방식을 사용하시나요?

일일히 쿼리를 querySelector로 가져와 이벤트를 추가하시는 분이 있으시다면 이 글을 본 뒤 한번 적용해 보시면 좋을것 같습니다.

이벤트를 등록하는 법

이벤트에 반응하기 위해선 이벤트가 발생했을 때 실행되는 함수 핸들러(handler)를 할당해야 합니다.

<button>my button</button>

var button = document.querySelector('button');
button.addEventListener('click', (event) => {
	console.log(event);
});

우리는 위의 예시와 같은 코드를 통해 버튼을 만들고, 그 버튼을 클릭했을때 어떤 동작을 할 지 정할 수 있습니다. 이때 브라우저는 어떻게 이벤트 발생을 감지할까요?

브라우저가 이벤트를 감지하는 방식

<div onclick="alert('div에 할당한 핸들러입니다.')" style="background-color: aqua;">
	<em><code>EM</code>을 클릭하면 <code>DIV</code>에 할당한 핸들러가 동작</em>
</div>

아래의 코드를 동작하면 div영역에 할당한 핸들러가 EM 영역을 클릭해도 동작합니다.

어떻게 이런 일이 발생하는걸까요?

브라우저가 이벤트를 감지하는 방식은 2가지가 있습니다.

  • 이벤트 버블링 - Event bubbling
  • 이벤트 캡처링 - Event Capturing

이벤트 버블링이란?

이벤트 버블링은 특정 한 요소에 이벤트가 발생하면, 이 요소에 할당된 핸들러가 동작하고, 이어서 부모 요소의 핸들러가 동작합니다. 가장 최 상단의 조상 요소를 만날 때까지 이 과정이 반복되면서 요소 각각에 할당된 핸들러가 동작합니다. 이 모습이 마치 물속 거품과 닮아 버블링 이라고 합니다.

<style>
    body * {
        width: 300px;
        border: 1px solid blue;
    }
</style>

<form onclick="alert('form')">FORM
    <div onclick="alert('div')">DIV
        <p onclick="alert('p')">P</p>
    </div>
</form>

가장 안쪽의 <P>를 클릭하면

  1. <p>에 할당된 onclick 핸들러 동작
  2. <div>에 할당된 핸들러 동작
  3. <form>에 할당된 핸들러 동작
  4. document 객체를 만날 때 까지, 각 요소에 할당된 onclick 핸들러가 동작

이런 방식 때문에 p→div→form순으로 3개의 alert창이 뜨게 됩니다.

버블링을 중단하는 법

event.stopPropagation()을 이용하면 버블링이 일어나지 않도록 할 수 있습니다.

<div onclick="alert(`버블링은 여기까지 도달하지 못합니다.`)" style="border: 1px solid blue;width: 200px;">
    <button onclick="event.stopPropagation()" style="width: 100px;">클릭하세요.</button>
</div>

버튼을 클릭하면 <body>에 할당된 핸들러는 동작하지 않으며 div 영역을 클릭하면 alert창이 뜨게 됩니다.

🔥 꼭 필요한 경우가 아니라면 추후에 문제가 될 수 있는 상황을 만들어낼 수 있기 때문에,
버블링은 막지 않는것을 추천합니다.


이벤트 캡처링이란?

캡처링은 버블링과는 반대로 이벤트가 하위 요소로 전파되는 단계입니다.

실제 코드에서 잘 쓰이지는 않지만 표준 DOM 이벤트에서 정의한 이벤트 흐름 중 하나인데

  1. 캡처링 단계 – 이벤트가 하위 요소로 전파되는 단계
  2. 타깃 단계 – 이벤트가 실제 타깃 요소에 전달되는 단계
  3. 버블링 단계 – 이벤트가 상위 요소로 전파되는 단계

테이블 안의 <td>를 클릭했을 때 어떻게 이벤트가 동작하는지 아래의 그림을 보며 이해해봅시다.

<td>를 클릭하게 되면 이벤트가 최상위 윈도우(조상)에서 시작해 아래로 전파되고(캡처링) 이벤트가 타깃요소 <td>에 도착해 실행된 후(타깃), 다시 위로 전파됩니다(버블링).

이 과정을 통해 요소에 할당된 이벤트 핸들러가 호출됩니다.

앞에서 사용한 on<event>프로퍼티나 .addEventListener(event,handler)를 이용해 할당된 핸들러는 타깃 - 버블링 단계에서만 동작하기 때문에 캡처링에 대해선 알 수 없습니다.

그렇기 때문에 캡처링을 알기 위해선 .addEventListener의 capture 옵션을 true로 설정해야 합니다.

//둘 다 혼용 가능
elem.addEventListener(..., {capture: true})
elem.addEventListener(..., true)

capture 옵션이 true일 때는 캡처링 단계에서,

false(default)일 때는 버블링 단계에서 동작합니다.

<html>
    <head>
    </head>

    <body>
        <style>
            body * {
                border: 1px solid blue;
                width: 200px;
            }
        </style>

        <form>FORM
            <div>DIV
                <p>P</p>
            </div>
        </form>

        <script>
            for (let elem of document.querySelectorAll('*')) {
                elem.addEventListener("click", e => alert(`캡쳐링: ${elem.tagName}`), true);
                elem.addEventListener("click", e => alert(`버블링: ${elem.tagName}`));
            }
        </script>
    </body>
</html>

만약 <p>를 클릭하면 어떻게 동작할까요?

  1. 캡처링은 HTML → BODY → FORM → DIV
  2. 타깃은 P
  3. 버블링은 DIV → FORM → BODY → HTML

이와 같은 순서로 alert가 발생됩니다.


이벤트 위임

우리는 결국 이벤트 위임을 이해하기 위해 토대가 되는 버블링과 캡처링을 공부했습니다.

이벤트 위임은 비슷한 방식으로 여러 요소를 다뤄야 할 때 사용되는데,

요소마다 핸들러를 할당하지 않고, 요소의 공통 조상에 이벤트 핸들러를 단 하나만 할당해도 여러 요소를 한번에 다룰 수 있는 강력한 이벤트 핸들링 패턴입니다.

우리는 공통 조상에 할당한 핸들러에서 event.target을 이용해 실제 어디서 이벤트가 발생했는지 알 수 있습니다.

아래의 코드는 3개의 열로 이루어진 테이블입니다.

테이블을 클릭하면 클릭한 테이블의 글자색은 빨간색, 나머지는 검정색으로 변하고 다른 테이블을 클릭하면 클릭한 테이블의 글자색이 빨간색으로 되고 나머지는 검정이 됩니다.

<html>
    <head>
    </head>
    <body>
        <table id="table">
            <tr>
                <td class="nw" id="A" style="border: 1px solid blue;">*******<strong>AAA</strong>*******</td>
                <td class="n" id="B" style="border: 1px solid blue;">*******<strong>BBB</strong>*******</td>
                <td class="ne" id="C" style="border: 1px solid blue;">*******<strong>CCC</strong>*******</td>
            </tr>
        </table>
        <script>
            let selectedTd;

            table.onclick = function (event) {
                let td = event.target.closest('td'); // (1)
                if (!td) return; // (2)
                highlight(td); // (3)
            };

            function highlight(td) {
                if (selectedTd) { // 이미 강조되어있는 칸이 있다면 원상태로 바꿔줌
                    selectedTd.style.color = 'black';
                }
                selectedTd = td;
                selectedTd.style.color = 'red'; // 새로운 td를 강조 함
            }
        </script>
    </body>
</html>

코드는 다음과 같이 동작합니다.

  1. elem.closest(selector) elem의 상위 요소중 selector와 일치하는 가장 근접한 조상 요소를 반환합니다.
  2. 이벤트가 <td>안에서 일어나지 않으면 null을 반환해 아무일도 일어나지 않습니다.
  3. 검증된 <td>를 강조합니다.

🔥 이벤트 위임을 간단하게 정리하면

  1. 컨테이너에 하나의 핸들러를 할당합니다.
  2. 핸들러의 event.target 을 이용해 이벤트가 발생한 곳을 찾습니다.
  3. 원하는 요소에서 이벤트가 발생했는지 확인하고 이벤트를 핸들링합니다.

이벤트 위임의 장점

  • 많은 핸들러를 할당하지 않아도 되어 초기화가 단순해지고 메모리가 절약됩니다.
  • 요소를 추가하거나 제거할 때 해당 요소에 할당된 핸들러를 추가, 제거하지 않아도 되어 코드가 짧아집니다.
  • innerHTML이나 유사 기능 스크립트 요소 덩어리를 더하거나 뺄 수 있어 DOM 수정이 용이해집니다.

이벤트 위임의 단점

  • 응답할 필요가 있는 이벤트던 아니던 모든 하위 컨테이너에서 발생하는 이벤트에 응답해야 되기 때문에 CPU 작업 부하가 늘어남 (무시할만한 수준이라 실제로는 잘 고려하지 않음)

'개발 공부 > HTML' 카테고리의 다른 글

디바운스와 스로틀  (0) 2024.08.07