모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

21강 / 전체 22강

이벤트 위임과 버블링

14분 읽기 조회 12

이벤트 버블링·캡처링 원리, event.target과 event.currentTarget 구분, 이벤트 위임 패턴으로 동적 요소 처리, closest()로 위임 대상 찾기, stopPropagation 올바른 사용법까지 배웁니다.

🎯 학습 목표

이 강을 마치면 다음을 할 수 있습니다.

  • 이벤트 버블링과 캡처링이 무엇인지, DOM 트리에서 어떻게 전파되는지 이해합니다.
  • event.target과 event.currentTarget의 차이를 정확히 구분합니다.
  • 이벤트 위임 패턴으로 동적으로 추가된 요소들을 효율적으로 처리할 수 있습니다.
  • closest()로 이벤트 위임의 대상 요소를 안전하게 탐색할 수 있습니다.

🫧 이벤트 버블링

이 섹션에서는 이벤트가 발생한 요소에서 DOM 트리를 거슬러 올라가며 전파되는 버블링 현상을 살펴보겠습니다.

18강에서 stopPropagation()을 잠깐 언급했습니다. 이것이 왜 필요한지 이해하려면 먼저 이벤트 버블링(Event Bubbling)을 알아야 합니다. 중첩된 요소에서 이벤트가 발생하면, 그 이벤트는 이벤트가 발생한 요소에서 시작해 DOM 트리를 위로 거슬러 올라가며 document까지 전파됩니다. 마치 물속의 거품이 수면을 향해 올라가는 모습과 닮았습니다.

// HTML 구조:
// <div id="outer">
//   <div id="middle">
//     <button id="inner">클릭</button>
//   </div>
// </div>

const outer  = document.querySelector("#outer");
const middle = document.querySelector("#middle");
const inner  = document.querySelector("#inner");

outer.addEventListener("click",  (e) => console.log("outer 이벤트"));
middle.addEventListener("click", (e) => console.log("middle 이벤트"));
inner.addEventListener("click",  (e) => console.log("inner 이벤트"));

// 버튼을 클릭하면 출력 순서:
// "inner 이벤트"
// "middle 이벤트"
// "outer 이벤트"
// 이후 document, window까지 전파

// event.target과 event.currentTarget 차이
outer.addEventListener("click", (e) => {
  console.log(e.target);         // #inner (실제 클릭된 요소)
  console.log(e.currentTarget);  // #outer (리스너가 붙은 요소)
});

// stopPropagation — 버블링 중단
middle.addEventListener("click", (e) => {
  e.stopPropagation();  // outer까지 전파 차단
  console.log("middle에서 전파 차단");
  // outer 이벤트는 실행되지 않음
});

버블링은 대부분의 이벤트에서 발생합니다. 예외는 focus, blur, load, mouseenter/mouseleave 등 몇 가지입니다. focusin/focusout은 버블링되는 대안입니다. 버블링을 이해하면 의도치 않은 이벤트 전파로 발생하는 버그를 찾고 수정할 수 있습니다. stopPropagation()은 필요한 경우에만 사용하세요. 남발하면 이후 코드에서 이벤트를 가로채야 할 때 문제가 됩니다.

📡 캡처링 단계

이 섹션에서는 이벤트가 버블링과 반대 방향으로 전파되는 캡처링 단계를 살펴보겠습니다.

실제 이벤트 전파는 3단계입니다. 캡처링 단계: document → 이벤트 발생 요소로 내려감. 타깃 단계: 실제 이벤트 발생 요소에서 리스너 실행. 버블링 단계: 이벤트 발생 요소 → document로 올라감. 기본적으로 addEventListener는 버블링 단계에서 실행됩니다.

// 캡처링 단계에서 실행하려면 세 번째 인수에 true 또는 { capture: true }
outer.addEventListener("click", (e) => {
  console.log("outer 캡처링");  // 버블링보다 먼저 실행
}, true);

middle.addEventListener("click", (e) => {
  console.log("middle 캡처링");
}, { capture: true });

inner.addEventListener("click", (e) => {
  console.log("inner 타깃");
});

// 버튼 클릭 시 출력 순서:
// "outer 캡처링"   ← 캡처링, 위에서 아래로
// "middle 캡처링"
// "inner 타깃"     ← 타깃
// (버블링 단계 리스너가 있다면 여기서 실행)

// stopPropagation은 캡처링도 막을 수 있음
outer.addEventListener("click", (e) => {
  e.stopPropagation();  // 하위 요소로 이벤트 전파 차단
}, true);
// 결과: outer 캡처링만 실행, 이후 전파 없음

캡처링은 실무에서 자주 사용하지 않습니다. 이벤트 위임 패턴은 대부분 버블링을 활용합니다. 캡처링이 유용한 경우는 하위 요소의 이벤트를 부모에서 먼저 가로채야 할 때입니다. 예를 들어 로딩 중 사용자 입력을 완전히 차단하는 오버레이 구현 시 캡처링이 효과적입니다.

🎯 이벤트 위임 패턴

이 섹션에서는 버블링을 활용해 많은 요소의 이벤트를 부모 하나에서 처리하는 이벤트 위임 패턴을 살펴보겠습니다.

이벤트 위임(Event Delegation)은 이벤트 처리를 자식 요소 각각이 아닌 부모 요소에 위임하는 패턴입니다. 버블링이 있으므로, 자식에서 발생한 이벤트도 부모가 받을 수 있습니다. 이 패턴은 두 가지 강력한 이점이 있습니다. 첫째, 이벤트 리스너 수를 대폭 줄여 메모리를 절약합니다. 둘째, 나중에 동적으로 추가된 자식 요소에도 자동으로 이벤트가 적용됩니다.

// ❌ 비효율적 — 요소마다 리스너 등록
const items = document.querySelectorAll(".item");
items.forEach(item => {
  item.addEventListener("click", () => {
    item.classList.toggle("selected");
  });
});
// 나중에 추가된 .item에는 이벤트 없음!

// ✅ 이벤트 위임 — 부모 하나에만 리스너
const list = document.querySelector(".list");

list.addEventListener("click", (e) => {
  // event.target으로 실제 클릭된 요소 확인
  if (e.target.classList.contains("item")) {
    e.target.classList.toggle("selected");
  }
});
// 나중에 동적으로 추가된 .item도 자동으로 처리됨!

// 실전: 다양한 버튼이 있는 목록
// <ul id="todo-list">
//   <li data-id="1">
//     <span class="text">할 일 1</span>
//     <button class="done-btn">완료</button>
//     <button class="delete-btn">삭제</button>
//   </li>
// </ul>

document.querySelector("#todo-list").addEventListener("click", (e) => {
  const target = e.target;

  if (target.classList.contains("done-btn")) {
    // 완료 버튼 클릭
    const li = target.closest("li");  // 가장 가까운 li 조상
    const id = Number(li.dataset.id);
    toggleTodo(id);
  }

  if (target.classList.contains("delete-btn")) {
    // 삭제 버튼 클릭
    const li = target.closest("li");
    const id = Number(li.dataset.id);
    deleteTodo(id);
    li.remove();
  }
});

이벤트 위임은 현대 웹 개발의 핵심 패턴입니다. e.target으로 실제 클릭된 요소를 확인해 분기하고, closest()로 관련 데이터(ID 등)가 있는 상위 요소를 안전하게 탐색합니다. 특히 20강에서 만든 것처럼 동적으로 추가된 요소에 이벤트를 붙이려면 위임이 필수입니다. 나중에 추가된 자식에도 자동으로 이벤트가 작동하는 것이 가장 큰 장점입니다.

🔍 closest() — 조상 탐색

이 섹션에서는 이벤트 위임에서 필수적인 closest() 메서드의 작동 방식을 살펴보겠습니다.

이벤트 위임을 사용할 때 자주 발생하는 문제가 있습니다. 예를 들어 <button> 안에 <span>이 있을 때, 스팬을 클릭하면 e.target이 <span>이 되어버립니다. closest()로 이 문제를 안전하게 해결합니다.

// HTML:
// <ul id="list">
//   <li class="item" data-id="1">
//     <button class="delete">
//       <span class="icon">X</span>  ← 이걸 클릭하면 e.target이 span
//     </button>
//   </li>
// </ul>

document.querySelector("#list").addEventListener("click", (e) => {
  // e.target이 .icon(span)일 수 있음

  // closest — 자신 또는 가장 가까운 조상 중 선택자에 맞는 요소 반환
  const deleteBtn = e.target.closest(".delete");  // span → button.delete 탐색
  if (deleteBtn) {
    const li = e.target.closest(".item");  // button → li.item 탐색
    const id = Number(li.dataset.id);
    console.log(`${id}번 삭제`);
  }
  // closest가 ul 바깥까지 탐색하지 않도록 범위 제한
  // matches() — 해당 요소가 선택자와 일치하는지 확인
  if (e.target.matches(".delete, .delete *")) {
    // .delete 또는 그 하위 요소 클릭
  }
});

// closest 특성:
const span = document.querySelector(".icon");
console.log(span.closest(".delete"));  // 자기 자신이 아닌 상위 .delete 반환
console.log(span.closest(".item"));    // .item 반환
console.log(span.closest(".nonexistent"));  // null (못 찾으면)
console.log(span.closest("document"));      // null (DOM 트리 범위 내에서만)

closest(css선택자)는 해당 요소에서 시작해 DOM 트리를 위로 올라가며 선택자와 일치하는 첫 번째 요소를 반환합니다. 자기 자신도 검사합니다. 못 찾으면 null을 반환하므로 항상 null 체크를 해야 합니다. matches(css선택자)는 해당 요소가 특정 선택자와 일치하는지 확인하는 메서드입니다. 이벤트 위임에서 대상 요소를 더 정교하게 판별할 때 활용합니다.

⚙️ 종합 예제 — 동적 할 일 목록

이 섹션에서는 20강의 동적 DOM 생성과 21강의 이벤트 위임을 결합해 완전한 할 일 목록을 구현해 보겠습니다.

let todos = [
  { id: 1, text: "자바스크립트 공부", done: false },
  { id: 2, text: "운동하기",        done: true }
];

const list = document.querySelector("#todo-list");
const input = document.querySelector("#todo-input");
const addBtn = document.querySelector("#add-btn");

function createTodoEl(todo) {
  const li = document.createElement("li");
  li.dataset.id = todo.id;
  if (todo.done) li.classList.add("done");

  const span = document.createElement("span");
  span.className = "todo-text";
  span.textContent = todo.text;

  const doneBtn = document.createElement("button");
  doneBtn.className = "done-btn";
  doneBtn.textContent = todo.done ? "취소" : "완료";

  const delBtn = document.createElement("button");
  delBtn.className = "del-btn";
  delBtn.textContent = "삭제";

  li.append(span, doneBtn, delBtn);
  return li;
}

function render() {
  const fragment = document.createDocumentFragment();
  todos.forEach(todo => fragment.append(createTodoEl(todo)));
  list.replaceChildren(fragment);  // 기존 내용 교체
}

// 이벤트 위임 — 부모 하나에서 모두 처리
list.addEventListener("click", (e) => {
  const li = e.target.closest("li");
  if (!li) return;
  const id = Number(li.dataset.id);

  if (e.target.classList.contains("done-btn")) {
    todos = todos.map(t => t.id === id ? { ...t, done: !t.done } : t);
    render();
  }
  if (e.target.classList.contains("del-btn")) {
    todos = todos.filter(t => t.id !== id);
    render();
  }
});

// 추가
addBtn.addEventListener("click", () => {
  const text = input.value.trim();
  if (!text) return;
  const newId = todos.length ? Math.max(...todos.map(t => t.id)) + 1 : 1;
  todos = [...todos, { id: newId, text, done: false }];
  input.value = "";
  render();
});

render();  // 초기 렌더링

📝 핵심 요약

21강에서 배운 내용을 정리해 보겠습니다.

개념설명
이벤트 버블링이벤트가 target → document로 위로 전파
이벤트 캡처링document → target으로 아래로 전파 (기본 비활성)
event.target실제 이벤트 발생 요소
event.currentTargetaddEventListener가 붙은 요소
이벤트 위임부모에 리스너 1개, 자식 이벤트를 target으로 구분
closest(css)자신 포함 가장 가까운 조상 반환 (없으면 null)
stopPropagation버블링/캡처링 전파 중단 — 꼭 필요할 때만

다음 강인 22강 — 타이머에서는 일정 시간이 지난 후 실행(setTimeout)하거나 주기적으로 반복 실행(setInterval)하는 방법을 배웁니다. 디바운스, 스로틀, 애니메이션 등 타이머는 거의 모든 웹 기능에 사용됩니다!

관련 주제

  • 이벤트 버블링
  • 이벤트 캡처링
  • target currentTarget
  • 이벤트 위임
  • closest()
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • 자바스크립트 기초 30강 — 웹을 움직이는 언어
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

📚 시리즈 전체 공유

자바스크립트 기초 30강 — 웹을 움직이는 언어

이 강의가 속한 시리즈는 총 22강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.

댓글

0/1000

불러오는 중...