이벤트 버블링이란, UI상에 중첩된 DOM 요소에 이벤트가 걸려있을 때, 가장 구체적인 부분으로부터 밖으로 이벤트가 퍼져나가는 현상을 의미한다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>이벤트 버블링 테스트</title>
<style>
div {
border: 1px solid black;
padding: 5px;
max-width: 200px;
}
</style>
</head>
<body>
<div
id="hello"
onclick="alert('안녕하세요' + '\n this.id: ' + this.id + '\n event.target.id: ' + event.target.id)"
>
안녕하세요
<div
id="mynameis"
onclick="alert('제 이름은' + '\n this.id: ' + this.id + '\n event.target.id: ' + event.target.id)"
>
제 이름은
<div
id="jake"
onclick="alert('Jake입니다.' + '\n this.id: ' + this.id + '\n event.target.id: ' + event.target.id)"
>
Jake입니다.
</div>
</div>
</div>
</body>
</html>
해당 코드예제는 여기에서 구경해볼 수 있다. 위 코드의 결과는 아래와 같다.
HTML 코드를 살펴보면 알 수 있듯, 3개의 DIV가 중첩된 형태이고, 각 DIV마다 onclick
이벤트가 걸려있다.
가장 안쪽 요소인 Jake입니다.
를 클릭하면, 어떤 일이 일어날까?
순서대로 다음의 메시지가 출력된다.
이게 바로 이벤트 버블링 현상이다.
동작의 핵심을 짚어보자면 위와 같은 DOM tree가 존재할 때, 실제로 클릭한 하위 영역의 객체부터 document
객체를 만날 때까지 각 요소에 할당된 이벤트(onclick
) 핸들러가 동작한다는 것이다.
이렇게 가장 내부적인(구체적인) 요소에서부터 바깥 요소까지 이벤트가 퍼지게 된다. 버블링이라고 불리는 이유는 바닷속에서 우리가 공기를 뱉었을 때를 생각해보면 이해가 쉽다. 바닷속에서 공기를 뱉으면 기포 같은 게 뽀글뽀글 생기면서 수면 위까지 올라오게 된다.
무료 이미지중에 찾다보니 마땅한게 이거밖에 없었다.. 저 거품은 수면 위로 떠오르게 될 것이다.
모든 이벤트가 버블링되진 않는다. focus
처럼 버블링되지 않는 이벤트도 존재한다.
이벤트 버블링 때문에 주로 발생하는 문제는
Element
에 대해서만 어떤 행동을 취하고 싶은데, 실제로 클릭한 요소를 알 수 있는 방법을 모르겠다.Element
에도 이벤트가 걸려버렸다.이를테면 위 그림에서 Jake입니다.
를 눌렀을 때 alert
메시지로 Jake입니다.
만 출력시키고 싶은데, 자꾸 안녕하세요
제 이름은
까지 출력되는 현상이 발생하는 것이다. 물론, div
가 겹치지 않게 배치해서 해결할 수도 있지만, div
의 구조는 그대로 가져가고 싶다고 예를 들어보자.
사용자가 진짜로 클릭한 Element
가 무엇인지 알기 위해서 event.target
을 사용할 수 있다. 그리고 이 event.target
은 this
와 구분될 수 있다.
onclick
에 들어가는 코드를 유심히 살펴보자.
onclick="alert('안녕하세요' + '\n this.id: ' + this.id + '\n event.target.id: ' + event.target.id)"
this.id
와 event.target.id
를 출력하도록 되어있다.
해당 코드예제는 여기에서 구경해볼 수 있다.
Jake입니다.
텍스트가 적혀있는 div
를 클릭하고 결과를 확인해보자.
가장 안쪽에 위치한 div
하나만 클릭했음에도 위에서 배웠던 '버블링' 때문에 3개의 메시지가 차례로 뜬다.
여기서 유심히 봐야 할 것은 event.target.id
는 버블링에 의해 출력되는 메시지에서 한번도 변하지 않았다는 것이다. 그런데 this
는 계속해서 변하고 있다.
event.target
: 사용자가 실제로 클릭한 Elementevent.currentTarget(=this)
: 현재 등록된 이벤트가 트리깅된 Element위와 같이 정리할 수 있다.
버블링은 위와 같은 DOM tree를 타고 올라가는데, HTML
객체를 넘어 document
를 만날 때까지 진행된다. 그런데 버블링이 해당 Element
에서 더이상 버블링되지 않고 멈추게 하고 싶을 때가 있다.
그럴 때 사용하는 것이 stopPropagation()
함수이다.
영어단어 사전의 뜻 그대로 이벤트의 전파를 막는 역할을 한다.
<div
id="jake"
onclick="alert('Jake입니다.' + '\n this.id: ' + this.id + '\n event.target.id: ' + event.target.id); event.stopPropagation();"
>
Jake입니다.
</div>
위처럼 onclick
이벤트의 끝에 event.stopPropagation()
을 붙이면,
클릭 시, 이 메시지 하나만 나오게 된다.
stopImmediatePropagation()
은 여러개의 이벤트가 하나의 Element
에 걸려있을 때에 유용하다. 이를테면 한 Element
에 두개의 onclick
이벤트 리스너를 단 경우를 생각해볼 수 있다.
한 Element
에 여러 개의 onclick()
이벤트 리스너가 달렸다면, 가장 앞에 달린 onclick
이벤트부터 처리하다가, stopImmediatePropagation()
를 만나는 순간 뒤 이후에 있는 onclick
이벤트들은 무시된다.
w3schools 링크에서 체험해볼 수 있다.
그림으로 설명하면 위와 같다.
stopPropagation
: 다른 Element
로의 전파를 막는다.stopImmediatePropagation
: 같은 Element
의 다음 이벤트로의 전파를 막는다.버블링을 막을 때는 반드시 이유가 필요하다.
stopPropagation()
을 의미없이 남발하면, 나중에 해당 이벤트 영역이 '죽은 영역(dead zone)'인지 구분하지 못하고 이벤트가 왜 동작하지 않는지 이해할 수 없는 경우가 생긴다.
어찌됐건간에 이벤트 버블링을 막아야 하는 경우는 거의 드물다. 버블링을 막아야 해결되는 문제는 CustomEvent를 이용해 해결할 수도 있다. 핸들러의 event
객체에 데이터를 저장해 다른 핸들러에서 읽을 수 있게 하면 아래쪽에서 어떤 일이 일어나는지 부모 요소의 핸들러에게 전달할 수 있으므로, 이 방법으로도 이벤트 버블링을 통제할 수 있다.
커스텀 이벤트는 여기에서 공부해볼 수 있다.
사실 위의 그림과 같이 버블링보다 먼저 일어나는 것이 캡처링이다. 자주 쓸 일이 생기진 않지만 가끔 유용할 수 있다.
위는 w3.org에서 정한 표준 DOM 이벤트의 흐름이다. 위 그림은 문서 내부
<td>
요소를 클릭했을 때의 경우이다.
총 3가지 단계인데,
이렇게 3단계가 있다.
우리는 보통 자바스크립트에서 selector
를 이용해 element
를 선택한 뒤에 .addEventListener()
메소드를 통해 이벤트를 등록한다. .addEventListener()
의 두번째 파라미터에서 해당 이벤트를 캡처링 단계에서 동작하게 할 것인지를 정할 수 있다.
element.addEventListener(() => {doSomething;}, true)
혹은 element.addEventListener(() => {doSomething;}, {capture: true})
와 같은 방식으로 해당 이벤트가 캡처링 단계에서 동작하게 만들 수 있다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>이벤트 캡쳐링 + 버블링 테스트</title>
<style>
div,
p,
form {
border: 1px solid black;
padding: 5px;
max-width: 200px;
}
</style>
</head>
<body>
<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>
위 코드는 모든 HTMLElement
에 버블링과 캡처링 이벤트를 건 코드이다.
위 코드의 실행결과는 이 링크에서 체험해볼 수 있다.
실행 결과를 그림으로 표현하면 다음과 같이 실행된다.
컬러풀한 좋은 그림으로 설명하면 아래와 같다.
이 웹사이트에서 캡처링과 버블링을 체험해볼 수 있다.
오잉? 3 단계라고 했는데 '타겟 단계는 어디갔지?' 생각한다면, 타겟 단계는 해당 엘리먼트에 대한 이벤트가 진행되는 시점이다.
event.phase
로 이벤트 단계를 알 수 있다. 이전에 보여준 소스코드 예제에 event.phase
를 넣어보면 단계를 알 수 있는데, 난 p 태그
라고 써져있는 텍스트를 클릭했을 때의 예시를 잠시 살펴보자.
HTML이 캡처링되는 건 당연히 1단계이다.
BODY도 당연히 1단계고, 1단계인 캡처링은 타겟 요소에 도달할 때까지 유지된다.
타겟 요소에 도달하니 캡처링이긴 하지만, 2단계에 도달한 것을 확인할 수 있다.
버블링도 타겟 요소에서는 2단계이며, 타겟을 벗어나면 3단계인 진짜 버블링이 일어난다.