바닐라 자바스크립트로 앱을 만들 때 어떤 요소를 클릭시 상위, 하위 DOM 요소를 변화시켜야 할 때가 있다. 혹은 여러 개의 하위 요소에 핸들러를 등록해야 할 때가 있다. 이런 경우 일일이 각 요소에 이벤트를 걸어주어도 되지만, 상위 DOM에서만 이벤트 핸들러를 등록함으로써 해결할 수 있다. 이를 이벤트 위임이라고 한다.
본문은 이벤트 위임에 대한 설명을 목적으로 하며, 이에 대한 이해를 위해 브라우저에서 DOM 이벤트가 어떻게 발생하고 또 어떤식으로 진행되는지에 대해서도 다루고자 한다.
브라우저는 처리해야 하는 특정 사건이 발생할 시 이를 감지하여 이벤트를 발생시킨다. 만약 애플리케이션이 특정 타입의 이벤트에 대해 반응하여 어떤 일을 하고 싶다면 해당하는 타입의 이벤트가 발생했을 때 호출될 함수를 브라우저에 알려 호출을 위임한다. 이때 이벤트가 발생했을 때 호출될 함수를 이벤트 핸들러라 하고, 이벤트가 발생했을 때 브라우저에게 이벤트 핸들러의 호출을위임하는 것을 이벤트 핸들러 등록이라고 한다.
브라우저는 사용자의 행동을 감지하여 이벤트를 발생시킬 수 있고, 이벤트가 발생하면 특정 함수(이벤트 핸들러)를 호출하도록 브라우저에게 위임할 수 있다.
이처럼 이벤트와 그에 대응하는 함수(이벤트 핸들러)를 통해 사용자와 애플리케이션은 상호작용할 수 있다. 이와 같이 프로그램의 흐름을 이벤트 중심으로 제어하는 프로그래밍 방식을 이벤트 드리븐 프로그래밍이라고 한다.
DOM 트리 상에 존재하는 DOM 요소 노드에서 발생한 이벤트는 이벤트를 통해 전파된다. 이를 이벤트 전파(event propagation)이라 한다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
</body>
</html>
위의 예제는 두 번째 자식 요소인 li 요소를 클릭하면 클릭 이벤트가 발생한다. 이 때 생성된 이벤트 객체는 이벤트를 발생시킨 DOM 요소인 이벤트 타깃을 중심으로 DOM 트리를 통해 전파된다.
이벤트 전파는 이벤트 객체가 전파되는 방향에 따라 3단계로 구분된다.
예시로 아래 코드에서 이벤트가 발생했을 때의 결과를 확인해보자.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
<script>
const $fruits = document.getElementById('fruits');
// #fruits의 하위 요소인 li 요소를 클릭 시
$fruits.addEventListener('click', e => {
console.log(`이벤트 단계: ${e.eventPhase}`); // "이벤트 단계: 3" = 버블링 단계
console.log(`이벤트 타깃: ${e.target}`); // "이벤트 타깃: [object HTMLLIElement]"
console.log(`커런트 타깃: ${e.currentTarget}`); // "커런트 타깃: [object HTMLUListElement]"
});
</script>
</body>
</html>
이처럼 이벤트는 이벤트를 발생시킨 이벤트 타깃은 물론 상위 DOM 요소에서도 캐치할 수 있다.
대부분의 이벤트는 캡쳐링과 버블링을 통해 전파된다. 하지만 아래 이벤트들은 버블링을 통해 전파되지 않는다.
위 이벤트들은 버블링을 통해 이벤트를 전파하는지 여부를 나타내는 event.bubbles의 값이 모두 false다. 따라서 위의 이벤트들은 버블링되지 않으므로, 이벤트 타깃의 상위 요소에서 위 이벤트를 캐치하려면 캡쳐링 단계의 이벤트를 캐치해야 한다. 하지만 이 이벤트들은 다른 이벤트들로 대체할 수 있으므로 캡쳐링 단계에서 이벤트를 캐치해야 할 경우는 거의 없다.
이벤트 위임은 여러 개의 하위 DOM 요소에 각각 이벤트 핸들러를 등록하는 대신 하나의 상위 DOM 요소에 이벤트 핸들러를 등록하는 방법을 말한다. 이벤트는 상위 DOM에서도 캐치할 수 있기 때문에 여러 개의 하위 DOM 요소에 이벤트 핸들러를 등록할 필요가 없다. 또한 동적으로 하위 DOM 요소를 추가하더라도 일일이 추가된 DOM 요소에 이벤트 핸들러를 등록할 필요가 없다.
<!DOCTYPE html>
<html>
<head>
<style>
#fruits {
display: flex;
list-style-type: none;
padding: 0;
}
#fruits li {
width: 100px;
cursor: pointer;
}
#fruits .active {
color: red;
text-decoration: underline;
}
</style>
</head>
</head>
<body>
<ul id="fruits">
<li id="apple" class="active">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
<div>선택된 내비게이션 아이템: <em class="msg">apple</em></div>
<script>
const $fruits = document.getElementById('fruits');
const $msg = document.querySelector('.msg');
// 사용자 클릭에 의해 선택된 내비게이션 아이템(li 요소)에 active 클래스를 추가하고
// 그 외의 모든 내비게이션의 active 클래스를 제거한다.
function activate({target}) {
// 이벤트를 발생시킨 요소(target)가 ul#fruits의 자식요소가 아니라면 무시한다.
if (!target.matches('#fruits > li')) return;
[...$fruits.children].forEach($fruit => {
$fruit.classList.toggle('active', $fruit === target);
$msg.textContent = target.id;
})
}
// 이벤트 위임: 상위 요소(ul#fruits)는 하위 요소의 이벤트를 캐치할 수 있다.
$fruits.onclick = activate;
</script>
</body>
</html>
Element.prototype.matches
인수로 전달된 선택자에 의해 특정 노드를 탐색 가능한지를 확인한다.function activate({target}) { if (!target.matches('#fruits > li')) return; }
일반적으로 이벤트 객체의 target 프로퍼티와 currentTarget 프로퍼티는 동일한 DOM 요소를 가리키지만 이벤트 위임을 통해 상위 DOM 요소에 이벤트를 바인딩할 경우 이벤트 객체의 target 프로퍼티와 currentTarget 프로퍼티가 다른 DOM 요소를 가리킬 수 있다.
위 코드의 경우 $fruits 요소의 하위 요소에서 클릭 이벤트가 발생한 경우 currentTarget 프로퍼티는 $fruits 요소를 가리키나, target 프로퍼티는 이벤트를 발생시킨 하위 요소를 가리킨다.
(docs) 모던 자바스크립트 딥다이브 40강 이벤트