자바스크립트는 클릭, 드래그, 키(KEY) 다운 등 사용자가 HTML DOM과 상호작용 할 수 있는 다양한 브라우저 이벤트를 지원하고 있다. 그 중 html 태그의 onClick 속성과 onContextMenu 속성은 각각 클릭과 우클릭 이벤트 발생 시 작동할 이벤트 핸들러를 속성값으로 갖는다.
이벤트 버블링과 이벤트 캡쳐링은 DOM 트리를 타고 전달되는 이벤트 객체의 이동 단계로, 이벤트 객체는 캡쳐링 단계인지 버블링 단계인지에 따라 부모 혹은 자식 요소로 전파된다. 쉽게 생각해서 각 요소에서 이벤트가 발생하는 순서라고 이해해도 괜찮을 듯 싶다. 웹3(w3.org) 공식 사이트에서는 이벤트 객체의 전이 단계와 순서를 다음과 같이 정의하고 있다.
(1) 캡쳐링 단계 (Capture Phase) 에서는 window에서 목표 요소(이벤트가 발생한 요소) 방향으로 이벤트 객체가 이동한다. <td>에서 이벤트를 실행시켰다면 window, document, html, ... , tr, td 순으로 이벤트 객체가 이동하며 이벤트 핸들러를 작동시킨다.
(2) 타켓 단계 (Target Phase) 에서는 이벤트 객체가 목표 요소에 도착해서, 사용자가 의도했던 이벤트 핸들러를 실행시킨다. event.target 속성은 이벤트 타겟 객체를 가리키고, event.currentTarget 속성은 현재 이벤트 핸들러가 실행된 객체를 가리킨다. (그 유명한 this 변수도 이 객체를 가리킨다.)
(3) 버블링 단계 (Bubbling Phase) 는 캡쳐링 단계의 반대 방향(타겟 요소 → window)으로 이벤트 객체가 이동한다. 대부분 이벤트의 default 값은 버블링 단계의 이벤트이다.
공식 문서의 내용을 대략 요약하면 그러하다. 즉, 사용자가 html 내의 타겟 객체에서 이벤트를 발생시키면 이벤트 객체가 window에서 시작해 dom 트리를 타고 타겟 객체에 도달한 후, 다시 window로 되돌아가며 이벤트를 발생시킨다는 뜻이다. 이를 코드를 통해 확인해볼 수도 있다.
위 그림은 다음과 같은 코드로 생성됐다. div 안에 div를, 그리고 그 div 안에 또 div를 넣어 삼중 중첩으로 사각형을 만든 후 각 사각형에 클릭 이벤트를 달아줬다. 개발자 도구의 콘솔창을 연 후 위 그림에서 가장 작은 사각형을 클릭해보라.
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Test</title>
<style>
.grand { width:100px; height:100px; background-color : red;}
.parent { width:60px; height:60px; background-color : orange;}
.child { width:30px; height:30px; background-color : yellow;}
</style>
</head>
<body onClick="console.log('body 온클릭 실행!')">
<div class="grand" id="grand">
<div class="parent" id="parent">
<div class="child" id="child"></div>
</div>
</div>
</body>
<script>
let grand = document.getElementById('grand');
grand.addEventListener('click', function (e) {
console.log("조상", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
});
let parent = document.getElementById('parent');
parent.onclick = function (e) {
console.log("부모", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
}
let child = document.getElementById('child');
child.onclick = function (e) {
console.log("자식", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
}
</script>
</html>
아무 옵션 없이 평상시 사용하듯 이벤트를 만들었을 때 실행 순서는 자식-부모-조상 순이다. 또한 body 요소의 onClick 속성에 달아준 이벤트 핸들러가 조상 이벤트 이후 실행되는 모습도 볼 수 있다. 이벤트 객체가 타겟 요소에서 조상 요소 방향으로 이동하고 있으며, 아무 옵션 없이 생성한 이벤트는 기본적으로 버블링 단계의 이벤트임을 확인할 수 있다.
또한 event.target(타겟) 은 이벤트가 실행된 타겟 요소를, event.currentTarget(현재 타겟)은 이벤트 객체를 현재 넘겨받은 요소를, this는 event.currentTarget과 동일한 요소를 가리키고 있음도 확인했다.
캡쳐링 단계에서의 이벤트 객체 전이까지 확인하기 위해 이벤트를 추가로 더 달아줬다. 이벤트 리스터 함수의 두 번째 인자로 캡쳐 단계의 이벤트를 붙이겠다는 내용의 객체를 보내주면 된다.
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Test</title>
<style>
.grand { width:100px; height:100px; background-color : red;}
.parent { width:60px; height:60px; background-color : orange;}
.child { width:30px; height:30px; background-color : yellow;}
</style>
</head>
<body onClick="console.log('body 온클릭 실행!')">
<div class="grand" id="grand">
<div class="parent" id="parent">
<div class="child" id="child"></div>
</div>
</div>
</body>
<script>
let grand = document.getElementById('grand');
grand.addEventListener('click', function (e) {
console.log("조상-버블,", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
});
grand.addEventListener('click', function (e) {
console.log("조상-캡쳐,", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
}, {capture : true});
let parent = document.getElementById('parent');
parent.addEventListener('click', function (e) {
console.log("부모-버블,", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
});
parent.addEventListener('click', function (e) {
console.log("부모-캡쳐,", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
}, {capture : true});
let child = document.getElementById('child');
child.addEventListener('click', function (e) {
console.log("자식-버블,", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
});
child.addEventListener('click', function (e) {
console.log("자식-캡쳐,", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
}, {capture : true});
</script>
</html>
이해했던대로 이벤트 핸들러가 DOM 트리의 '조상→타겟 →조상' 순서대로, 각각 '캡쳐 →타겟 →버블' 단계 순으로 실행되는 모습이다. 처음 그림에서 확인했던 것 처럼 타겟 요소의 자식 요소로는 이벤트 객체가 전이되지 않고, window에서 트리 구조를 타고 내려오다가 타겟 요소를 마주치면 마침표를 찍은 후 다시 트리를 타고 window로 되돌아가는 순서다. 자식 div가 아닌 조상 div를 클릭했을 경우에는 타겟 요소가 조상 div가 되어 부모, 자식 div까지는 이벤트 객체가 전이되지 않는다.
버블링과 캡쳐링의 전이를 중간에 막을 수도 있다. 이벤트 객체의 stopPropagation() 함수를 실행하면 이벤트 객체의 이후 전이 단계가 실행되지 않는다. 위 코드에서 '부모-캡쳐' 단계의 핸들러에서 위 함수를 실행해보겠다.
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Test</title>
<style>
.grand { width:100px; height:100px; background-color : red;}
.parent { width:60px; height:60px; background-color : orange;}
.child { width:30px; height:30px; background-color : yellow;}
</style>
</head>
<body onClick="console.log('body 온클릭 실행!')">
<div class="grand" id="grand">
<div class="parent" id="parent">
<div class="child" id="child"></div>
</div>
</div>
</body>
<script>
let grand = document.getElementById('grand');
grand.addEventListener('click', function (e) {
console.log("조상-버블,", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
});
grand.addEventListener('click', function (e) {
console.log("조상-캡쳐,", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
}, {capture : true});
let parent = document.getElementById('parent');
parent.addEventListener('click', function (e) {
console.log("부모-버블,", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
});
parent.addEventListener('click', function (e) {
e.stopPropagation();
console.log("부모-캡쳐,", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
}, {capture : true});
let child = document.getElementById('child');
child.addEventListener('click', function (e) {
console.log("자식-버블,", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
});
child.addEventListener('click', function (e) {
console.log("자식-캡쳐,", `타겟 : ${e.target.id}, 현재 타겟 : ${e.currentTarget.id}, this : ${this.id}`);
}, {capture : true});
</script>
</html>
부모-캡쳐 단계까지 실행된 후 이후 단계가 진행되지 않는다. 캡쳐링 단계는 물론이고 이후 이어질 타겟 단계, 버블링 단계가 모두 스킵된 모습이다. 하지만 자바스크립트 공식 문서에서는 이벤트 객체의 전이 스킵을 가급적 지양하도록 권장하고 있다. 데이터 분석 도구를 사용할 경우 클릭 이벤트를 감지하는 경우가 많은데, stopPropagation으로 스킵된 영역은 아예 감지가 불가능하다는 모양이다. 혹 stopPropagation을 사용해야할 일이 생긴다면, 이 함수 대신 '커스텀 이벤트' 를 사용하기를 권장하고 있다.
이렇게 캡쳐링-타겟-버블링 페이즈가 매 이벤트 발생 시 마다 한 세트로 작동하는 이벤트 객체의 생명주기까지 관찰해보았다. 캡쳐링 단계와 버블링 단계를 구분하지 못해도 당장 코드 만드는데 지장은 없지만 겹쳐져있는 요소 객체마다 이벤트 속성을 부여할 경우에는 이 내용을 알아두면 꽤 요긴하다.
END.
'웹 > javascript' 카테고리의 다른 글
javascript 정규식(정규 표현식) 사용하기 (1) | 2024.02.04 |
---|