버블링(Bubbling)의 원리는 간단합니다.
한 요소에 이벤트가 발생하면, 이 요소에 할당된 핸들러가 동작하고, 이어서 부모 요소의 핸들러가 동작합니다. 가장 최상단의 조상 요소를 만날 떄 까지 이 과정이 반복되면서 요소 각각에 할당된 핸들러가 동작합니다.
<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>요소를 클릭하면 p → div → form 순서로 3개의 Alert 창이 뜨는 것입니다. 이런 흐름을 '이벤트 버블링' 이라고 부릅니다. 이벤트가 제일 깊은 곳에 있는 요소에서 시작해 부모 요소를 거슬러 올라가며 발생하는 모양이 마치 물속 거품과 닮았기 때문입니다.
⚠️ 거의 모든 이벤트는 버블링 됩니다.
focus 이벤트와 같이 버블링 되지 않는 이벤트도 있습니다. 버블링 되지 않는 이벤트의 종류에 대해선 조금 후에 알아보겠습니다. 몇몇 이벤트를 제외하곤 대부분의 이벤트는 버블링 됩니다.
부모 요소의 핸들러는 이벤트가 정확히 어디서 발생했는지 등에 대한 자세한 정보를 얻을 수 있습니다.
이벤트가 발생한 가장 안쪽의 요소는 타깃(target)요소라고 불리고, event.target
을 사용해 접근할 수 있습니다.
*event.target
과 this
(=event.currentTarget)
는 다음과 같은 차이점이 있습니다.*
*event.target
은 실제 이벤트가 시작된 '타깃' 요소입니다. 버블링이 진행되어도 변하지 않습니다.**this
는 '현재' 요소로, 현재 실행 중인 핸들러가 할당된 요소를 참조합니다. 즉 event.currentTarget
은 이벤트 핸들러를 명시적으로 연결하는 요소입니다.*예시를 살펴보면, 핸들러는 form.onclick 하나밖에 없지만 이 핸들러에서 폼 안의 모든 요소에서 발생하는 클릭 이벤트를 '잡아내고(catch)' 있습니다. 클릭 이벤트가 어디서 발생했든 상관없이 <form>
요소까지 이벤트가 버블링 되어 핸들러를 실행시키기 때문입니다.
form.onclick 핸들러 내의 this와 event.target 은 다음과 같습니다.
*this (event.currentTarget)
- <form>
요소에 있는 핸들러가 동작했기 때문에 <form>
요소를 가리킵니다.**event.target
- 폼 안쪽에 실제 클릭한 요소를 가리킵니다.**<form>
요소를 정확히 클릭 시 → target = FORM, this = FORM**<div>
요소를 정확히 클릭 시 → target = DIV, this = FORM**<p>
요소를 정확히 클릭 시 → target = P, this = FORM*이벤트 버블링은 타깃 이벤트에서 시작해서 <html> 요소를 거쳐 document 객체를 만날 때까지 각 노드에서 모두 발생합니다. 몇몇 이벤트는 window 객체까지 거슬러 올라가기도 합니다. 이 떄도 모든 핸들러가 호출됩니다.
그런데 핸들러에게 이벤트를 완전히 처리하고 난 후 버블링을 중단하도록 명령할 수 도 있습니다. 이벤트 객체의 메서드인 event.stopPropagetion() 를 사용하면 됩니다.
아래 예시에서 <button> 을 클릭해도 body.onclick 은 동작하지 않습니다.
<body onclick="alert(`버블링은 여기까지 도달하지 못합니다.`)">
<button onclick="event.stopPropagation()">클릭해 주세요.</button>
</body>
*event.stopPropagation()
한 요소의 특정 이벤트를 처리하는 핸들러가 여러 개인 상황에서, 위쪽으로 일어나는 버블링은 막아주지만, 다른 핸들러들이 동작하는 건 막지 못합니다.*
버블링을 멈추고, 요소에 할당된 다른 핸들러의 동작도 막으려면
*event.stopImmediatePropagation()
을 사용해야 합니다. 이 메서드를 사용하면 요소에 할당된 특정 미벤트를 처리하는 핸들러 모두가 동작하지 않습니다.*
⚠️ 꼭 필요한 경우를 제외하곤 버블링을 막지 마세요!
버블링은 유용합니다. 버블링을 꼭 멈취야 하는 명백한 상항이 아니라면 버블링을 막지 마세요. 아키텍처를 잘 고려해 진짜 막아야 하는 상황에서만 버블링을 막으세요.
event.stopPropagation()은 추후에 문제가 될 수 있는 상황을 만들어낼 수 있습니다.
문제가 발생할만한 시나리오를 살펴봅시다.
이벤트 버블링을 막아야 하는 경우는 거의 없습니다. 버블링을 막아야 해결되는 문제라면 커스텀 이벤트 등을 사용해 문제를 해결할 수 있습니다.
커스텀 이벤트 사용 방법은 추후에 다루며, 핸들어의 event 객체에 데이터를 저장해 다른 핸들러에서 읽을 수 있게 하면, 아래쪽에서 무슨 일이 일어나는지를 부모 요소의 핸들러에게 전달할 수 있으므로, 이 방법으로도 이벤트 버블링을 통제할 수 있습니다.