ℹ️ 이 글을 읽으면 알 수 있는 것
1. 브라우저 이벤트와 이벤트 할당 방법
2. 이벤트 할당 방법의 한계와 이벤트 위임의 필요성
3. 브라우저 이벤트 흐름과 이벤트 위임 방법
4. 이벤트가 캡쳐링 단계가 아닌 버블링 단계에서 실행되는 이유
사전적으로 이벤트(event)란 사건, 일어난 일을 의미합니다.
브라우저에서 이벤트란 브라우저와 관련된 사건이 발생한 순간을 의미합니다. 대표적인 브라우저 이벤트로는 클릭, 웹페이지 로드 등이 있으며 클릭 등의 일부 이벤트는 사용자 경험과 깊게 연관되어 있습니다.
웹페이지가 사용자와 상호작용하기 위해서는 클릭 등의 이벤트가 발생했을 시 적절한 반응을 하는 것이 필요합니다. 하지만 우리는 사용자가 정확히 어떤 순간에 클릭할 지 알 수 없습니다. 따라서 이벤트가 발생했는지 지속적으로 감시할 수 있는 역할이 필요합니다.
이러한 감시자 역할을 해주는 것이 바로 브라우저입니다. 브라우저는 웹페이지에서 발생하는 이벤트들을 지속적으로 감시하며, 이벤트가 발생하는 순간 이를 감지하여 웹페이지에게 이벤트가 발생했음을 통지해 줍니다.
브라우저가 통지한 이벤트에 반응하기 위해선 이벤트가 발생했을 때 실행될 함수를 할당해야 합니다. 이러한 함수를 이벤트 핸들러라고 합니다. 이벤트 핸들러는 여러 방법으로 할당할 수 있지만, 이 글에서는 addEventListener
를 이용하여 핸들러를 할당하는 방법에 대해 알아보도록 하겠습니다.
addEventListener
를 이용하여 이벤트 핸들러를 할당하는 문법은 다음과 같습니다.
element.addEventListener(event, handler, [options]);
event
: 반응할 이벤트 종류 (click 등)handler
: 이벤트에 반응할 핸들러options
: once, capture, passive 등의 프로퍼티를 갖는 객체addEventListener
를 이용하여 이벤트 핸들러를 할당하면 다른 방법과 달리 하나의 이벤트에 여러 핸들러를 할당할 수 있다는 장점이 있습니다. 또한 필요에 따라 여러 옵션을 지정할 수 있으며 removeEventListener
를 통해 이벤트 핸들러 할당을 해제할 수도 있습니다.
addEventListener
의 한계는 반응해야 할 이벤트가 무한히 많아질 때 발생합니다.
각 요소에서 발생하는 이벤트에 대응하기 위해 일일히 addEventListener
를 달아주는 것은 매우 번거로운 일입니다. 또한 요소를 새로 생성하고 제거할 때 마다 매번 이벤트를 등록하고 해제해줘야 하며, 매번 이벤트를 등록해줘야 하기 때문에 innerHTML
등을 이용한 유동적인 요소 생성 및 삭제가 불가능해집니다.
결론적으로 이러한 방식의 프로그래밍은 코드를 매우 복잡하게 만들고 자칫하면 필요 이상의 addEventListener
등록으로 인해 메모리 누수를 유발할 수 있습니다.
<body>
<section class="parent">
<button class="child">버튼 1</button>
<button class="child">버튼 2</button>
... // 무수히 많은 버튼들
<button class="child">버튼 3</button>
</section>
<script>
// 한계 1. 모든 자식 요소에 일일히 이벤트를 달아줘야 한다.
const children = document.querySelectorAll(".child");
children.forEach((child) =>
child.addEventListener("click", (event) => {
const child = event.target.closest(".child");
alert(child.innerText);
})
);
// 한계 2. 새로 생성하는 자식 요소에 일일히 이벤트를 달아줘야 한다.
// 한계 3. innerHTML등을 이용한 유동적인 요소 생성 및 삭제가 불가능해진다.
const newChild = document.createElement("button");
newChild.className = "child";
newChild.innerText = "버튼 4";
newChild.addEventListnener("click", (event) => {
alert(event.target.innerText);
});
document.querySelector("section").appendChild(newChild);
</script>
</body>
이러한 addEventListener
의 한계를 극복하기 위한 방법으로 이벤트 위임이 있습니다.
이벤트 위임이란 여러 자식 요소들에서 발생하는 이벤트를 하나의 부모 요소에서 처리하는 프로그래밍 방식을 의미합니다.
이벤트 위임 방식을 이용하면 단 한번의 이벤트 만으로도 모든 자식 요소의 이벤트에 반응할 수 있기에 이벤트 초기화 과정을 단순화할 수 있습니다. 또한 요소를 추가, 제거할 때 마다 해당 요소에 할당된 핸들러에 대해 신경쓸 필요가 없기 때문에 innerHTML
등을 이용하여 DOM 수정이 쉬워집니다.
자바스크립트에서 이벤트 위임은 이벤트 핸들러에 전달되는 event 객체의 target.closest
속성을 이용하여 구현할 수 있습니다. target.closest
속성은 과거 IE에선 지원이 안된다는 단점이 존재했으나, IE의 몰락(?)으로 인해 현재 모든 브라우저에서 사용이 가능합니다.
target.closest
를 이용한 이벤트 위임 예시는 다음과 같습니다.
아래 예시를 통해 상단의 addEventListener
한계 부분에서 작성된 코드와 동일한 기능을 지원하면서도, 코드가 훨씬 간결해진 것을 확인할 수 있습니다.
<body>
<section class="parent">
<button class="child">버튼 1</button>
<button class="child">버튼 1</button>
<!-- 무수히 많은 버튼들... -->
<button class="child">버튼 1</button>
</section>
<script>
// 한번의 이벤트 핸들러 할당만으로 모든 자식 요소 이벤트에 대응 가능
const parent = document.querySelector(".parent");
parent.addEventListener("click", (event) => {
const child = event.target.closest(".child");
alert(child.innerText);
});
// innerHTML 등을 이용하여 손쉽게 DOM 수정 가능
const nextParentHTML = `
${parent.innerHTML}<button class='child'>버튼 3</button>
`;
parent.innerHTML = nextParentInnerHTML;
</script>
</body>
한가지 신기한 점이 있습니다. 사용자가 클릭한 요소는 버튼인데 이상하게 버튼의 부모 요소인 section
에서도 동일한 클릭 이벤트가 발생하고 있습니다. 왜 이러한 현상이 발생하는 걸까요?
브라우저에서 위와 같은 현상이 발생하고, 이러한 현상을 이용해 이벤트 위임이 가능한 이유는 바로 브라우저 이벤트 흐름과 연관이 있습니다. 브라우저 이벤트 흐름에 대해 알아보도록 하겠습니다.
브라우저의 DOM은 계층적 트리 구조로 구성되어 있습니다. 이러한 DOM에 이벤트가 발생할 경우 브라우저는 계층 구조를 바탕으로 해당 이벤트에 대한 연쇄 반응을 일으키는데 이러한 현상을 이벤트 전파라고 합니다.
브라우저에서 이벤트 전파가 발생하는 흐름을 이벤트 흐름이라고 합니다. 이벤트 흐름은 크게 3가지 단계로 나뉩니다.
캡처링 단계는 이벤트가 전파되는 초기 단계로, 이벤트가 최상위(루트) 엘리먼트에서부터 시작하여 이벤트를 발생시킨 타겟 엘리먼트까지 이동하는 과정을 의미합니다. 이때 이벤트는 부모에서 자식 요소로 향하면서 발생한 엘리먼트들을 찾아갑니다. 해당 단계가 완료된 후 타겟 단계가 실행됩니다.
브라우저 이벤트 흐름의 타겟 단계는 이벤트가 실제로 발생한 대상(타겟) 엘리먼트에 도달하는 단계를 의미합니다. 해당 단계가 완료된 후 버블링 단계가 실행됩니다.
버블링 단계는 이벤트가 발생한 엘리먼트에서 시작하여 상위로 올라가며 부모 엘리먼트들을 따라가는 단계입니다. 버블링 단계는 캡처링 단계와 반대 방향으로 전파됩니다. 이러한 버블링은 최상위 엘리먼트까지 도달한 후 종료됩니다. 버블링 단계까지 종료되면 모든 이벤트 흐름이 종료됩니다.
위와 같이 브라우저에서는 이벤트는 총 3가지의 단계를 거쳐 최상위 요소에서 타겟 요소, 그리고 다시 최상위 요소까지 전파되게 됩니다. 참고로 이벤트를 전파를 막는 방법 또한 존재하나 (event.stopPropagation()
등) 이벤트 전파를 막으면 deadzone
이 발생하기 때문에 가능한 이벤트 전파는 막지 않는 것이 권장됩니다.
이벤트 흐름 동작 방식에 따라 모든 자식 요소에서 발생한 이벤트는 부모 요소를 거치게 됩니다. 따라서 addEventListener
를 이용하여 부모 요소에 이벤트를 등록하면, 하위 자식 요소에서 이벤트가 발생하여도 부모 요소에서 해당 이벤트를 캐치하여 반응하게 됩니다. 이러한 원리를 바탕으로 이벤트 위임이 가능해 집니다.
여기서 한가지 유의해야 할 점이 있는데요, addEventListener
로 등록한 이벤트는 버블링 단계에서 동작한다는 것입니다. 만약 캡처링 단계에서 해당 이벤드가 동작하게 하고 싶다면 addEventListener
의 option 매개변수에 {capture : true}
값을 전달해야 합니다. (option 매개 변수에 true
값을 전달해도 동일하게 작동)
이벤트 반응은 버블링 단계에서 발생하도록 기본 설정 되어 있습니다. 실제로 현업에서도 캡처링 단계를 사용하는 경우는 거의 존재하지 않습니다. 그렇다면 왜 이벤트 반응이 캡처링 단계가 아닌 버블링 단계에서 발생하도록 하는 것이 선호되는 것일까요?
그 이유는 다음과 같이 예시를 들 수 있을 것 같습니다.
한 마을에서 사건이 발생하면, 지역 경찰이 경찰청보다 먼저 그 사건에 대해 조사합니다. 이는 지역 경찰이 상위 조직인 경찰청보다 해당 마을에 대해 더 잘 알고 있기 때문입니다. 하지만 지역 경찰에서 해당 사건을 해결할 수 없을 시 상위 조직인 경찰청에게 조사 권한을 넘기게 됩니다.
비슷한 원리로 이벤트가 발생했을 때, 해당 이벤트의 의도와 가장 적합한 핸들러는 해당 이벤트가 발생한 요소에 할당되어 있을 확률이 가장 큽니다. 또한 의도한 핸들러가 이벤트가 발생한 요소에 없다고 해도, 근접한 요소에 핸들러가 있을 확률이 높습니다.
위와 같은 논리로 인해 1. 이벤트 발생 요소의 이벤트를 먼저 실행시키고, 2. 이벤트 발생 요소와 근접한 이벤트부터 순차적으로 실행하기 위해 이벤트 발생 요소에서부터 이벤트를 실행할 필요가 있습니다. 따라서 이벤트 발생 요소에서 부터 시작되는 단계인 버블링 단계에서 이벤트를 실행하는 것입니다.
이번 글을 통해 브라우저 이벤트와 이벤트 반응 방법에 대해 알아보고, 이벤트 반응을 더욱 효율적으로 하기 위한 방법인 이벤트 위임과 이벤트 위임의 핵심 원리인 이벤트 흐름에 대해 알아보았습니다.
이번 글을 작성하면서 이벤트가 캡처링이 아닌 버블링에서 실행되는 이유에 대해 고민해 볼 수 있어 즐거운 경험이었습니다. 여러분들도 해당 글을 통해 도움을 얻으실 수 있길 바랍니다.
긴 글 읽어주셔서 감사합니다 :D
참고 자료