본문 바로가기

Sparta x 이노베이션 캠프/React

TIL : 이벤트 버블링과 캡처링

반응형

이벤트 버블링

 

한 요소에 이벤트가 발생하면, 이 요소에 할당된 핸들러가 동작하고, 이어서 부모 요소의 핸들러가 동작한다. 가장 최상단의 조상 요소를 만날 때 까지 이 과정이 반복되면서 요소 각각에 할당된 핸들러가 동작한다.

 

<style>
	body * {
    	margin: 10px;
        border: 1px solid blue;
    }
</style>

<form onclick="alert(form)"> Form
	<div onclick="alert('div')"> DIV
 		<p onclick="alert('p')">P</p>
	</div>
</form>

위와 같은 구조에서 가장 안쪽의 <p> 를 클릭하면 다음과 같은 일이 벌어진다.

 

1. <p>에 할당된 onclick 핸들러가 동작

2. 바깥의 <div>에 할당된 핸들러가 동작

3. 그 바깥의 <form> 에 할당된 핸들러가 동작

4. document 객체를 만날 때 까지, 각 요소에 할당된 onclick 핸들러가 동작한다.

거의 모든 이벤트는 버블링이 된다. (몇몇 이벤트 (ex. focus)는 버블링이 일어나지 않음)

event.target

부모 요소의 핸들러는 이벤트가 정확히 어디서 발생했는지 등에 대한 자세한 정보를 얻을 수 있다. 
이벤트가 발생한 가장 안쪽의 요소는 타깃(target)요소라고 불리고, event.target 을 사용해 접근한다.

event.target과 this(=event.currentTarget)는 다음과 같은 차이가 있다. 

 

event.target 은 실제 이벤트가 시작된 '타깃' 요소 이다. 버블링이 진행되어도 변하지 않는다.

this는 '현재' 요소로, 현재 실행 중인 핸들러가 할당된 요소를 참조한다.

 

버블링 중단하기

 

이벤트 버블링은 타깃 이벤트에서 시작해 html 요소를 거쳐 document 객체를 만날 때까지 각 노드에서 모두 발생한다. 몇몇 이벤트는 window 객체까지 거슬러 올라가며 이 때도 모든 핸들러가 호출된다.

그런데 event.stopPropagation() 메서드를 사용하면 핸들러에게 이벤트를  완전히 처리하고 난 후 버블링을 중단하도록 명령할 수 있다.

하지만 이 메소드는 위쪽으로 일어나는 버블링은 막아주나, 다른 핸들러들이 동작하는 건 막지 못한다.

 

event.stopImmediatePropagation()을 사용하면 요소에 할당된 특정 이벤트를 처리하는 핸들러 모두가 동작하지 않는다.

 

이벤트 버블링을 막아야 할 경우는 거의 없다. 꼭 필요한 경우를 제외하고는 버블링을 막지 말자! 

event.stopPropagation()은 추후에 문제가 될 수 있는 상황을 만들어 낼 수 있다.

1. 중첩메뉴를 만들었을때 각 서브메뉴에 해당하는 요소에서 클릭 이벤트를 처리하도록 하고, 상위 메뉴의 클릭 이벤트 핸들러는 동작하지 않도록 stopPropagation()을 적용한다. 

2. 사람들이 페이지에서 어디를 클릭했는지 등의 패턴 분석을위해 window 내 발생하는 클릭이벤트를 모두 감지하기로 하고 분석 시스템을 도입한다. 이 분석 시스템 코드는 document.addEventListener('click'...)을 사용한다.

3. stopPropagation로 버블링을 막아놓으면 분석 시스템의 코드가 동작하지 않아 분석이 제대로 되지 않는다.(죽은영역 dead zone)이 되어버린다.

 

 

캡쳐링 

이벤트엔 캡쳐링이라는 흐름 또한 존재한다. 

표준 DOM 이벤트에서 정의한 이벤트 흐름의 3가지 단계

1. 캡쳐링 - 이벤트가 하위요소로 전파되는 단계

2. 타깃 단계 - 이벤트가 실제 타깃요소에 전달되는 단계

3. 버블링 -이벤트가 상위요소로 전파되는 단계

 

캡쳐링 단계에서 이벤트를 잡아내려면 addEventListener의 capture 옵션을 true로 설정해야 한다.

 

elem.addEventListener(..., {capture: true})
//아니면, 아래와 같이 {capture: true} 대신, true를 써줘도 된다.
elem.addEventListener(..., true)

capture 옵션은 두 가지 값을 가질 수 있다.
false 이면 (default 값) 핸들러는 버블링 단계에서 동작

true이면 핸들러는 캡쳐링 단계에서 동작

 

<style>
  body * {
	margin: 10px;
    border: 1px solid blue;
}
</style>

<form>FORM
  <div>DIV
    <p>P</p>
  </div>
</form>

<script>
  for(let elem of document.querySelectorAll('*')) {
	elem.addEventListener("click", e => alert(`캡쳐링: ${elem.tagName}`), true)
	elem.addEventListener("click", e => alert(`버블링: ${elem.tagName}`));
}
</script>

이 예시는 문서 내 요소 '전체'에 핸들러를 할당해서 어떤 핸들러가 동작하는지 보여준다.

<p>를 클릭하면

1. HTML -> BODY -> FORM -> DIV (캡쳐링 단계, 첫 번째 리스너)

2. P (타깃 단계, 캡쳐링과 버블링 둘 다에 리스너를 설정하여 두 번 호출)

3. DIV -> FORM -> BODY -> HTML (버블링 단계, 두 번째 리스너)

 

event.eventPhase 프로퍼티를 이용하면 현재 발생 중인 이벤트 흐름의 단계를 알 수 있다. 반환되는 정숫값에 따라 이벤트 흐름의 현재 실행 단계를 구분할 수 있다. 

 

핸들러를 제거할 때 removeEventListener가 같은 단계에 있어야한다. 

addEventListener(..., true)로 핸들러를 할당해 주면, 핸들러를 지울 때, 
removeEventListener(..., true)를 사용해 지워야 한다. 같은 단계에 있어야 핸들러가 지워짐.

 

반응형