Javascript 이벤트 전파(버블링/캡처링)와 위임

Javascript

2025년 4월 2일

·

9 min read

JavaScript에서 이벤트를 다루다 보면 예상과 다르게 동작하는 경우를 종종 마주하게 됩니다.
특히 중첩된 요소들에서 이벤트가 발생할 때, "왜 클릭하지도 않은 요소의 이벤트까지 실행되지?"라고 의문을 가져본 적이 있을 것입니다.

이러한 현상을 이해하기 위해서는 이벤트 전파(Event Propagation)의 개념을 알아야 합니다. 그리고 이를 제대로 활용하면 이벤트 위임(Event Delegation)이라는 강력한 패턴도 사용할 수 있습니다.

이벤트 전파

먼저 간단한 예시로 현상을 확인해보겠습니다.

<!DOCTYPE html>
<html>
  <head>
    <style>
      .outer {
        width: 300px;
        height: 300px;
        background-color: yellow;
        padding: 50px;
      }
      .middle {
        width: 200px;
        height: 200px;
        background-color: lightblue;
        padding: 50px;
      }
      .inner {
        width: 100px;
        height: 100px;
        background-color: pink;
        cursor: pointer;
      }
    </style>
  </head>
  <body>
    <div class="outer">
      <div class="middle">
        <div class="inner">클릭하세요</div>
      </div>
    </div>
 
    <script>
      document.querySelector(".outer").addEventListener("click", () => {
        console.log("OUTER 클릭됨");
      });
 
      document.querySelector(".middle").addEventListener("click", () => {
        console.log("MIDDLE 클릭됨");
      });
 
      document.querySelector(".inner").addEventListener("click", () => {
        console.log("INNER 클릭됨");
      });
    </script>
  </body>
</html>

위 코드에서 가장 안쪽의 분홍색 박스(inner)를 클릭하면 어떻게 될까요?
콘솔에는 다음과 같이 출력됩니다.

INNER 클릭됨
MIDDLE 클릭됨
OUTER 클릭됨

분명 inner만 클릭했는데 모든 요소의 이벤트가 실행됩니다. 이것이 바로 이벤트 전파 때문에 발생하는 것입니다.

캡처링과 버블링의 3단계

이벤트 전파는 다음 3단계로 이루어집니다.

  1. 캡처링 단계 (Capturing Phase): 이벤트가 상위 요소에서 하위 요소 방향으로 전파되는 단계입니다.
  2. 타겟 단계 (Target Phase): 이벤트가 실제 발생한 요소에 도달하는 단계입니다.
  3. 버블링 단계 (Bubbling Phase): 이벤트가 하위 요소에서 상위 요소 방향으로 전파되는 단계입니다.
캡처링: Document → HTML → Body → outer → middle → inner
타겟:   inner
버블링: inner → middle → outer → Body → HTML → Document

이벤트 전파 제어하기

어쨌든 위의 예시에서 의도한 바로는, 클릭된 부분의 요소만 호출되고 상위 요소의 이벤트 핸들러는 호출되지 않도록 하는 것이 목표입니다.

어떻게 해결할 수 있을까요?

보통 캡처링 단계에서는 뭔가 해야하는 일은 거의 없습니다. 버블링을 신경써서 볼 필요가 있습니다.

일반적인 해결법: stopPropagation()

많은 분들에 처음에는 stopPropagation()을 사용해 이벤트 전파를 막습니다.

document.querySelector(".inner").addEventListener("click", (event) => {
  console.log("INNER 클릭됨");
  event.stopPropagation(); // 버블링 중단
  // 이제 상위 요소들의 이벤트는 실행되지 않음
});

이 방법이 제일 간단하면서도 즉시 효과가 있어서 많이 사용되곤 합니다. 하지만...! 여러 부가적인 문제들을 발생시킬 확률이 있습니다.

  1. 다른 코드와의 충돌
// 어딘가 다른 코드에서 상위 요소의 클릭을 감지해야 할 수도 있음
document.querySelector(".outer").addEventListener("click", () => {
  trackAnalytics("outer-click"); // 이 코드가 실행되지 않음!
});
  1. 라이브러리와의 충돌: 외부 라이브러리가 상위 요소에서 이벤트를 감지해야 하는데 막혀버림
  2. 팀 협업 문제: 다른 팀원이 상위 요소에 이벤트를 추가했는데 작동하지 않아 혼란

위와 같은 문제가 발생할 수 있어, 저는 stopPropagation()의 사용을 최대한 지양하는 편입니다.

이벤트 객체의 속성(currentTarget, target) 활용하기

결론을 먼저 말씀 드리면, 저는 이벤트 객체의 속성인 currentTarget과 target을 활용하여
이벤트 전파의 문제를 해결합니다.

document.querySelector(".outer").addEventListener("click", (event) => {
  // 나에게 직접 발생한 이벤트만 처리
  if (event.target !== event.currentTarget) {
    return; // 자식에서 버블링된 이벤트는 무시하고 조용히 종료
  }
  console.log("outer 영역이 직접 클릭됨");
});
 
document.querySelector(".middle").addEventListener("click", (event) => {
  if (event.target !== event.currentTarget) {
    return;
  }
  console.log("middle 영역이 직접 클릭됨");
});

위의 패턴을 이해하기 위해서는 두 속성에 대해서 알아야겠죠?
이벤트 객체의 두 가지 중요한 속성을 살펴보겠습니다.

document.querySelector(".outer").addEventListener("click", (event) => {
  console.log("currentTarget:", event.currentTarget.className); // outer
  console.log("target:", event.target.className); // inner
});
  • event.currentTarget: 이벤트 리스너가 등록된 요소 (현재 이벤트를 처리하는 요소)
  • event.target: 이벤트가 실제로 발생한 요소 (사용자가 클릭한 요소)

위 두 가지 속성을 활용하면, "내가 직접 클릭한 요소인지"를 판단할 수 있습니다.

inner를 클릭했을 때:

  • outer의 이벤트에서 currentTarget은 outer, target은 inner->다름->return으로 무시
  • inner의 이벤트에서 currentTarget은 inner, target은 inner->같은->정상 처리

위 방법을 이용하면, 위에서 stopPropagation()을 사용했을 때 발생하는 문제도 발생시키지 않으면서 안전하게 이벤트 전파를 처리할 수 있습니다.

이벤트 위임

이벤트 전파의 특성을 활용하면 이벤트 위임(Event Delegation)이라는 강력한 패턴을 사용할 수 있습니다.

이벤트 위임이란 부모 요소에서 자식 요소의 이벤트를 감지하는 기법입니다.

앞서 설명한 버블링의 내용을 기반으로 생각하면, 부모 요소는 어떤 자식 요소에서 이벤트가 발생하든, 모든 이벤트를 감지할 수 있다는 말이기도 합니다.

이벤트 위임은 "여러 요소의 이벤트를 하나의 부모 요소에서 관리하고 싶을 때". 이런 상황에서 주로 활용합니다.

예를 들어 다음과 같은 할 일 목록이 있다고 가정해봅시다.

<ul id="todo-list">
  <li><button class="delete">삭제</button> 할 일 1</li>
  <li><button class="delete">삭제</button> 할 일 2</li>
  <li><button class="delete">삭제</button> 할 일 3</li>
  <!-- 동적으로 더 많은 항목이 추가될 수 있음 -->
</ul>

일반적으로 각 아이템에 대해 이벤트를 처리한다고 한다면, 다음과 같은 방법을 먼저 떠올릴 수 있을 것입니다.

// 모든 삭제 버튼에 개별적으로 이벤트 리스너 추가
document.querySelectorAll(".delete").forEach((button) => {
  button.addEventListener("click", function () {
    this.parentElement.remove();
  });
});

하지만 위 방법은 비효율적입니다. 문제점은 다음과 같습니다.

  1. 새로 추가되는 버튼에는 이벤트가 없음
  2. 버튼이 많을수록 많은 메모리 사용량
  3. 코드 관리 복잡

위의 문제를 이벤트 위임을 활용하여 해결할 수 있습니다.

// 부모 요소 하나에만 이벤트 리스너 등록
document
  .getElementById("todo-list")
  .addEventListener("click", function (event) {
    // 클릭된 요소가 삭제 버튼인지 확인
    if (event.target.classList.contains("delete")) {
      event.target.parentElement.remove();
    }
  });
 
// 새로운 할 일 추가 (이벤트 리스너 등록 불필요!)
function addTodoItem(text) {
  const todoList = document.getElementById("todo-list");
  const li = document.createElement("li");
  li.innerHTML = `<button class="delete">삭제</button> ${text}`;
  todoList.appendChild(li);
}
  • Javascript
  • Event
  • Propagation
  • Bubbling
  • Capturing
  • Delegation