버블링
이벤트가 발생한 타겟 요소에서 시작하여 최상위 요소(window)까지 위로 올라가는 전파 단계입니다.
이벤트 전파에서의 위치
이벤트 전파 3단계:
① 캡처링 (Capturing) window → 타겟으로 내려감
② 타겟 (Target) 실제 이벤트가 발생한 요소
③ 버블링 (Bubbling) 타겟 → window로 올라감 ← 여기
캡처링 버블링
window ────┼───────────────────────┼────
│ ▲
html ──────┼───────────────────────┼────
│ │
body ──────┼───────────────────────┼────
│ │
div ───────┼───────────────────────┼────
│ │
▼ │
button ─────────── 타겟 ────────────┘
↑
거품처럼 위로 올라감
왜 "버블링"인가
비유: 물속에서 공기방울이 올라오는 것
수면 (window)
▲
│
│
│
│
바닥 (타겟) ← 공기방울 발생
→ 이벤트가 거품(bubble)처럼 위로 올라감
버블링 동작 확인
<div id="grandparent">
<div id="parent">
<button id="child">클릭</button>
</div>
</div>
grandparent.addEventListener("click", () => console.log("grandparent"));
parent.addEventListener("click", () => console.log("parent"));
child.addEventListener("click", () => console.log("child"));
버튼 클릭 시 출력:
child ← 타겟 (이벤트 발생 지점)
parent ← 버블링 (위로 올라감)
grandparent ← 버블링
클릭!
│
▼
button (child) ─── "child" 출력
│
│ 버블링
▼
div (parent) ─── "parent" 출력
│
│ 버블링
▼
div (grandparent) ─── "grandparent" 출력
│
│
▼
body → html → document → window (계속 올라감)
버블링이 기본값인 이유
// addEventListener의 세 번째 인자
element.addEventListener("click", handler); // 버블링 (기본)
element.addEventListener("click", handler, false); // 버블링 (명시)
element.addEventListener("click", handler, true); // 캡처링 (명시 필요)
실무에서 99%는 버블링을 사용
이유:
1. 이벤트 위임 패턴에 적합
2. 직관적 (클릭한 곳 → 부모들)
3. 대부분의 이벤트 처리 로직과 부합
버블링 활용 — 이벤트 위임
버블링의 가장 중요한 실무 활용입니다.
문제: 각 요소에 리스너 등록
<ul id="list">
<li>항목 1</li>
<li>항목 2</li>
<li>항목 3</li>
<!-- 1000개 항목 -->
</ul>
// X 비효율적 — 1000개 리스너
document.querySelectorAll("li").forEach(li => {
li.addEventListener("click", () => {
console.log(li.textContent);
});
});
// 문제:
// 1. 메모리 낭비 (리스너 1000개)
// 2. 동적 추가 항목에 리스너 없음
해결: 버블링으로 이벤트 위임
// O 효율적 — 리스너 1개
list.addEventListener("click", (e) => {
if (e.target.tagName === "LI") {
console.log(e.target.textContent);
}
});
// 장점:
// 1. 리스너 1개로 모든 li 처리
// 2. 동적 추가 항목도 자동 처리
항목 2 클릭:
li (항목 2) ← 클릭 발생 (e.target)
│
│ 버블링
▼
ul (list) ← 리스너가 여기서 잡음 (e.currentTarget)
e.target으로 어떤 li인지 확인
버블링 중단 — stopPropagation()
parent.addEventListener("click", () => {
console.log("parent");
});
child.addEventListener("click", (e) => {
e.stopPropagation(); // 버블링 중단
console.log("child");
});
버튼 클릭 시:
child ← 출력됨
← parent는 출력 안 됨 (버블링 중단)
클릭!
│
▼
button (child) ─── "child" 출력
│
X stopPropagation() ─── 버블링 중단!
│
div (parent) ─── 이벤트 도달 안 함
stopPropagation vs stopImmediatePropagation
child.addEventListener("click", (e) => {
console.log("child 1");
e.stopPropagation(); // 버블링만 중단
});
child.addEventListener("click", () => {
console.log("child 2"); // 실행됨 O
});
parent.addEventListener("click", () => {
console.log("parent"); // 실행 안 됨 X
});
출력: child 1, child 2
child.addEventListener("click", (e) => {
console.log("child 1");
e.stopImmediatePropagation(); // 모든 전파 즉시 중단
});
child.addEventListener("click", () => {
console.log("child 2"); // 실행 안 됨 X
});
parent.addEventListener("click", () => {
console.log("parent"); // 실행 안 됨 X
});
출력: child 1
| 메서드 | 같은 요소의 다른 핸들러 | 부모 요소 |
|---|---|---|
stopPropagation() | 실행됨 | 실행 안 됨 |
stopImmediatePropagation() | 실행 안 됨 | 실행 안 됨 |
버블링되지 않는 이벤트
일부 이벤트는 버블링되지 않습니다.
| 이벤트 | 버블링 | 버블링되는 대안 |
|---|---|---|
focus | X | focusin |
blur | X | focusout |
mouseenter | X | mouseover |
mouseleave | X | mouseout |
load | X | - |
scroll | X (대부분) | - |
// focus는 버블링 안 됨
parent.addEventListener("focus", handler); // 자식 focus 시 안 잡힘
// focusin은 버블링 됨
parent.addEventListener("focusin", handler); // 자식 focus 시 잡힘 O
// 이벤트 위임에서 focus 처리
form.addEventListener("focusin", (e) => {
e.target.classList.add("focused");
});
form.addEventListener("focusout", (e) => {
e.target.classList.remove("focused");
});
실무 활용 예시
드롭다운 외부 클릭 감지
// document까지 버블링되는 것을 활용
document.addEventListener("click", (e) => {
const dropdown = document.getElementById("dropdown");
// 드롭다운 외부 클릭 시 닫기
if (!dropdown.contains(e.target)) {
dropdown.classList.remove("open");
}
});
// 드롭다운 내부 클릭은 닫지 않음
dropdown.addEventListener("click", (e) => {
e.stopPropagation(); // document까지 버블링 방지
});
동적 리스트 처리
const todoList = document.getElementById("todo-list");
todoList.addEventListener("click", (e) => {
// 삭제 버튼
if (e.target.matches(".delete-btn")) {
e.target.closest("li").remove();
}
// 완료 체크박스
if (e.target.matches(".complete-checkbox")) {
e.target.closest("li").classList.toggle("completed");
}
// 항목 자체 클릭
if (e.target.matches("li")) {
openDetail(e.target.dataset.id);
}
});
모달 바깥 클릭
const modal = document.getElementById("modal");
modal.addEventListener("click", (e) => {
// 모달 배경 클릭 시만 닫기 (내부 콘텐츠 클릭은 무시)
if (e.target === e.currentTarget) {
modal.style.display = "none";
}
});
모달 구조:
┌─── modal (배경) ─────────────────┐
│ │
│ ┌─── modal-content ─────────┐ │
│ │ │ │
│ │ 내용 │ │
│ │ │ │
│ └────────────────────────────┘ │
│ │
└───────────────────────────────────┘
배경 클릭: e.target === modal → 닫기
내용 클릭: e.target === modal-content → 버블링으로 올라와도
e.target !== e.currentTarget → 안 닫힘
핵심 정리
버블링은 타겟에서 window까지 이벤트가 거품처럼 위로 올라가는 전파 과정이며, 대부분의 이벤트에서 기본 동작입니다. 가장 중요한 활용은 이벤트 위임으로, 부모에 리스너 하나만 등록하여 모든 자식 이벤트를 효율적으로 처리할 수 있습니다. stopPropagation()으로 버블링을 중단할 수 있고, focus/blur 등 버블링되지 않는 이벤트는 focusin/focusout 같은 대안을 사용합니다.