본문 바로가기

CS

[JavaScript] 이벤트 흐름 이해하기

이벤트 위임이 크게 중요하다고 생각하지 않았는데, 이번 과제를 진행하면서 하위 요소에 직접 이벤트를 줄 수 없는 상황에서 상위 요소에 이벤트를 위임하는 방식으로 해결하면서 필요성을 느끼게 되었습니다. 그래서 이번 아티클에서는 이벤트 위임이 무엇인지 살펴보고자 합니다. 


 

제가 고민했던 코드를 같이 봅시다 

목표: title class를 가진 td를 눌렀을 때 이벤트 등록하기 

// 처음에 <td> 생성되는 코드
expenseData.forEach((item) => {
    const row = document.createElement("tr");

    row.innerHTML = `
    <td><input type="checkbox" class="row-check" data-id="${item.id}" /></td>
    <td class="title" data-id="${item.id}">${item.title}</td>
    <td class='${item.amount > 0 ? "td-income" : "td-expense"}'>${item.amount > 0 ? `+${item.amount}` : item.amount}</td>
    <td>${item.date}</td>
    <td>${item.category}</td>
    <td>${item.payment}</td>
  `;

    tbody.appendChild(row);
  });

 

처음에는 class나 id를 통해서 querySelector로 불러오면 되겠다~ 생각해서 해봤는데요 

필터링하거나 정렬한 뒤에 이벤트가 없어지더라고요.. 

시점을 정리해보면 처음에 테이블이 렌더링될 때 <td>가 생성되고, 그 시점에 이벤트를 등록합니다. 

근데 필터나 정렬을 하면 renderTable()이 다시 호출되면서 기존 <td>를 지우고 새로운 <td>를 만듭니다. -> 이거다!!!! 여기서 문제가 생긴거죠.. 

그러면 어떡하지.. 변하지 않는 상위요소인 tbody에 이벤트를 등록해주면 됩니다. 

// tbody에 이벤트 등록
tbody.addEventListener("click", (e) => {
  if (e.target.classList.contains("title")) {
    const id = Number(e.target.dataset.id);
    const item = expenseData.find((item) => item.id === id);
    detailData(item);
    detailModal.style.display = "flex";
  }
});

 

그럼 이제 이벤트 흐름에 대해서 알아보겠습니다. 

 

DOM 이벤트 흐름

DOM 이벤트 흐름에는 3단계가 존재합니다. 

1. 캡처링: 이벤트가 하위 요소로 전파

2. 타깃: 이벤트가 실제로 의도한 타깃 요소에 전달

3. 버블링: 이벤트가 상위 요소로 전파 

 

그럼 위의 예시는 이벤트 버블링입니다. tbody의 이벤트가 td에 적용되니 이벤트 캡처링 아니야? 할 수 있지만

실제로 내가 누른 td에서 시작해서 부모인 tbody로 올라가서 이벤트가 발생하는 것이니 이벤트 버블링이라고 할 수 있습니다. 

 

이벤트 속성

// 이벤트가 발생한 가장 구체적인 요소(실제로 클릭한 요소)
Event.target
// 이벤트 핸들러가 동작하는 요소(this, eventListener가 달린 요소) 
Event.currentTarget
// 현재 이벤트 단계 (캡처링 = 1, 타깃 = 2, 버블링 = 3)
Event.eventPhase

 

이벤트 위임

이벤트 핸들러를 각각의 하위 요소에 붙이지 않고, 상위 요소에 붙이는 개발 방식을 의미합니다. (이벤트 버블링을 응용한 것) 

 

이벤트 전파가 있는 이유

논리적으로 생각해보면 자식 요소가 부모 요소 안에 위치하고 있다보니 자식 요소를 클릭했을 때 부모 요소의 이벤트가 실행되는 것은 당연한 일입니다. 성능 측면에서 보면 이벤트 핸들러를 한 번만 붙이면 되므로 메모리의 사용량이 줄어들게 됩니다. 

 

이벤트 메소드 

자식 요소를 클릭했을 때 부모 요소의 이벤트를 발생시키고 싶지 않은 상황에서는 다음과 같은 메소드를 사용하면 됩니다. 

브라우저는 기본적으로 캡처링 - 버블링으로 동작되기 때문에 이벤트 동작 자체를 바꿀 수는 없어서 이벤트 전파를 방지하는 식으로 해결해야 합니다. 

// 이벤트 버블링 실행 X
Event.stopPropagation()
// 해당 이벤트의 기본 동작 실행 X
Event.preventDefault()

 

stopPropagation은 상위요소로 이벤트를 전달하고 싶지 않을 때 사용하고, 

preventDefault는 주로 form 요소에서 submit 시 새로고침하는 기본 동작을 실행하고 싶지 않을 때 사용합니다. 

e.stopImmediatePropagation은 e.stopPropagation과 같이 그 다음 요소로의 전파를 방지하는데요, 

inner1.addEventListener("click", function(e) {
  console.log("첫번째");
  e.stopPropagation(); // 
});

inner1.addEventListener("click", function(e) {
  console.log("두번째"); // 
});

 

이런식으로 같은 요소에 이벤트를 여러 개 등록했을 때 

stopPropagation은 "두번째"가 실행되고 

stopImmediatePropagation은 "두번째"가 실행되지 않는다는 차이가 있습니다. 

그 요소에 등록한 다른 이벤트 리스너의 실행 유무에 차이가 있습니다. 

 

이 방법 외에도 직접 조건 분기를 통해 e.target으로 일일이 지정해 줄 수 있습니다. 

 

이벤트 전파 방지 주의점

서비스에서 사용자의 클릭 위치나 행동 패턴을 분석하기 위해 페이지 전반에 클릭 이벤트를 수집하는 경우가 있습니다.

이때 분석시스템은 보통 documnet.addEventListener('click', ...)과 같이 전역에서 이벤트를 감지합니다.

하지만 특정 영역에서 stopPropagation()으로 이벤트 버블링을 막아버리면, 해당 영역은 분석 시스템이 감지하지 못하는 죽은 영역이 되어 정확한 데이터 수집이 어려워질 수 있습니다. 

생각보다 영향 범위가 크기 때문에 

1. 전역 이벤트 시스템이 깨질 수 있음 

- 로깅 시스템 

- 공통 UI 핸들러 (모달 닫기, 드롭다운 등)

-> 상위에서 이벤트를 듣는 로직이 전부 영향 받음

2. 협업 시 예상 못한 버그 발생

- 다른 팀원이 상위에서 이벤트 처리하고 있을 수도 있음

다음과 같은 주의사항을 고려해야 합니다. 

 

그렇다면 해결방법에는 어떤 게 있을까요? 

1. e.target으로 일일이 이벤트 지정

e.target을 이용하면 이벤트를 막지 않고도 원하는 요소만 필터링할 수 있기 때문에 stopPropagation을 쓸 필요가 없어집니다. 

<div class="card">
  <button>버튼</button>
</div>

document.addEventListener('click', (e) => {
  console.log(e.target);
});

document.addEventListener('click', (e) => {
  if (e.target.closest('.ignore-area')) return;

  console.log('분석 대상 클릭');
});

 

이런 상황에서 e.target은 <button>이고 e.currentTarget은 이벤트를 걸어둔 document입니다. 

이벤트는 버블링되지만 e.target은 절대 바뀌지 않습니다. 

closest():  이벤트가 발생한 요소부터 상위 요소로 탐색하며 특정 조건에 해당하는 가장 가까운 조상을 찾는 메서드 

1. 클릭이 발생하면

2. document까지 올라간 후

3. e.target으로 조건을 체크해서

4. 무시하거나 처리하는 방식으로 진행됩니다. 

 

2. CustomEvent 활용 

stopPropagation 때문에 상위로 이벤트가 올라가지 않을 때 직접 이벤트를 만들어서 전달하는 방식입니다. 

// 하위 요소에서
element.addEventListener('click', (e) => {
  e.stopPropagation();

  const customEvent = new CustomEvent('customClick', {
    detail: { target: e.target },
    bubbles: true
  });

  document.dispatchEvent(customEvent);
});

// 상위(document)에서
document.addEventListener('customClick', (e) => {
  console.log('커스텀 클릭 감지:', e.detail.target);
});

 

하위 요소에서 막아져있는 상황인데요, customEvent를 통해 새로운 이벤트를 생성합니다. 

detail: 데이터를 전달하는 공간

bubbles: true => 버블링 되도록 허용

dispatchEvent를 통해 만든 이벤트가 실행되도록 설정

 

stopPropagation()은 간단한 해결책처럼 보이지만, 전역 이벤트 흐름을 끊어 분석 시스템이나 공통 로직에 영향을 줄 수 있기 때문에 신중하게 사용해야 하며, 필요한 경우 CustomEvent를 통해 이벤트 흐름을 보완할 수 있습니다. 

 


 

참고자료:

https://dev-district.tistory.com/25

 

[Javascript] DOM 이벤트 흐름(버블링, 캡처링, 이벤트 위임)

🫧 이벤트 흐름표준에서 정의한 DOM 이벤트의 흐름에는 3단계가 존재합니다.캡처링(capturing): 이벤트가 하위 요소로 전파되는 단계타깃(target): 이벤트가 실제로 의도한 타깃 요소에 전달되는 단

dev-district.tistory.com

https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-%EB%B2%84%EB%B8%94%EB%A7%81-%EC%BA%A1%EC%B3%90%EB%A7%81

 

🌐 한눈에 이해하는 이벤트 흐름 제어 (버블링 & 캡처링)

HTML 이벤트의 흐름 HTML 문서의 각 엘리먼트들은 아래와 같이 태그 안의 태그가 위치하는 식으로 계층적으로 이루어짐을 볼 수 있다. 이러한 계층적 구조 특징 때문에 만일 HTML 요소에 이벤트가

inpa.tistory.com

https://velog.io/@eunjin/JavaScript-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%BA%A1%EC%B3%90%EB%A7%81-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B2%84%EB%B8%94%EB%A7%81-%EA%B0%9C%EB%85%90-%EB%B0%A9%EC%A7%80%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95

 

[JavaScript] 이벤트 캡쳐링, 이벤트 버블링 개념, 방지하는 방법

HTML 요소에서 이벤트가 발생하면 해당 요소를 포함한 모든 조상 요소에 이벤트를 전달한다. 왜 전달하는지 알아보고 이벤트가 전파되는 과정을 설명한다.

velog.io