여러분은 여러개의 버튼을 반복문을 통해 만들었고, 각 버튼에 이벤트를 추가해야한다면 어떤 방식을 사용하시나요?
일일히 쿼리를 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>를 클릭하면
- <p>에 할당된 onclick 핸들러 동작
- <div>에 할당된 핸들러 동작
- <form>에 할당된 핸들러 동작
- 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 이벤트에서 정의한 이벤트 흐름 중 하나인데
- 캡처링 단계 – 이벤트가 하위 요소로 전파되는 단계
- 타깃 단계 – 이벤트가 실제 타깃 요소에 전달되는 단계
- 버블링 단계 – 이벤트가 상위 요소로 전파되는 단계
테이블 안의 <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>를 클릭하면 어떻게 동작할까요?
- 캡처링은 HTML → BODY → FORM → DIV
- 타깃은 P
- 버블링은 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>
코드는 다음과 같이 동작합니다.
- elem.closest(selector) elem의 상위 요소중 selector와 일치하는 가장 근접한 조상 요소를 반환합니다.
- 이벤트가 <td>안에서 일어나지 않으면 null을 반환해 아무일도 일어나지 않습니다.
- 검증된 <td>를 강조합니다.
🔥 이벤트 위임을 간단하게 정리하면
- 컨테이너에 하나의 핸들러를 할당합니다.
- 핸들러의 event.target 을 이용해 이벤트가 발생한 곳을 찾습니다.
- 원하는 요소에서 이벤트가 발생했는지 확인하고 이벤트를 핸들링합니다.
이벤트 위임의 장점
- 많은 핸들러를 할당하지 않아도 되어 초기화가 단순해지고 메모리가 절약됩니다.
- 요소를 추가하거나 제거할 때 해당 요소에 할당된 핸들러를 추가, 제거하지 않아도 되어 코드가 짧아집니다.
- innerHTML이나 유사 기능 스크립트 요소 덩어리를 더하거나 뺄 수 있어 DOM 수정이 용이해집니다.
이벤트 위임의 단점
- 응답할 필요가 있는 이벤트던 아니던 모든 하위 컨테이너에서 발생하는 이벤트에 응답해야 되기 때문에 CPU 작업 부하가 늘어남 (무시할만한 수준이라 실제로는 잘 고려하지 않음)