자바스크립트는 다른 언어와 다른 특징이 있는데 그 중 하나가 바로 이벤트 전파이다. 전파라는 이름에서 알 수 있듯이 이벤트가 발생하면 그 발생한 요소에 그치지 않고 어딘가로 그 이벤트가 흘러서 그 요소에도 이벤트가 발생한다는 것이다.
Window 객체나 XMLHttpRequest 객체와 같이 원래부터 존재하고 단독으로 존재하는 객체에서 이벤트가 발생하면 웹 브라우저는 해당 객체에 등록된 이벤트 처리기(핸들러, 리스너)를 호출한다. 그러나 HTML 요소에서 이벤트가 발생할 경우 그 요소 및 해당 요소의 조상 요소까지 이벤트가 전파된다. 보통 HTML 코드를 짤 때 계층형 구조를 따르고 있기 때문에 결과적으로는 버튼A 하나만 눌렀는데 버튼B에 걸린 이벤트까지 호출할 수 있다는 것이다.
그렇다면 HTML 문서에서 이벤트가 발생한다고 가정해보자.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
window.onload = function () {
let div1 = document.getElementById("outer");
div1.addEventListener("click", function () {
console.log("여기는 outer");
}, false);
}
function innerEvent2() {
console.log("여기는 inner2");
}
</script>
</head>
<body>
<div id="outer">
outer
<div id="inner1">inner1</div>
<div id="inner2" onclick="innerEvent2();">inner2</div>
</div>
</body>
</html>
위 코드에서는 inner2라는 식별자를 가진 div 객체에 innerEvent2라는 이벤트 핸들러를 등록시켰다. 또한, inner2의 부모 객체인 outer에도 클릭 이벤트를 등록시켰다. 이 때 웹 브라우저 콘솔에 inner2 이벤트에 걸린 로그만 출력될까? 결론은
아래 사진처럼 둘 다 출력된다. 이것이 바로 이벤트 전파의 기본적인 예이다.
자식 요소의 click 이벤트가 전파되어 부모 요소의 click 이벤트까지 발생시킨 것이다.
HTML에서 이벤트를 발생한 inner2 같은 HTML 요소를 이벤트 타깃이라 부른다. HTML 구조상 부모 요소안에 자식 요소가 겹쳐진 상태로 표시된다.
실제로 위 코드에서 inner2는 outer라는 div객체 안에 포함되어 있으므로 클릭했을 때 웹브라우저는 부모를 누른 것인지 자식을 누른 것인지 알 수가 없다. 따라서 이벤트가 발생하면 DOM 객체 트리의 전체에 그 이벤트에 반응하는 이벤트 핸들러나 리스너를 검색하고 등록되어 있을 경우 그것을 실행하도록 내부 구조가 짜여있다.
이벤트가 발생한 HTML 요소부터 웹브라우저 최상위 객체인 window 객체까지 이벤트가 전파되는 과정을 단계별로 구분할 수 있다. 아래에 나와있는 모든 예제는 inner2를 클릭해서 이벤트가 발생하는 것을 전제로 하겠다.
이벤트 단계 |
캡처링 |
타깃 |
버블링 |
기본적으로 이벤트는 캡처링 -> 타깃 -> 버블링 단계 순으로 전파된다.
첫 포스팅인 이벤트 핸들러와 리스너 편에서 addEventListner를 사용할 때 마지막 인수로 useCapture를 받는다고 말했다. 바로 이 useCapture가 이벤트 단계, 즉 어떤 이벤트 단계에서 리스너를 발생시킬 것이냐를 결정하는 논리값이다.
이벤트가 window 객체에서 출발해서 DOM 트리를 타고 이벤트 타깃까지 전파된다. 따라서 캡처링 단계에 등록된 이벤트 핸들러나 리스너는 이벤트 타켓에 등록된 것보다 먼저 실행된다. 캡처링이란 어원은 이벤트 타깃에서 이벤트를 수신하기 전에 캡처(빼돌림)한다는 의미이다. 위 코드를 아래와 같이 수정하고 결과가 어떻게 발생했는지 확인해보자.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
window.onload = function () {
let div1 = document.getElementById("outer");
div1.addEventListener("click", function () {
console.log("여기는 outer");
}, true); // useCapture : 이벤트 단게 변경 (true : 캡처링 및 타깃)
}
function innerEvent2() {
console.log("여기는 inner2");
}
</script>
</head>
<body>
<div id="outer">
outer
<div id="inner1">inner1</div>
<div id="inner2" onclick="innerEvent2();">inner2</div>
</div>
</body>
</html>
부모 요소에 addEventListener로 등록된 이벤트 리스너에 useCapture값을 true로 변경했다. 이는 해당 리스너를 캡처링 단계에 반응하도록 등록했다는 뜻이다. 따라서 이벤트 타깃인 inner2가 onclick 이벤트를 수신하기 전에 부모 요소 이벤트가 먼저 처리된 것이다. 그 후 자식 요소에 반응하도록 등록된 이벤트 핸들러인 innerEvent2가 실행된 것이다.
이벤트가 이벤트 타깃에 전파되는 단계를 말한다. 이벤트 타깃에 등록된 핸들러나 리스너는 이 시점에 실행된다.
캡처링과 반대로 이벤트가 이벤트 타깃에서 출발해서 DOM 트리를 타고 window 객체까지 전파되는 단계이다. 버블링 단계에 등록된 이벤트 리스너는 이벤트 타깃에 등록된 이벤트가 실행된 다음에 실행된다. 맨 처음에 이벤트 전파에서 살펴본 예제가 바로 버블링 단계이다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
window.onload = function () {
let div1 = document.getElementById("outer");
div1.addEventListener("click", function () {
console.log("여기는 outer");
}, false); // useCapture : 이벤트 단게 변경 (false : 버블링, default로 생략가능)
}
function innerEvent2() {
console.log("여기는 inner2");
}
</script>
</head>
<body>
<div id="outer">
outer
<div id="inner2" onclick="innerEvent2();">inner2</div>
</div>
</body>
</html>
부모 요소의 DOM 객체인 div1에 등록된 이벤트 리스너는 버블링 단계에 실행하도록 등록되어있다. 따라서 이벤트 타깃인 inner2를 클릭했을 때 타깃 단계에서 해당 이벤트 핸들러인 innerEvent2가 실행되고 다음 단계인 버블링 단계에서 부모 요소의 이벤트 리스너가 실행된 것이다. 버블링의 어원은 이벤트가 아래에서 위로 거품이 올라오는 것 같다고 해서 붙여진 것이다.
주의할 점은 focus나 blur 이벤트의 경우 해당 요소를 포커싱하는지 벗어나는지에 필요한 이벤트이기 때문에 버블링되지 않는다.
또한, 이벤트 핸들러의 경우는 리스너와 달리 타깃과 버블링 단계에서만 실행된다. 만약 같은 요소의 같은 이벤트를 등록하는데 버블링 단계에 등록하는 경우에는 핸들러 -> 리스너 순으로 실행되고 이벤트 리스너는 전파되는 순서에 따라 실행된다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
window.onload = function () {
let div1 = document.getElementById("outer");
let div2 = document.getElementById("inner2");
div1.addEventListener("click", function () {
console.log("여기는 outer");
}, true);
// inner2 리스너 등록
div2.addEventListener("click", function () {
console.log("여기는 inner2 리스너");
}, false)
}
// inner2 핸들러 등록
function innerEvent2() {
console.log("여기는 inner2 핸들러");
}
</script>
</head>
<body>
<div id="outer">
outer
<div id="inner1">inner1</div>
<div id="inner2" onclick="innerEvent2();">inner2</div>
</div>
</body>
</html>
그리고 같은 요소, 같은 이벤트의 캡처링과 버블링 단계 모두에 이벤트 리스너를 등록할 수도 있다. 아래 예제는 부모 요소 click이벤트를 캡처링과 버블링 모두 반응하도록 등록했다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
window.onload = function () {
let div1 = document.getElementById("outer");
let div2 = document.getElementById("inner2");
div1.addEventListener("click", function () {
console.log("여기는 outer 캡쳐링");
}, true); // outer 이벤트 : 캡처링 단계에 실행
div1.addEventListener("click", function () {
console.log("여기는 outer 버블링");
}, false); // outer 이벤트 : 버블링 단계에 실행
div2.addEventListener("click", function () {
console.log("여기는 inner2 버블링");
}, false);
}
</script>
</head>
<body>
<div id="outer">
outer
<div id="inner2">inner2</div>
</div>
</body>
</html>
위 코드처럼 같은 요소의 같은 이벤트 리스너를 캡처링과 버블링 단계에 반응하도록 설정할 수 있다. 또한 말했던 것 처럼 이벤트 리스너의 경우 동일한 단계일 때는 전파되는 순서에 따라 실행된다. 아래 다이어그램과 같은 순서에 따라 실행된 것이다. 이러한 전파 단계를 컨트롤 할 수 있는 장점 때문에 이벤트 핸들러보다는 이벤트 리스너를 사용하는 것이 좋다.
이렇게 자식 요소에서 발생한 이벤트가 부모 요소에도 전파되기 때문에 이벤트 전파를 제어하지 않으면 의도하지 않은 동작을 야기할 수 있다. 다음에 소개할 메서드를 통해 이벤트 전파를 제어할 수 있다. 해당 메서드들은 이벤트 객체의 프로퍼티로 등록되어있다.
이벤트 리스너에서 이 메서드를 호출하면 이벤트가 다음 상위 요소로 전파되는 것을 막을 수 있다.
// stopPropagation : 이벤트 전파를 취소
event객체.stopPropagation();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>stopPropagation</title>
<script>
window.onload = function () {
let div1 = document.getElementById("outer");
let div2 = document.getElementById("inner2");
div1.addEventListener("click", function (e) {
console.log("여기는 outer 캡쳐링");
}, true); // outer 이벤트 : 캡처링 단계에 실행
div1.addEventListener("click", function (e) {
console.log("여기는 outer 버블링");
}, false); // outer 이벤트 : 버블링 단계에 실행
div2.addEventListener("click", function (e) {
console.log("여기는 inner2 버블링1");
e.stopPropagation(); // 이벤트 전파 막음
}, false);
div2.addEventListener("click", function (e) {
console.log("여기는 inner2 버블링2");
}, false);
}
</script>
</head>
<body>
<div id="outer">
outer
<div id="inner2">inner2</div>
</div>
</body>
</html>
버블링 단계에서 inner2 클릭 이벤트가 발생하고 해당 클릭 이벤트가 그다음 요소로 전파되는 것을 e.stopPropagation 메서드를 통해 막았다. 따라서 그 다음 요소인 outer 버블링 이벤트는 발생하지 않았다. 또한 같은 요소의 클릭 이벤트에 등록된 다른 리스너는 정상적으로 실행된다.
이벤트 리스너에서 이 메서드를 호출한다면 그 다음 요소의 이벤트 전파 및 같은 요소의 다른 이벤트 리스너도 멈춘다.
// stopImmediatePropagation : 이벤트 전파를 일시정지
event객체.stopImmediatePropagation();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>stopImmediatePropagation</title>
<script>
window.onload = function () {
let div1 = document.getElementById("outer");
let div2 = document.getElementById("inner2");
div1.addEventListener("click", function (e) {
console.log("여기는 outer 캡쳐링");
}, true); // outer 이벤트 : 캡처링 단계에 실행
div1.addEventListener("click", function (e) {
console.log("여기는 outer 버블링");
}, false); // outer 이벤트 : 버블링 단계에 실행
div2.addEventListener("click", function (e) {
console.log("여기는 inner2 버블링1");
e.stopImmediatePropagation(); // 이벤트 전파를 즉시 정지
}, false);
div2.addEventListener("click", function (e) {
console.log("여기는 inner2 버블링2");
}, false);
}
</script>
</head>
<body>
<div id="outer">
outer
<div id="inner2">inner2</div>
</div>
</body>
</html>
inner2에 걸린 이벤트 리스너는 2개인데 첫 번째 이벤트 리스너 안에서 stopImmediatePropagation을 선언했기 때문에 다음 버블링 단계에 걸린 같은 요소의 두 번째 이벤트 리스너와 상위 요소인 outer 버블링 리스너가 동작하지 않는다.
앞서 소개한 두 메서드는 이벤트의 전파를 막는다. 뿐만 아니라 자바스크립트는 웹 브라우저의 기본 동작도 취소할 수 있는 기능을 preventDefault 메서드를 통해 제공한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>preventDefault</title>
<script>
window.onload = function () {
let anchor = document.getElementById("move-to");
anchor.addEventListener("click", function (e) {
if (!confirm("구글로 이동하시겠습니까?")) {
e.preventDefault();
}
}, false);
}
</script>
</head>
<body>
<div>
<a href="https://google.co.kr" id="move-to">구글 이동</a>
</div>
</body>
</html>
a 태그를 클릭하면 웹 브라우저는 href 속성을 읽어 해당 링크로 이동시키는 동작을 수행한다. 그러나 preventDefault 메서드를 사용하면 해당 기능을 막을 수 있다.
위 코드는 confirm 메서드를 통해 확인 버튼을 클릭하면 구글로 이동하고 취소버튼을 누르게 되면 e.preventDefault 메서드가 동작해 구글로 이동을 막는 기능을 제공한다.
preventDefault 메서드로 취소할 수 없게 만들려면 해당 이벤트 객체의 cancelable 프로퍼티를 false로 바꾸면 된다. 단, 이벤트 취소 가능여부는 이벤트 객체의 초기화 시에 판별되므로 커스텀 이벤트 객체를 생성해서 사용해야한다.
다른 분께서 설명 잘 해놓으신 것 같아서 링크를 남기겠다. 내가 하니 잘 안된다.
좀 더 시도를 해봐야할 것 같다 ㅠㅠ
https://ko.javascript.info/dispatch-events
[JavaScript] 자바스크립트 이벤트 리스너 - 추가 정보 전달 (0) | 2020.09.01 |
---|---|
[JavaScript] 자바스크립트 이벤트 리스너 안의 this (0) | 2020.09.01 |
[JavaScript] 자바스크립트 이벤트 객체 (0) | 2020.08.31 |
[JavaScript] 자바스크립트 이벤트 핸들러와 리스너 (0) | 2020.08.30 |
[JavaScript] 자바스크립트 프로토타입 객체 (0) | 2020.08.27 |