const div = document.querySelector('div')
const p = document.querySelector('P')
const span = document.querySelector('span')
div.addEventListener('click', () => console.log('div click'))
p.addEventListener('click', () => console.log('p click'))
span.addEventListener('click', () => console.log('span click'))
이러한 코드가 있다고 가정했을때, div와 p와 span을 각각 클릭했을때의 결과는 어떨까?
처음 코드만 보고 생각했을때는 아마 결과는 이렇게 될것이라고 생각했다.
- div 클릭 => 콘솔에 div click 출력
- p 클릭 => 콘솔에 p click 출력
- span 클릭 => 콘솔에 span click 출력
하지만 막상 만들어보니 결과는 달랐다.
왜 하위 요소들인 p와 span을 클릭했을때 div에 할당된 이벤트 핸들러가 동작하는걸까?
이벤트의 흐름에 대해서 알아보자.
이벤트의 흐름
표준 DOM 이벤트에서 정의한 이벤트 흐름은 3가지가 있다.
- 캡쳐링 단계 : 이벤트가 하위 요소로 전파되는 단계
- 타깃 단계 : 이벤트가 실제 타깃 요소에 전달되는 단계
- 버블링 단계 : 이벤트가 상위 요소로 전파되는 단계
<td> 요소에 이벤트가 할당되어 있다고 가정하고 위의 이미지를 살펴보자.
<td>가 클릭되면 이벤트가 최상위 조상에서 시작해 아래로 전파된다. (캡쳐링 단계)
그 다음 이벤트가 타깃 요소에 도착해 실행된다. (타깃 단계)
그리고 다시 위로 전파되며 요소에 할당된 이벤트 핸들러가 호출된다. (버블링 단계)
이벤트 버블링(Event Bubbling)
어떤 요소에 이벤트가 발생했을때, 이 요소의 이벤트 핸들러가 동작한 다음 이어서 부모 요소의 핸들러가 동작하는 것이다. 가장 최상단의 조상요소를 만날때까지 이 과정이 반복되고 이때 요소 각각에 할당된 핸들러가 동작하게 된다.
위의 예시에서도 이벤트 버블링이 발생했기 때문에 span을 클릭했을때 상위 요소들의 이벤트가 실행되었던 것이다.
정말로 버블링이 발생하기때문에 저렇게 출력이 되는지 확인하기 위해서 각 이벤트 핸들러에 event.target도 함께 출력하도록 코드를 수정했다. event.target은 이벤트가 발생한 대상 객체를 가리킴으로 어떤 요소에서 이벤트가 발생한 것인지 알 수 있다.
const div = document.querySelector('div')
const p = document.querySelector('P')
const span = document.querySelector('span')
div.addEventListener('click', () =>
console.log(event.target, 'div click')
)
p.addEventListener('click', () => console.log(event.target, 'p click'))
span.addEventListener('click', () =>
console.log(event.target, 'span click')
)
결과를 보면, span의 경우 이벤트가 발생한 주체(event.target)은 span이지만 이벤트 버블링이 발생하게 되고 부모요소의 핸들러들이 동작했음을 알수있다.
버블링을 중단하고 싶을때
그렇다면 버블링을 중단하고 싶을때는 어떻게 해야할까? 그때는 이벤트 객체의 메서드인 event.stopPropagation()을 사용한다. 위의 예시에서 span에 event.stopPropagation()을 추가해보자.
div.addEventListener('click', () => console.log(event.target, 'div click'))
p.addEventListener('click', () => console.log(event.target, 'p click'))
span.addEventListener('click', () => {
event.stopPropagation()
console.log(event.target, 'span click')
})
event.stopPropagation()을 사용하지 않은 p는 이벤트 버블링이 적용되어있지만, 이 메서드를 사용한 span에는 버블링이 중단되어 상위 요소들의 핸들러가 동작하지 않는다.
다만 이 방법은 추천하지 않는데, 추후에 버블링이 필요한 경우가 생기면 문제가 될 수 있기 때문이다.
또한 한 요소의 특정 이벤트를 처리하는 핸들러가 여러개일 경우 event.stopPropagation()은 위쪽으로 일어나는 버블링은 막아주지만 다른 형제 핸들러들이 동작하는 것은 막을 수 없다.
버블링을 멈추고 요소에 할당된 다른 형제 핸들러들의 동작을 막기 위해서는 event.stopImmediatePropagation()을 사용해야한다.
이벤트 캡쳐링 (Event Capturing)
버블링과 반대되는 개념이다. 특정 요소에서 이벤트가 발생했을때 브라우저가 가장 바깥쪽의 조상인 <html>부터 캡쳐링 단계에 대해 등록된 이벤트 핸들러가 있는지를 확인하기 위해 검사하고 있다면 실행한다. 그리고 다음 요소로 이동해 같은 동작을 하고 이것을 실제 이벤트가 발생한 요소에 닿을 때까지 반복한다.
on<event> 프로퍼티나 html 속성, addEventListener(event, handler)를 이용해 할당된 핸들러는 캡쳐링에 대해서 알 수 없다. 캡쳐링을 사용하기 위해서는 addEventListener의 capture 옵션을 true로 설정해야 한다. 기본 옵션은 false이며, false일 경우 핸들러가 버블링 단계에서 동작하고 true일 경우 핸들러가 캡쳐링 단계에서 동작한다.
element.addEventListener(..., {capture: true})
// 또는
element.addEventListener(..., true)
캡쳐링 단계를 이용해야 하는 경우는 흔치 않지만, 알아두면 유용하게 사용할 수 있다고 한다.
이벤트 위임(Event Delegation)
그렇다면 버블링은 언제 유용하게 사용할 수 있을까? 바로 버블링과 캡쳐링을 활용한 이벤트 위임(Event Delegation)을 구현할때이다.
이벤트 위임은 비슷한 방식으로 여러개의 요소를 다뤄야 할때 사용할 수 있는 이벤트 핸들링 패턴이다.
이벤트 위임을 사용하면 요소마다 핸들러를 할당하는 것이 아니라, 공통되는 조상 요소에 이벤트 핸들러를 단 하나만 할당해서 여러개의 자식요소를 한꺼번에 다룰 수 있다.
이벤트 위임은 다음과 같은 알고리즘으로 동작한다.
- 컨테이너에 하나의 핸들러 할당
- 핸들러의 event.target을 사용해 이벤트가 발생한 요소 찾아냄
- 원하는 요소에서 이벤트가 발생한 경우 이벤트를 핸들링
<div>
<ul>
<li class="disabled">item list 1</li>
<li>item list 2</li>
<li class="disabled">item list 3</li>
<li>item list 4</li>
<li>item list 5</li>
<li class="disabled">item list 6</li>
<li>item list 7</li>
<li>item list 8</li>
<li class="disabled">item list 9</li>
<li>item list 10</li>
</ul>
</div>
ul 내의 여러개의 ul에 이벤트를 등록해야 하는 상황을 가정해보자.
li가 클릭이 되면 .selected라는 클래스를 붙여서 변화를 나타내는 이벤트를 등록해야한다고 할때, 모든 li에 이벤트를 등록하는 것은 비효율적인 일이다. 위의 코드처럼 li가 10개인 상황에서도 그렇지만, 만약 자식요소가 더 많거나 구조가 복잡할 경우에는 일일이 이벤트를 등록하는것이 힘들것이다. 이런 상황에서 이벤트 위임을 사용할 수 있다.
모든 이벤트를 잡아내는 핸들러를 ul 요소에 할당하고, 클릭된 li에 disabled 클래스가 없을경우 selected 클래스가 추가되도록한다.
const ul = document.querySelector('ul')
ul.addEventListener('click', () => {
const target = event.target;
if (target.classList.contains('disabled')) return;
target.classList.add('selected');
})
코드를 보면 현재 클릭 이벤트가 발생한 요소(event.target)가 변수 target에 담기고, 만약 target의 클래스에 disabled라는 클래스가 있지 않을때에만 selected라는 클래스를 추가한다는 것을 알 수 있다.
아래 이미지를 보면 요소들을 클릭했을때 disabled 클래스가 없는 li들에만 이벤트 핸들러가 실행됨을 알 수 있다.
*************************************
참고
https://developer.mozilla.org/ko/docs/Learn/JavaScript/Building_blocks/Events
https://ko.javascript.info/bubbling-and-capturing
https://developer.mozilla.org/ko/docs/Web/API/Event/target
https://ko.javascript.info/event-delegation
'study > JavaScript' 카테고리의 다른 글
딥다이브 스터디 : 11장 원시 값과 객체의 비교 (0) | 2024.05.29 |
---|---|
즉시 실행 함수(IIFE) (0) | 2023.03.28 |
[JavaScript] localStorage 사용하기 (0) | 2023.01.24 |
[JavaScript] ISO형식의 날짜 표현 변경하기 (0) | 2023.01.22 |
[JavaScript] class로 상속 구현 (0) | 2023.01.16 |