이벤트 버블링, 캡쳐링, 이벤트 위임

fromzoo·2021년 1월 17일
0
  • 이벤트 등록 방법
  • 이벤트 전달 방식 (이벤트 버블링, 이벤트 캡쳐, 이벤트 위임)
  • event.stopPropagation()

이벤트 등록

공부하기 전에 기본적으로 이해하고 있어야할 내용은 바로 웹 애플리케이션의 이벤트 등록이다. 이벤트 등록이란 웹 애플리케이션에서 사용자의 입력을 받기 위해서 필요한 기능이다. 아래와 같은 코드를 의미한다.

<button>add one item</button>
var button = document.querySelector('button')
button.addEventListener('click',addItem)

function addItem(event) {
	console.log(event)
}

add one item 이라는 간단한 버튼을 만들어 클릭했을때 addItem이라는 함수를 실행시키는 코드다. 버튼을 클리하고 나면 addItem함수가 실행되고 addItem함수에 event 인자가 넘어온다. event인자를 콘솔에 출력해보면 이벤트와 관련된 정보를 확인할 수 있다.

이처럼 addEventListener() 웹 API는 웹 개발자들이 화면에 동적인 기능을 추가하기 위해 자연스럽게 접하게 되는 기본적인 기능이다. 사용자의 입력에 따라 추가 동작을 구현할 수 있는 방법이다. 여기서 브라우저는 어떻게 이벤트의 발생을 감지할까?

브라우저가 이벤트를 감지하는 방식 2가지를 알아보자.

이벤트 버블링 - Event Bubbling

이벤트 버블링은 특정 화면 요소에서 이벤트가 발생했을때 해당 이벤트가 더 상위의 화면 요소들로 전달되어 가는 특성을 의미한다.

하위의 클릭 이벤트가 상위로 전달되어 가는 그림

상위의 화면 요소란? HTML요소는 기본적으로 트리구조를 갖는다. 여기서는 트리 구조상으로 한 단계 위에 있는 요소를 상위 요소라고 하며 body 태그를 최상위 요소라고 부른다.

위 그림은 아래에 예시로 들 코드를 미리 도식화한 그림이다. 세 개의 div 태그가 있고 가장 아래에 있는 div 태그에서 이벤트가 발생했을 때 최상위 요소인 body 태그까지 이벤트가 전달되는 모습이다.

<body>
	<div class="one">
		<div class="two">
			<div class="three">
			</div>
		</div>
	</div>
</body>
var divs = document.querySelectorAll('div');
divs.forEach(function(div) {
	div.addEventListener('click', logEvent);
});

function logEvent(event) {
	console.log(event.currentTarget.className);
}

위 코드는 세 개의 div 태그에 모두 클릭 이벤트를 등록하고 클릭했을때 logEvent 함수를 실행시키는 코드다. 여기서 위 그림대로 최하위 div 태그 <div class="three"></div>를 클릭하면 아래와 같은 결과가 실행된다.

// 콘솔화면
three
two
one

three 클래스를 갖는 div 태그를 클릭했을때의 결과

div태그 한 개만 클릭했을 뿐인데 왜 3개의 이벤트가 발생되는 걸까? 그 이유는 브라우저가 이벤트를 감지하는 방식 때문이다.

브라우저는 특정 화면 요소에서 이벤트가 발생했을 때 그 이벤트를 최상위에 있는 화면 요소까지 이벤트를 전파시킨다. 따라서, 클래스명 three -> two -> one 순서로 div 태그에 등록된 이벤트들이 실행된다. 마찬가지로 two 클래스를 갖는 두번째 태그를 클릭했다면 two -> one순으로 클릭이벤트가 동작한다.

여기서 주의해야할 점은 각 태그마다 이벤트가 등록되어 있기 때문에 상위요소로 이벤트가 전달되는 것을 확인할 수 있다. 만약 이벤트가 특정 div 태그에만 달려 있다면 위와 같은 동작 결과는 확인할 수 없다.

위와 같은 하위에서 상위요소로의 이벤트 전파 방식을 이벤트 버블링이라고 한다.

이벤트 캡쳐 - Event Capture

이벤트 캡쳐는 이벤트 버블링과 반대 방향으로 진행되는 이벤트 전파 방식이다.

클릭 이벤트가 발생한 지점을 찾아내려가는 그림

위 그림처럼 특정이벤트가 발생했을때 최상위 요소인 body 태그에서 해당 태그를 찾아 내려간다. 그럼 이벤트 캡쳐는 코드로 어떻게 구현할까?

<body>
	<div class="one">
		<div class="two">
			<div class="three">
			</div>
		</div>
	</div>
</body>
var divs = document.querySelectorAll('div');
divs.forEach(function(div) {
	div.addEventListener('click', logEvent, {
		capture: true // default 값은 false
	});
});

function logEvent(event) {
	console.log(event.currentTarget.className);
}

addEventListener() API에서 옵션 객체에 capture:true를 설정해주면 된다. 그러면 해당 이벤트를 감지하기 위해 이벤트 버블링과 반대 방향으로 탐색한다.

따라서, 아까와 동일하게 <div class="three"></div>를 클릭해도 아래와 같은 결과가 나타난다.

// 콘솔 화면
one 
two
three

three 클래스를 갖는 div 태그를 클릭했을 때의 결과

event.stopPropagation()

"난 이렇게 복잡한 이벤트 전달 방식을 알고 싶지 않고, 그냥 원하는 화면 요소의 이벤트만 신경쓰고 싶어요."라고 생각할 수 있다. 실제로 마감기한에 쫓기는 상황에서 이런 동작 방식을 정확히 이해하는 시간보다는 구현에 더 많은 시간을 쏟아야 하기 때문이다. 그럴때는 아래처럼 stopPropagation() 웹 API를 사용한다.

function logEvent(event) {
	event.stopPropagation();
}

위 API는 해당 이벤트가 전파되는 것을 막는다.

따라서, 이벤트 버블링의 경우에는 클릭한 요소의 이벤트만 발생시키고 상위 요소로 이벤트를 전달하는 것을 방해한다.

그리고 이벤트 캡쳐의 경우에는 클릭한 요소의 최상위 요소의 이벤트만 동작시키고 하위요소들로 이벤트를 전달하지 않는다.

위와 같이 logEvent 함수에 stopPropagation() API를 사용한다면 앞의 '이벤트 버블링 예제'와 '이벤트 캡쳐 예제'에서 사용한 코드 기준으로 각각 three와 one이 찍힌다.

// 이벤트 버블링 예제
divs.forEach(function(div) {
	div.addEventListener('click', logEvent);
});

function logEvent(event) {
	event.stopPropagation();
	console.log(event.currentTarget.className); // three
}
// 이벤트 캡쳐 예제
divs.forEach(function(div) {
	div.addEventListener('click', logEvent, {
		capture: true // default 값은 false입니다.
	});
});

function logEvent(event) {
	event.stopPropagation();
	console.log(event.currentTarget.className); // one
}

이벤트 위임 - Event Delegation

앞에서 살펴본 이벤트 버블링과 캡쳐는 사실 이벤트 위임을 위한 선수 지식이다. 이벤트 위임은 실제 바닐라 JS로 웹 앱을 구현할때 자주 사용하게 되는 코딩 패던이다.

이벤트 위임을 한 문장으로 요약해보면 하위 요소에 각각 이벤트를 붙이지 않고 상위요소에서 하위 요소의 이벤트들을 제어하는 방식이다.

코드를 보면서 살펴보자.

<h1>오늘의 할 일</h1>
<ul class="itemList">
	<li>
		<input type="checkbox" id="item1">
		<label for="item1">이벤트 버블링 학습</label>
	</li>
	<li>
		<input type="checkbox" id="item2">
		<label for="item2">이벤트 캡쳐 학습</label>
	</li>
</ul>
var inputs = document.querySelectorAll('input');
inputs.forEach(function(input) {
	input.addEventListener('click', function(event) {
		alert('clicked');
	});
});

할 일 목록의 체크박스를 클릭했을때 클릭 이벤트 리스너가 동작하는 모습

자바스크립트 querySelectorAll()를 이용해 화면에 존재하는 모든 인풋 박스 요소를 가져온 다음 각 인풋 박스의 요소에 클릭 이벤트 리스너를 추가한다. 화면을 실행시키고 각 리스트 아이템의 인풋 박스(체크박스)를 클릭하면 위와 같이 경고창이 표시된다.

여기까지는 별 다를 것 없는 코드이다. 만약 여기서 할 일이 더 생겨 리스트 아이템을 추가하면 어떻게 될까?

// 새 리스트 아이템을 추가하는 코드
var itemList = document.querySelector('.itemList');

var li = document.createElement('li');
var input = document.createElement('input');
var label = document.createElement('label');
var labelText = document.createTextNode('이벤트 위임 학습');

input.setAttribute('type', 'checkbox');
input.setAttribute('id', 'item3');
label.setAttribute('for', 'item3');
label.appendChild(labelText);
li.appendChild(input);
li.appendChild(label);
itemList.appendChild(li);

새로 추가된 리스트 아이템(이벤트 위임 학습)에서 클릭 이벤트가 동작하지 않는 모습

새로 추가된 리스트 아이템에는 클릭 이벤트 리스너가 동작하지 않는다. 왜그럴까?

코드를 다시 살펴보면 인풋 박스에 클릭 이벤트 리스너를 추가하는 시점에서 리스트 아이템은 두 개이다. 따라서, 새롭게 추가된 리스트 아이템에는 클릭 이벤트 리스너가 등록되지 않았다. 이런식으로 매번 새롭게 추가된 리스트 아이템까지 클릭 이벤트 리스너를 일일이 달아줘야 할까?

리스트 아이템이 많아지면 많아질수록 이벤트 리스너를 다는 작업 자체가 번거롭다. 이 번거로운 작업을 해결할 수 있는 방법이 이벤트 위임이다.

위에서 살펴본 코드를 아래와 같이 변경한다.

var itemList = document.querySelector('.itemList');
itemList.addEventListener('click', function(event) {
	alert('clicked');
});

// 새 리스트 아이템을 추가하는 코드
// ...

화면의 모든 인풋박스에 일일이 리스너를 추가하는 대신 이제는 인풋 박스의 상위 요소인 ul태그, .itemList에 이벤트 리스너를 달아놓고 하위에서 발생한 클릭 이벤트를 감지하도록 한다. 이 부분이 앞에서 배웠던 이벤트 버블링이다.

결과는 다음과 같다.

새로 추가된 리스트 아이템에서 클릭 이벤트가 정상적으로 동작하는 모습

이젠 리스트 아이템을 새로 추가할때마다 클릭 이벤트를 달지않아도 된다.

궁금한 점

  • 기본 디폴트 값은 이벤트 버블링인지?
    - 디폴트 값은 false
    - true로 설정해주면 캡쳐링을 통해 이벤트 전파
  • 중첩된 요소들만(반복문으로 이벤트 리스너를 사용했을시?) 엘리먼트들이 전파되는건지?

질문

  1. 이벤트 버블링에 대해 말씀해주세요.

    • 브라우저가 이벤트를 감지하는 방식
    • 중첩된 요소에서 이벤트가 발생했을때 하위에서 상위로 이벤트가 전달되는 방식
  2. 이벤트 버블링은 기본적으로 child -> parent인데 반대로 구현하는 법은?

    • 이벤트 캡쳐링을 사용한다.
    • 이벤트 캡쳐링이란 이벤트 버블링과는 반대로 하위에서 상위로 이벤트가 전파되는 방식이다.
    • target.addEventListener("click", function(){}, true);
    • 이벤트 리스너의 세번째 인자에 true값이나, 객체로 { capture : true } 이런식으로 전달한다.
  3. 이벤트 버블링을 막기위한 방법은?

    • stopPropagation()
    • 이 속성 사용시 이벤트 버블링은 클릭한 요소만 이벤트가 발생하고,
    • 캡쳐링은 클릭한 요소의 최상위 요소의 이벤트만 동작시키고 하위 이벤트는 전달하지 않는다.
  4. 이벤트를 잘 활용하면 어떻게 사용할 수 있을까?

    • 이벤트 위임을 사용하자!
    • 이벤트 위임이란? 리스너가 필요한 엘리먼트 요소에 일일이 등록하지 않고 최상위에 리스너를 달아놓고 하위에서 발생한 이벤트를 감지하도록하는 방식.
  5. passive?

참고한 출처

profile
프론트엔드 주니어 개발자 🚀

0개의 댓글