Arganoのイジェソンです。 今回はJS DOM Eventのbubblingとcapturingについて調べてみました。


bubbling 이란?

어느 한 요소에 이벤트가 발생하면, 이 요소에 할당된 핸들러가 동작하고, 이어서 가장 최상단의 조상의 요소를 만날 때 까지 이 과정이 반복되면서 요소 각각에 할당된 핸들러가 동작하는 것을 bubbling이라고 합니다.

좀 더 짧게 말하면 document 혹은 window 객체를 만날 때까지 상단을 향해서 이벤트가 전파되는 현상을 말합니다.

몇몇 이벤트를 제외한 거의 모든 이벤트는 버블링이 됩니다. (focus 이벤트 등은 전파되지 않는다.) (MDN event reference)

아무것도 설정하지 않은 경우 기본적으로 버블링이 발생합니다.

예시

가장 하위요소인 child div를 클릭하면 child > parent > ancestor > document > window 순으로 이벤트가 전파되어 alert이 실행되는 것을 알수 있습니다.

bubbling.html

    <div id="ancestor">>ancestor
        <div id="parent">>parent
            <div id="child">child</div>
        </div>
    </div>
    <script src="bubbling.js"></script>

bubbling.js

document.querySelectorAll("div").forEach(el => {
    el.addEventListener("click", event => {
        alert(`${el.id}`);
    });
});
document.addEventListener("click", event => {
    alert("document");
});
window.addEventListener("click", event => {
    alert("window");
});

capturing 이란?

capturing이란 bubbling과 반대로 최상단 요소에서 최하위 요소를 만날 때까지 이벤트가 전파되는 방식을 이야기합니다.

하지만 bubbling과는 다르게 아무 설정없이는 capturing이 일어나지 않습니다. 이벤트 핸들러를 등록할 때 useCapture 부분에 설정을 해주어야 할 필요가 있습니다.

element.addEventListener(event, function, useCapture);

예시

가장 하위요소인 child div를 클릭하면 최상위 요소부터 child까지 이벤트가 전파되어 alert이 실행되는 것을 알 수 있습니다. 여기에서 capture 옵션을 true로 설정하지 않은 이벤트핸들러는 버블링으로 동작합니다.

예시에서는 window의 핸들러는 버블링으로 alert을 표시합니다. 기본적으로 capturing이 우선시 되므로 document > ancestor > parent > child > window 순으로 alert이 표시 됩니다.

capturing.html

    <div id="ancestor">>ancestor
        <div id="parent">>parent
            <div id="child">child</div>
        </div>
    </div>
    <script src="capturing.js"></script>

capturing.js

document.querySelectorAll("div").forEach(el => {
    el.addEventListener("click", event => {
        alert(`${el.id}`);
    }, { capture: true });
});
document.addEventListener("click", event => {
    alert("document");
}, { capture: true });
window.addEventListener("click", event => {
    alert("window");
});

bubbling을 제어하는 방법

내가 의도하지 않은 element까지 이벤트가 전파되는것을 막기위해 아래 세가지를 방법을 소개하겠습니다.

  1. event.stopPropagation()
  2. event.stopImmediatePropagation()
  3. customEvent 사용

stopPropagation()

stopPropagation()은 이벤트 객체에 포함되어 있는 메소드로서 말 그대로 이벤트의 전파를 막는 역할을 합니다.
child div의 이벤트 리스너에 사용한다면 parent로 이벤트가 전파되지 않아 child의 alert만 실행되는 것을 볼 수 있습니다.

예시

document.querySelector("#child").addEventListener("click", event => {
    alert("child");
    event.stopPropagation();
});

stopImmediatePropagation()

stopImmediatePropagation()은 여러개의 이벤트 리스너가 하나의 element에 부여되어 있을때 stopImmediatePropagation()을 사용한 뒤의 이벤트 리스너에는 이벤트가 전파되지않습니다.

stopPropagation()과의 다른점은 stopPropagation()의 경우 같은 element의 이벤트 리스너는 모두 실행된다는 점입니다. 예시로써 child div에 click이벤트가 3개가 부여되어 있다고 예를들어 보겠습니다.

예시

이처럼 하나의 element에 3개의 click이벤트가 부여되어 있을때 event.stopImmediatePropagation()을 사용한 다음의 이벤트 리스너에게는 이벤트가 전파되지 않습니다. 이벤트 리스너의 호출 순서는 부착 순서와 동일하므로 alert은 child alert 2까지만 표시됩니다.

const child = document.querySelector("#child");
child.addEventListener("click", event => {
    alert("child alert 1");
});
child.addEventListener("click", event => {
    alert("child alert 2");
    event.stopImmediatePropagation();
});
child.addEventListener("click", event => {
    alert("child alert 3");
});

customEvent 사용

버블링을 막아야 하는 경우는 드물지만 막아야 해결되는 문제는 customEvent를 이용해 해결할 수도 있습니다.

핸들러의 event 객체에 데이터를 저장해 다른 핸들러에서 읽을 수 있게 하면 자식 요소에서 어떤 일이 일어나는지 부모 요소의 핸들러에게 전달할 수 있으므로 이 방법으로도 이벤트 버블링을 제어할 수 있습니다.

예시

직접 타겟 요소에 dispatchEvent를 할수도 있고 클릭한 요소로부터 버블링을 이용하여 타겟 요소까지 이벤트를 전파시킬 수 있다.

  1. 버블링을 이용하여 타겟요소까지 이벤트전파
const child = document.querySelector("#child");
child.addEventListener("click", () => {
    const event = new Event("hello", { bubbles: true });
    child.dispatchEvent(event);
});
document.querySelector("#ancestor").addEventListener("hello", () => {
    alert("hello ancestor");
});
  1. 타겟요소에 직접 dispatchEvent를 사용
const ancestor = document.querySelector(#ancestor);
document.querySelector("click", () => {
    const event = new Event("hello");
    ancestor.dispatchEvent(event);
});
ancestor.addEventListener("hello", () => {
    alert("hello ancestor");
});

Dom에서의 bubbling 과 capturing의 실행 우선순위

W3C에 의하면 dom에서의 event flow는 capturing phase > target phase > bubbling phase로 되어있다고 합니다. (W3C Dom event flow)

직접 실험해보겠습니다.

document.querySelectorAll("div").forEach(el => {
    el.addEventListener("click", () => {
        alert(`bubbling => ${el.id}`);
    });
    el.addEventListener("click", () => {
        alert(`capturing => ${el.id}`);
    }, true);
});

이벤트 핸들러의 부착순서를 bubbling > capturing순으로 부여했는데도 불구하고 W3C의 문서대로 capturing > bubbing 순서로 alert이 실행되었습니다.


이상으로 버블링과 캡쳐링에 대해 알아보았습니다. 읽어 주셔서 감사합니다.