
HTML에서 부모요소에만 클릭 이벤트를 붙여주었는데, 어떻게 자식요소를 클릭해도 똑같은 이벤트가 발생할까?
부모 요소를 덮고있는 투명한 이벤트 처리용 막이라도 있어서 부모 안이라면 어떤 것을 클릭해도 가능한 것일까?
이벤트 전파에 대해 잘 이해하기 위해서 먼저 이벤트 객체에 대해 간략하게 알아보자.
기본적으로 이벤트 객체는 특정 이벤트가 발생했을 때 브라우저가 직접 생성한다.
이벤트 객체는 이벤트 Type에 따라 수많은 종류가 존재한다.
click이벤트와 관련되어 있다면 MouseEvent 객체 등,,
아래와 같이 이벤트 리스너를 사용할 때 콜백 함수에 첫 번째 인자로 e(꼭 e가 아니어도 됨)를 무의식적으로 넣은 경우가 많았을 것이다.
button.addEventListener("click", (e) => {
});
위 예제에서 e에 해당하는 것이 바로 이벤트 객체이고, 클릭 이벤트가 발생했을 때 브라우저가 MouseEvent 객체를 만들어서 보내주기 때문에 클릭 이벤트에 관련된 여러 값이나 메서드를 쓸 수 있는 것이다.
위 예제에서는 click 이벤트를 사용했지만, 이벤트 타입별로 고유하게 사용할 수 있는 값이나 메서드도 있고 공통적으로 사용할 수 있는 값이나 메서드도 존재한다.
중요한 것은 특정 이벤트가 발생했을 때 이벤트 객체가 만들어 진다는 것이다.
이 사실을 잘 기억하면서 이벤트 전파를 알아보자.
이벤트 전파란
상위 DOM 요소 ↔ 하위 DOM 요소
이렇게 양방으로 이벤트가 전파되는 것이다.
그렇다면 이벤트가 전파된다는 말은 무엇일까?
이벤트 전파라는 것은 특정 이벤트가 발생했을 때 해당 이벤트 객체가 전파되는 것을 의미한다.
그렇다면, 이벤트 객체가 언제 어디서 어떻게 전파된다는 것일까?
단계를 자세히 알아보자.
발생한 1개의 이벤트에 대해서 이벤트 전파는 총 3단계로 나눌 수 있다.
각 단계별로 어떤 차이점이 있는지 알아보자.
window 노드에서 target DOM 요소까지 이벤트 객체가 전파되는 것이다.
이벤트 객체를 브라우저가 만들고, window 노드를 통해 target DOM 요소까지 이벤트 객체가 전달된다.
window 노드는 브라우저가 직접 만드는 것이므로 브라우저가 만든 이벤트 객체를 전달할 수 있는 것이다.
그림으로 알아보면 이해하기가 조금 더 수월하다.

이렇게 브라우저가 이벤트 객체를 만들어 target DOM 요소까지 객체를 전달하는 것이 바로 캡처링 단계이다.
아직까진 target DOM 요소가 무엇인지 감이 잘 안 잡힐 수 있다. 타깃 단계에서 함께 알아보자.
먼저 target DOM 요소란 이벤트를 발생시킨 DOM 요소를 말한다.
위 그림 예시에서는 click이벤트가 발생했다고 가정해보겠다.
사용자는 ul의 두 번째 li요소를 클릭한 상황이다.
그렇다면 이 상황에서는 두 번째 li 요소가 target DOM이 되는 것이다.
그리고, 타깃 단계는 이벤트 객체가 부모로부터 전파되다가 클릭 이벤트를 발생시킨 target DOM에 도착한 단계라고 보면 된다.
버블링 단계는 캡처링 단계와 반대로 target DOM 요소로부터 부모 요소까지 이벤트가 전파되는 단계이다.
버블링 단계까지 포함한 이벤트 전파 단계는 아래 그림과 같다.

지금까지 이벤트 전파 단계를 구체적으로 확인했다.
그런데 여기서 이런 의문이 들 수도 있다.
단계는 알았는데 이걸 실제로 어떻게 확인할 수 있지? 또는 어떻게 써먹을 수 있지?
그래서 이제는 이 추상적인 개념을 실제 코드를 통해 이해해 보도록 하겠다.
먼저 이벤트 전파를 확인해 보기 전에 이벤트 핸들러를 등록하는 3가지 방법에 대해 소개하겠다.
이벤트 핸들러 어트리뷰트 방식
html 태그 내에 on+Type 속성으로 함수 호출을 전달해주는 방식이다.
<button onclick="sayHi('Lee')" > CLick me!</button>
이벤트 핸들러 프로퍼티 방식
js에서 DOM에 접근하여 onclick 프로퍼티를 직접 설정해주는 방식이다.
const $button = document.querySelector('button');
$button.onclick = function () {
console.log('button click');
};
addEventListener 메서드 방식
가장 최근에 추가된 addEventListener를 사용하는 방식이다.
button.addEventListener("click", (e) => {
함수 내용
});
이 글에서는 핸들러를 등록할 때 addEventListener를 사용할 것이다.
이유는 어트리뷰트 방식과 핸들러 프로퍼티 방식으로는 이벤트 캡처링을 확인할 수 없다.
모든 단계를 확인 해보기 위해서는 addEventListener를 사용하는 것이 필수적이다.
그렇다면 addEventListenerd의 사용법부터 다시 확인해보자.
이 메서드를 많이 사용해본 적은 있지만, 파라미터를 3개까지 써본적은 거의 없을 것이다. 하지만, 이 메서드에서는 3번째 파라미터까지 설정할 수 있다.
mdn에 나와있는 사용법이다.
addEventListener(type, listener);
addEventListener(type, listener, options);
addEventListener(type, listener, useCapture);
우리가 주의깊게 봐야할 것은 3번째 인자인 useCapture이다.
true 또는 false를 설정할 수 있다.
해당 인자를 true로 설정하게 되면, 캡처링 단계에서 핸들러 함수가 실행되게 된다.
false로 설정할 시 핸들러 함수는 버블링 단계에서 실행되게 된다.
3번째 인자가 없으면 기본값인 false로 설정되어 버블링 단계에서 함수가 실행된다.
이벤트 전파를 확인하기 위해서는 어떤 작업을 해야할까?
이벤트를 발생시켰을 때 코드가 어떤 순서로 동작하는지 확인해보는 것이 가장 좋은 방법이라고 생각하기 때문에 관련 예제를 통해 알아보겠다.
예제를 볼 땐 예상 결과를 미리 생각해보자.
DOM 트리에서 parent가 child의 부모 요소인 상태이다.
그리고 child 요소를 직접적으로 클릭했을 때 어떻게 출력될지 알아보자.
const parent = document.getElementById('parent');
const child = document.getElementById('child');
parent.addEventListener('click', () => console.log('parent-bubble'));
parent.addEventListener('click', () => console.log('parent-capture'), true);
child.addEventListener('click', () => console.log('child-bubble'));
child.addEventListener('click', () => console.log('child-capture'), true);
결과는 아래와 같다.
개인적으로 이 예제에서 중요하다고 생각되는 부분은
부모, 자식 모두 클릭 이벤트를 붙인 상황에서 자식요소를 클릭하면 부모와 자식 이벤트가 모두 발생한다는 것이다.
글 잘봤습니다.. 이벤트 리스너에서 세 번째 인자가 있었다니..
그런데 마지막 결과의 4번에 parent-capture 가 두 번 들어갔습니다!!