버블링

이벤트가 발생한 타겟 요소에서 시작하여 최상위 요소(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()실행 안 됨실행 안 됨

버블링되지 않는 이벤트

일부 이벤트는 버블링되지 않습니다.

이벤트버블링버블링되는 대안
focusXfocusin
blurXfocusout
mouseenterXmouseover
mouseleaveXmouseout
loadX-
scrollX (대부분)-
// 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 같은 대안을 사용합니다.

CSLOG에서 보기