JavaScript 이벤트 전파 (Event Propagation)

JavaScript에서 이벤트가 DOM 트리를 따라 전파되는 방식을 이해하면 복잡한 UI 상호작용을 효과적으로 처리할 수 있습니다. 캡처링, 버블링, 이벤트 위임 패턴을 알아봅니다.

언제 사용하나요?

  • 동적으로 생성되는 요소에 이벤트 처리
  • 부모-자식 요소 간 이벤트 충돌 해결
  • 이벤트 위임으로 성능 최적화
  • 모달, 드롭다운 외부 클릭 감지

이벤트 전파 3단계

1. 캡처링 (Capturing) - 위에서 아래로
   window → document → html → body → ... → target
   
2. 타겟 (Target) - 실제 이벤트 발생 요소
   
3. 버블링 (Bubbling) - 아래에서 위로
   target → ... → body → html → document → window

대부분의 이벤트는 버블링됨 (focus, blur, scroll 제외)

기본 예시

<div id="outer">
    외부
    <div id="inner">
        내부
        <button id="btn">클릭</button>
    </div>
</div>

<script>
// 버블링 (기본)
document.getElementById("outer").addEventListener("click", () => {
    console.log("outer clicked");
});

document.getElementById("inner").addEventListener("click", () => {
    console.log("inner clicked");
});

document.getElementById("btn").addEventListener("click", () => {
    console.log("button clicked");
});

// 버튼 클릭 시 출력:
// button clicked
// inner clicked
// outer clicked
</script>

캡처링 사용

// 세 번째 인자를 true로 설정
document.getElementById("outer").addEventListener("click", () => {
    console.log("outer (capture)");
}, true);  // 캡처링 단계에서 실행

document.getElementById("inner").addEventListener("click", () => {
    console.log("inner (bubble)");
}, false); // 버블링 단계에서 실행 (기본값)

// 버튼 클릭 시:
// outer (capture) - 캡처링
// button clicked - 타겟
// inner (bubble) - 버블링

전파 중단

// stopPropagation() - 전파 중단
document.getElementById("btn").addEventListener("click", (e) => {
    e.stopPropagation();  // 버블링 중단
    console.log("button clicked");
});
// 버튼 클릭 시 "button clicked"만 출력

// stopImmediatePropagation() - 같은 요소의 다른 핸들러도 중단
document.getElementById("btn").addEventListener("click", (e) => {
    e.stopImmediatePropagation();
    console.log("first handler");
});

document.getElementById("btn").addEventListener("click", () => {
    console.log("second handler"); // 실행 안됨
});

이벤트 위임 (Event Delegation)

<ul id="list">
    <li data-id="1">항목 1</li>
    <li data-id="2">항목 2</li>
    <li data-id="3">항목 3</li>
</ul>

<script>
// 나쁜 예: 각 li에 이벤트 등록
document.querySelectorAll("li").forEach(li => {
    li.addEventListener("click", handleClick);
});

// 좋은 예: 부모에 위임
document.getElementById("list").addEventListener("click", (e) => {
    // 클릭된 요소 확인
    if (e.target.tagName === "LI") {
        const id = e.target.dataset.id;
        console.log("선택된 ID:", id);
    }
});

// 장점:
// 1. 이벤트 핸들러 수 감소 (메모리 절약)
// 2. 동적으로 추가된 요소도 자동 처리
// 3. DOM 조작 후 재등록 불필요
</script>

closest()로 이벤트 위임 개선

<div class="card-list">
    <div class="card" data-id="1">
        <h3>제목</h3>
        <p>내용</p>
        <button class="delete-btn">삭제</button>
    </div>
</div>

<script>
document.querySelector(".card-list").addEventListener("click", (e) => {
    // 삭제 버튼 클릭 확인
    const deleteBtn = e.target.closest(".delete-btn");
    if (deleteBtn) {
        const card = deleteBtn.closest(".card");
        const id = card.dataset.id;
        deleteCard(id);
        return;
    }
    
    // 카드 클릭 확인
    const card = e.target.closest(".card");
    if (card) {
        openCard(card.dataset.id);
    }
});
</script>

외부 클릭 감지 (모달 닫기)

<div class="modal-overlay">
    <div class="modal">
        <h2>모달 제목</h2>
        <p>내용</p>
    </div>
</div>

<script>
document.querySelector(".modal-overlay").addEventListener("click", (e) => {
    // 모달 외부(오버레이) 클릭 시 닫기
    if (e.target.classList.contains("modal-overlay")) {
        closeModal();
    }
});

// 또는 모달 내부 클릭 전파 중단
document.querySelector(".modal").addEventListener("click", (e) => {
    e.stopPropagation();
});
</script>

기본 동작 막기

// preventDefault() - 기본 동작 방지
document.querySelector("a").addEventListener("click", (e) => {
    e.preventDefault();  // 링크 이동 방지
    console.log("링크 클릭됨");
});

document.querySelector("form").addEventListener("submit", (e) => {
    e.preventDefault();  // 폼 제출 방지
    // 커스텀 제출 로직
});

이벤트 객체 주요 속성

element.addEventListener("click", (e) => {
    e.target;          // 실제 클릭된 요소
    e.currentTarget;   // 이벤트가 등록된 요소
    e.type;            // 이벤트 타입 ("click")
    e.bubbles;         // 버블링 여부
    e.eventPhase;      // 1:캡처, 2:타겟, 3:버블
});