이벤트 위임(event delegation)이란 Event capturing & bubbling 에서 정리했던 이벤트의 단계중 bubbling을 활용하여 구현하는 강력한 이벤트 핸들링 패턴입니다.
MDN - Event delegation
버블링은 또한 이벤트 위임의 이점을 취할 수 있게 합니다 — 이 개념은 만약 다수의 자식 요소 중 하나를 선택했을 때 코드를 실행하기를 원한다면, 모든 자식에 개별적으로 이벤트 리스너를 설정해야만 하는 것 대신 이벤트 리스너를 그들의 부모에 설정하고 그들에게서 일어난 이벤트가 그들의 부모에게까지 올라오게 할 수 있다는 사실에 의존합니다. 기억하세요, 버블링은 이벤트 핸들러에 대해 이벤트가 발생된 요소를 먼저 검사하고서, 요소의 부모 등등으로 올라가는 것을 포함합니다.
MDN에서 나와있듯 비슷한 방식으로 여러 요소의 이벤트를 다뤄야 할 때 사용할 수 있습니다.
이벤트 위임을 사용하면 요소마다 핸들러를 할당하지 않고 공통 부모 혹은 조상에게 핸들러를 하나만 할당하여 여러 요소를 한번에 다룰 수 있습니다.
Bubbling 덕분이죠!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="https://unpkg.com/mvp.css">
<title>Event delegation</title>
</head>
<body>
<main>
<header>
<nav>
<ul>
<li><a href="#">Apple🍎</a></li>
<li><a href="#">Banana🍌</a></li>
<li><a href="#">Orange🍊</a></li>
<li><a href="#">Avocado🥑</a></li>
<li><a href="#">Strawberry🍓</a></li>
<li><a href="#">Kiwi🥝</a></li>
</ul>
</nav>
</header>
</main>
</body>
</html>
여기 과일 리스트가 있습니다.
과일 리스트의 과일을 선택하면 선택한 과일의 이름을 빨간색으로 강조하려면 어떤 방법을 쓸 수 있을까요?
클릭이 될 각 a
태그에 이벤트 리스너를 등록하여 스타일을 변경하면 되겠죠?
...
<style>
.selected {
color: red;
}
</style>
...
<script>
const items = document.querySelectorAll('a')
items.forEach(item => item.addEventListener('click', () => item.classList.add('selected')))
</script>
이렇게 하면 원하는 것을 구현 할 수 있으나, 만약 리스트가 10000개, 100000개라면 어떨까요?
아이템에 이벤트 리스너를 각각 만들어서 등록해야 하므로 굉장히 성능적으로 좋지 않습니다.
이것을 해결하기 위해서는 event delegation을 사용할 수 있습니다.
event bubbling을 사용하여 하나의 리스너로 각 아이템의 이벤트를 다루는 것이죠.
...
<script>
const fruitList = document.querySelector('ul')
fruitList.addEventListener('click', (event) => {
if (event.target !== fruitList) {
event.target.classList.add('selected')
}
})
</script>
이렇게 부모 요소인 ul
에 리스너를 등록하고, 타겟에 스타일을 적용하도록 하면 리스너를 하나만 등록하고도 여러 요소를 다룰 수 있습니다.
...
<nav>
<ul>
<li>
<button>저장하기</button>
</li>
<li>
<button>불러오기</button>
</li>
<li>
<button>삭제하기</button>
</li>
</ul>
</nav>
'저장하기', '불러오기', '삭제하기' 기능을 가진 메뉴를 구현하려고 합니다.
각 버튼의 기능을 가진 메서드(save
, load
, delete
)가 있는 객체는 구현되어 있다고 가정해보겠습니다.
버튼과 메서드를 어떻게 연결할 수 있을까요?
위에서 본 것처럼 버튼에 각각 리스너를 등록하는 것은 그리 좋지 않은 방법입니다.
우아한 방법은 바로 메뉴 전체에 리스너를 등록하고, 각 버튼의 커스텀 속성에 호출할 메서드를 넣는 것입니다.
...
<nav id="menu">
<ul>
<li>
<button data-action="save">저장하기</button>
</li>
<li>
<button data-action="load">불러오기</button>
</li>
<li>
<button data-action="delete">삭제하기</button>
</li>
</ul>
</nav>
...
<script>
class Menu {
constructor(element) {
this._element = element;
element.onclick = this.onClick.bind(this);
// this 바인딩하지 않으면 Menu 객체가 아닌 DOM 객체를 참조하게 되므로
// this[action]을 찾을 수 없음
}
save() {
alert('저장하기');
}
load() {
alert('불러오기');
}
delete() {
alert('삭제하기');
}
onClick(event) {
let action = event.target.dataset.action;
if (action) {
this[action]();
}
};
}
new Menu(menu);
</script>
이러한 방법을 사용하면
button
태그에 원하는 메서드를 써주기만 하면 된다.이벤트 위임은 다음과 같은 알고리즘으로 동작한다.
1. 부모(조상)요소에 리스너, 핸들러를 등록한다.
2. 리스너, 핸들러의 event.target
을 사용해 이벤트가 발생한 요소를 찾는다.
3. 원하는 요소에서 이벤트가 발생했다면 이벤트를 핸들링한다.