🍁Event Bubbling, Event Tunneling
⬆️Event Bubbling
이벤트 버블링(Event Bubbling)은 이벤트가 발생한 요소에서 시작해 상위 DOM 트리 요소로 이벤트가 전파되는 방식이다.
가장 하위의 요소에서 이벤트가 시작되어 가장 상위의 부모 요소까지 이벤트가 전파되며, 각 요소의 이벤트 핸들러가 순차적으로 호출된다.
⬇️Event Tunneling
이벤트 터널링(Event Tunneling)은 이벤트 버블링과 반대로 이벤트가 상위 요소에서 시작하여 이벤트가 발생한 요소까지 전파되는 방식이다.
이벤트는 상위 요소에서 시작하여 하위 요소로 내려가며, 마찬가지로 이벤트 핸들러는 최상위 요소에서 시작하여 하위 요소로 내려가면서 호출된다.
이벤트 터널링은 다른 말로 Event Capturing(이벤트 캡쳐링)이라고도 부른다. 자바스크립트에서는 이벤트 터널링이라고 부르지만, 다른 언어에서는 이벤트 캡쳐링이라고 부르는 경우가 많다.
자바스크립트에서 옵션을 주면 이벤트 터널링을 줄 수 있다.
BOM 이벤트 / DOM 이벤트
- BOM: 이벤트 버블링 처리만 할 수 있다.
- DOM: 이벤트 버블링과 이벤트 터널링을 선택할 수 있다.
BOM과 DOM모두 이벤트 버블링이 기본 동작이다.
기본 코드
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box { padding: 50px; }
#p1 { background-color: tomato; }
#p2 { background-color: gold; }
#p3 { background-color: cornflowerblue; }
</style>
</head>
<body>
<div id="p1" class="box">
<div id="p2" class="box">
<div id="p3" class="box">
</div>
</div>
</div>
<script>
const p1 = document.getElementById('p1');
const p2 = document.getElementById('p2');
const p3 = document.getElementById('p3');
</script>
</body>
</html>
p1.onclick = function() {
alert('red');
}
p2.onclick = function() {
alert('yellow');
}
p3.onclick = function() {
alert('blue');
}
눈에 보이는 빨간색 영역이 전부가 아니라 계층으로 볼 때 테두리 경계선 안쪽은 모두 빨간색 영역이기 때문에 노란색과 파란색을 클릭해도 red 함수가 호출된다.
노란색을 클릭하면 yellow와 red가 순차적으로 출력되며, 파란색을 클릭하면 blue, yellow, red가 순차적으로 출력된다.
🍁Event Bubbling, Event Tunneling 실행 과정
운영체제는 사용자의 행동을 감시하는 이벤트를 처리하는 기능이 있다.
먼저 운영체제는 클릭된 부분이 VS CODE인지, 브라우저인지, 그림판인지 알아낸다. 운영체제가 사용자가 무엇을 클릭했는지 알아낸 다음에 해당 정보를 브라우저에 넘겨준다. 그리고 브라우저는 html에 해당 정보를 넘겨주게 된다.
이렇게 이벤트를 넘겨주는 방식에 이름이 있다.
상위태그에서부터 하위태그로 내려오면서 통제가 되는 위 이벤트의 전달 방식은 위에서 아래로 내려간다고 해서 이벤트 터널링이라고 한다.
반대로 밑에서 위로 이벤트가 발생했다고 전달되는 것을 공기방울 올라가듯 올라간다고 해서 이벤트 버블링이라고 한다.
자바스크립트는 이벤트 버블링을 지원한다.
자바스크립트는 기본적으로 이벤트 버블링을 지원하며, 이벤트 터널링을 지원하지 않는다.
p3를 클릭했을 때, blue > yellow > red 순으로 출력하는 이유는 이벤트 버블링이 발생했기 때문이다.
document.getElementsByTagName('html')[0].onclick = function() {
alert('html');
}
document.getElementsByTagName('body')[0].onclick = function() {
alert('body');
}
위 코드를 추가하면 p3을 클릭했을 때, blue > yellow > red > body > html 순으로 출력한다.
p3을 클릭했을 때, p2와 p1 이벤트를 발생하지 않게 하려면 어떻게 해야 할까?
이벤트 버블링이 발생할 때 당사자인 p3만 실행되게 하고, p2와 p1은 실행되지 않게 해야 한다.
이를 이벤트 버블링을 캔슬(cancel)한다고 한다.
🍂cancelBubble, stopPropagation
p3.onclick = function() {
alert('blue');
event.cancelBubble = true;
}
cancelBubble을 적용하면 해당 시점 이후로 발생하는 버블링이 사라진다.
부모와 자식이 겹쳐 있는 곳에서 둘 다 실행하게 할지, 이벤트 버블링을 중단하고 자신만 실행하게 할지 결정할 수 있다.
p3.addEventListener('click', function() {
alert('blue');
event.cancelBubble = true;
event.stopPropagation(); //이벤트 전파 중지
});
cancelBubble과 동일하게 stopPropagation 메서드로 이벤트 전파를 중지할 수 있다.
같은 구문이지만 표준인 stopPropagation을 사용하는 것을 권장한다.
window 키이벤트 설정
<div id="p1" class="box">
<div id="p2" class="box">
<div id="p3" class="box">
</div>
</div>
</div>
<hr>
<input type="text" id="txt1">
<hr>
<img src="../asset/images/dog01.jpg" id="dog1">
텍스트 박스와 이미지를 추가했다.
키보드 방향키를 눌러 이미지의 크기를 늘이고 줄이는 코드를 만들어보자.
전역에서 이벤트 발생
window.onkeydown = function() {
if (event.keyCode == 37) {
document.getElementById('dog1').width -= 10;
} else if (event.keyCode == 39) {
document.getElementById('dog1').width += 10;
}
};
window에 키이벤트를 걸었기 때문에 전역에서 이벤트가 발생한다.
그런데 텍스트 박스에서 글자를 수정하려고 방향키를 누르면 사진의 크기에 영향이 생긴다. 이는 최상위의 window에서 키이벤트가 실행되면 txt1까지 전달이 되어서 생기는 문제이다.
항상 이벤트가 전파되어 돌아올 때 제일 끝에서 window를 만날 수밖에 없다. 그래서 원하지 않는 곳에서 반응을 하지 않도록 텍스트 박스에서 이벤트 버블링을 캔슬시켜야 한다.
텍스트 박스에서 이벤트 버블링 캔슬
document.getElementById('txt1').onkeydown = function() {
event.cancelBubble = true;
};
이제 텍스트 박스에서 방향키를 눌러도 이벤트에 영향이 생기지 않는다.
Event Tunneling 전환
p1.addEventListener('click', function() {
alert('red');
}, true);
p2.addEventListener('click', function() {
alert('yellow');
}, true);
p3.addEventListener('click', function() {
alert('blue');
}, true);
옵션을 true로 주면 이벤트 터널링으로 변경되어 p3을 클릭해도 red > yellow > blue 순으로 출력한다.
🍁Event Bubbling 예제
테이블 tr 이벤트 적용
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#tbl1 {
border: 1px solid black;
border-collapse: collapse;
}
#tbl1 td {
border: 1px solid black;
width: 150px;
height: 70px;
text-align: center;
}
</style>
</head>
<body>
<h1>테이블</h1>
<!-- table#tbl.table>tr*10>td{item}*5 -->
<table id="tbl1" class="table">
<tr>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
</tr>
<tr>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
</tr>
<tr>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
</tr>
<tr>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
</tr>
<tr>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
</tr>
<tr>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
</tr>
<tr>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
</tr>
<tr>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
</tr>
<tr>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
</tr>
<tr>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
<td>item</td>
</tr>
</table>
<script>
const table = document.getElementById('tbl1');
tr = table.firstElementChild.children; //table.tbody.tr들
for (let i=0; i<tr.length; i++) {
//이벤트가 발생된 객체(태그)
//-event.target
//-event.srcElement
tr[i].onmouseover = function() {
//event.target.bgColor = 'gold';
//event.srcElement.bgColor = 'gold';
event.target.parentElement.bgColor = 'gold';
};
tr[i].onmouseout = function() {
event.currentTarget.bgColor = 'transparent';
}
}
</script>
</body>
</html>
표 위에 마우스를 올렸을 때, td가 아닌 tr에 이벤트를 적용하고 싶다.
이때 event.targe에 이벤트를 적용하면 td에 이벤트가 적용된다.
target은 이벤트 당사자를 가리키는 게 아니다.
마우스 이벤트 전파가 td에서부터 html까지 올라가는 table이 있다.
event.target은 이벤트에서 가장 밑에 있는 것을 의미한다. 그 아래에 자식 객체가 있는 바람에 당사자가 아니라 td를 반환하게 된다.
즉, target은 이벤트 발생 관련 계층 중 최하 하위(유턴 객체)를 의미한다. 그래서 tr에 이벤트를 걸어도 targer이 td가 되기 때문에 tr에 이벤트를 걸기 위해서는 td의 부모에 이벤트를 걸어주면 된다.
currentTarget으로 tr에 이벤트 적용
tr[i].onmouseover = function() {
event.currentTarget.bgColor = 'gold';
};
Closure로 tr에 이벤트 적용
for (let i=0; i<tr.length; i++)
tr[i].onmouseover = function() {
tr[i].bgColor = 'gold';
};
}
바깥쪽에서 죽어버릴 예정인 tr을 Closure로 인해 사용할 수 있게 된다.😵
이 경우에는 전역 변수가 되도록 for문에서 반드시 var가 아닌 let으로 변수가 선언되어야 한다.
클로저에 대해서는 위 글을 참고한다.
테이블 아이콘 이미지 삽입 1
if (event.buttons == 1) {
let img = document.createElement('img');
img.setAttribute('src', '../asset/images/rect_icon01.png');
event.target.appendChild(img);
console.log(event.target);
}
테이블을 클릭하면 해당 공간에 아이콘 이미지가 나타난다.
그리고 다시 한번 클릭하면 다른 이미지가 나타나야 하지만 이미지는 단독 태그이기 때문에 이미지 위에 새로운 이미지가 생기지 않는다.
이미지가 있는 상태에서 클릭을 하면 기존 이미지를 제거하고 새로운 이미지로 교체할 수 있게 해보자.
currentTarget 메소드
if (event.buttons == 1) {
let img = document.createElement('img');
img.setAttribute('src', '../asset/images/rect_icon01.png');
event.currentTarget.appendChild(img);
//console.log(event.target);
}
currentTarget 메소드로 아이콘 이미지를 나타나게 하면 새로운 이미지가 아래에 추가될 뿐이다.
이를 해결하기 위해서는 다음의 두 가지 방법을 사용할 수 있다.
새로운 이미지로 전환
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#tbl1 {
border: 1px solid black;
border-collapse: collapse;
}
#tbl1 td {
border: 1px solid black;
width: 126px;
height: 126px;
}
#tbl1 td img {
display: block;
}
</style>
</head>
<body>
<h1>테이블</h1>
<table id="tbl1">
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</table>
<script>
const list = document.querySelectorAll('#tbl1 td');
for (let i=0; i<list.length; i++) {
list[i].onmousedown = function() {
if (event.buttons == 1) { //마우스 왼쪽 버튼
if (event.target.nodeName == 'TD') {
//비어있는 <td>
let img = document.createElement('img');
img.setAttribute('src', '../asset/images/rect_icon01.png');
event.target.appendChild(img);
} else {
//<img>
event.currentTarget.removeChild(event.currentTarget.firstElementChild); //기존 이미지 삭제
let img = document.createElement('img');
img.setAttribute('src', '../asset/images/rect_icon02.png');
event.currentTarget.appendChild(img);
}
//console.log(event.target);
} else if (event.buttons == 2) { //마우스 오른쪽 버튼
if(event.target.nodeName =='TD') {
//비어있는 <td>
let img = document.createElement('img');
img.setAttribute('src', '../asset/images/rect_icon02.png');
event.target.appendChild(img);
} else {
event.currentTarget.removeChild(event.currentTarget.firstElementChild); //기존 이미지 삭제
let img = document.createElement('img');
img.setAttribute('src', '../asset/images/rect_icon01.png');
event.currentTarget.appendChild(img);
}
} else if (event.buttons == 4) { //마우스 휠 버튼
//클릭한 이미지 삭제
//-부모태그.removeChild(자식태그)
//event.target.removeChild(event.target.firstElementChild); //작동X
//event.target.parentElement.removeChild(event.target); //번거로움
event.currentTarget.removeChild(event.currentTarget.firstElementChild); //최종삭제코드
}
}
}
window.oncontextmenu = function() {
return false;
};
</script>
</body>
</html>
비어있는 td인지 비어있지 않은 td(img가 들어있는 td)인지를 확인하여 이미지를 추가할 수 있다.
만약 클릭을 했는데 이미지가 들어 있을 경우 기존의 이미지를 삭제하고 새로운 이미지를 추가한다.
테이블 아이콘 이미지 삽입 2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#tbl1 {
border: 1px solid black;
border-collapse: collapse;
}
#tbl1 td {
border: 1px solid black;
width: 80px;
height: 80px;
}
#tbl1 td img {
display: block;
width: 80px;
height: 80px;
}
</style>
</head>
<body>
<!-- table#tbl1>tr*10>td*10 -->
<table id="tbl1">
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
<script>
const list = document.querySelectorAll('#tbl1 td');
for (let i=0; i<list.length; i++) {
//마우스가 td 위에 있을 때
list[i].onmouseenter = function() {
//보조키의 눌림 상태
console.log(event.ctrlKey);
console.log(event.shiftKey);
console.log(event.altKey);
//이미지가 없을 경우에만 이미지 삽입
if(event.currentTarget.children.length == 0) {
//event.target.bgColor = 'gold';
const img = document.createElement('img');
if (event.ctrlKey) {
img.setAttribute('src', '../asset/images/rect_icon04.png');
} else {
img.setAttribute('src', '../asset/images/rect_icon05.png');
}
event.currentTarget.appendChild(img);
}
};
// 마우스가 td 위에 없을 때
// list[i].onmouseleave = function() {
// //event.target.bgColor = 'transparent';
// event.currentTarget.removeChild(event.currentTarget.firstElementChild);
// };
}
</script>
</body>
</body>
</html>
마우스를 클릭할 때가 아니라 오버했을 때 이벤트가 실행되도록 해보자.
over와 down과는 다르게 enter와 leaver는 td에게만 반응을 한다.
보조키 눌림 상태 반환
- ctrlKey
- shiftKey
- altKey
보조키(ctrl, shift, alt)의 눌림 상태를 반환받을 수 있다.