프로젝트 진행 중 DOM API 를 활용하여 이벤트를 다룰 일이 생겼다.
모든 컴포넌트에 이벤트 등록을 하던 중, 해당 방식이 매우 비효율적인 것을 알게 되었다.
이 글을 통해 이벤트의 전반적인 흐름에 대해 정리해보겠다.
1️⃣ DOM 이란 ?
이벤트에 대해 알아보기 전에, DOM(Document Object Model)에 대해 알아볼 필요가 있다.
우리가 웹사이트에 접속하면 브라우저 화면에 웹사이트의 내용이 보여지기 전까지
어떠한 일이 발생할까 ?
1. 브라우저가 서버에게 요청
2. 서버는 응답으로 브라우저에게 HTML, CSS, JS 파일 전송
3. 브라우저가 HTML 파일을 읽고 화면에 렌더링
위의 과정을 통해 우리 눈에 웹페이지가 보이게 된다.
DOM을 이해하기 위해 3번의 과정을 자세히 알아보자.
브라우저는 HTML 파일을 받으면, 먼저 해당 파일을 파싱(해석)한다.
파싱의 결과로 트리구조가 만들어지며, 이때 만들어진 트리를 DOM이라 한다.
CSS도 마찬가지로 브라우저가 CSS 파일을 파싱한 후, 트리를 만들고 이것을 CSSOM 이라 한다.
그리고, DOM과 CSSOM 을 결합하여 렌더 트리를 만들고 해당 트리를 통해 레이아웃을 그린다.
우리는 주제와 관련된 DOM에 대해서만 자세히 알아보겠다.
만약, 트리 구조에 대해 모른다면 트리 자료 구조에 대해 먼저 학습하는 것을 추천한다.
아래 예시를 확인하자. 예시를 보면 DOM의 구조에 대한 감이 온다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./main.css">
<title>Document</title>
</head>
<body>
<div>
<p> Hello World !</p>
<ul>
<li>하나</li>
<li>둘</li>
<li>셋</li>
</ul>
</div>
</body>
</html>
body 위의 노드들은 DOM이 만들어질 때 일반적으로 생기는 노드라고 이해하면 된다.
참고로, 텍스트도 DOM의 노드이다.
흔히, 우리는 JS에서 html 요소를 선택할 때 document.querySelector(선택자) 문법을 사용한다.
여기서 document가 위의 DOM 에서 document 노드를 의미하는 것이다.
즉, document.querySelector(선택자) 는 document 의 하위 노드 중 선택자에 맞는 html 요소를 가져와달라는 의미이다.
위 트리를 보면 document 노드 아래 모든 html 요소가 들어있는 것을 확인할 수 있다.
따라서, 모든 요소는 document 노드 아래에 있기 때문에 document.querySelector 문법을
사용하면 내가 원하는 요소를 정확히 찾을 수 있던 것이다.
2️⃣ 이벤트 버블링(bubbiling)
이벤트 버블링이란, 특정 화면 요소에서 이벤트가 발생했을 때,
해당 이벤트가 요소의 상위 노드로 전달되어가는 특성이다.
바로 예시를 확인하자.
위랑 다른 HTML 예시를 들고, CSS를 적용시키겠다.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./main.css">
<script defer src="event.js"></script>
<title>Document</title>
</head>
<body>
<div class="first">
<div class="second">
<div class="third"></div>
</div>
</div>
</body>
</html>
/* main.css */
.first {
width: 500px;
height: 500px;
border: 2px solid darkgreen;
}
.second {
width: 300px;
height: 300px;
border: 2px solid skyblue;
}
.third {
width: 100px;
height: 100px;
border: 2px solid purple;
}
이제 각 div에 click 이벤트 핸들러를 등록해보겠다.
// event.js
const $firstDiv = document.querySelector(".first");
const $secondDiv = document.querySelector(".second");
const $thirdDiv = document.querySelector(".third");
$firstDiv.addEventListener('click',()=>{
console.log('첫 번째 div에 클릭 이벤트 발생')
})
$secondDiv.addEventListener('click',()=>{
console.log('두 번째 div에 클릭 이벤트 발생')
})
$thirdDiv.addEventListener('click',()=>{
console.log('세 번째 div에 클릭 이벤트 발생')
})
그러고 가장 하위 div인 세 번째 div, 즉 위에서 보라색 div를 클릭해보겠다.
분명히 세 번째 div만 클릭했음에도 상위 div에도 click 이벤트 핸들러가 작동되었다.
이것이, 이벤트 버블링이다.
이해하기 쉽게 도식화 해서 알아보자
하위 노드에서 발생한 이벤트는 DOM의 최상위 노드로 계속 전달된다.
이번에는 모든 노드에 click 이벤트 핸들러를 등록하고, 가장 하위 div(보라색) 를 클릭해보겠다.
// event.js
const $firstDiv = document.querySelector(".first");
const $secondDiv = document.querySelector(".second");
const $thirdDiv = document.querySelector(".third");
const $body = document.querySelector("body");
const $html = document.querySelector("html");
$firstDiv.addEventListener('click',()=>{
console.log('첫 번째 div에 클릭 이벤트 발생')
})
$secondDiv.addEventListener('click',()=>{
console.log('두 번째 div에 클릭 이벤트 발생')
})
$thirdDiv.addEventListener('click',()=>{
console.log('세 번째 div에 클릭 이벤트 발생')
})
$body.addEventListener('click',()=>{
console.log('body에 클릭 이벤트 발생')
})
$html.addEventListener('click',()=>{
console.log('html에 클릭 이벤트 발생')
})
// document, window 는 전역 객체이므로 변수 선언을 하지 않아도 된다.
document.addEventListener('click',()=>{
console.log('document에 클릭 이벤트 발생')
})
window.addEventListener('click',()=>{
console.log('window에 클릭 이벤트 발생')
})
여기까지 이해했으면, 이벤트 버블링에 대해 충분히 이해한 것이다.
그렇다면, 이벤트 버블링을 막을 수는 없을까?
당연히 가능하다.
e.stopPropagation() API를 사용하면 상위 노드로 이벤트 전달을 막아준다.
두 번째 div(하늘색) 이벤트 리스너를 다음과 같이 수정해보겠다.
$secondDiv.addEventListener('click',(e)=>{
console.log('두 번째 div에 클릭 이벤트 발생')
e.stopPropagation()
})
그리고 이제 두 번째 div의 하위 노드인 세 번째 div(보라색) div를 클릭해보겠다.
다음 과정을 도식화 해보자.
예시는 마지막 div를 클릭하여 두 번째 div까지 이벤트가 전달되었지만,
만약 두 번째 div를 클릭했다면 콘솔창에 두 번째 div 관련해서만 출력될 것이다.
3️⃣ 이벤트 위임(delegation)
div 모든 요소에 이벤트 핸들러를 등록하고 싶은 상황이라고 가정하자.
그러면 각 div에 핸들러를 등록하여 총 3개의 핸들러가 등록된다.
// event.js
const $firstDiv = document.querySelector(".first");
const $secondDiv = document.querySelector(".second");
const $thirdDiv = document.querySelector(".third");
$firstDiv.addEventListener("click", () => {
console.log("첫 번째 div에 클릭 이벤트 발생");
});
$secondDiv.addEventListener("click", (e) => {
console.log("두 번째 div에 클릭 이벤트 발생");
e.stopPropagation();
});
$thirdDiv.addEventListener("click", () => {
console.log("세 번째 div에 클릭 이벤트 발생");
});
하지만, 이벤트 핸들러 등록 행위는 메모리를 사용하기에 많이 하면 효율성이 떨어진다.
가령, div가 100개 있다면 총 100개의 핸들러를 등록해야 된다.
성능면에서 매우 비효율적이며 번거로운 행위이다.
예시를 확인해보자. 위랑 다른 예시를 들겠다.
현재 HTML,CSS 렌더링 화면은 다음과 같다.
<!-- index.html -->
<body>
<div class="outer">
<div class="inner-1">1</div>
<div class="inner-2">2</div>
<div class="inner-3">3</div>
</div>
</body>
/* main.css */
.outer {
font-size: 40px;
text-align: center;
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: center;
width: 400px;
height: 400px;
border: 2px solid black;
}
.outer > div {
width: 100px;
height: 100px;
border: 2px solid black;
}
.skyblue{
background-color: skyblue;
}
이제 1,2,3 div에 각각 이벤트 핸들러를 등록하자.
mouse를 div에 올리면 배경색이 하늘색이 되고,
mouse가 벗어나면 배경색을 없애는 핸들러를 등록한다.
// event.js
const $firstDiv = document.querySelector(".inner-1");
const $secondDiv = document.querySelector(".inner-2");
const $thirdDiv = document.querySelector(".inner-3");
[$firstDiv, $secondDiv, $thirdDiv].forEach((div) => {
div.addEventListener("mouseover", () => {
div.classList.add("skyblue");
div.addEventListener("mouseout", () => {
div.classList.remove("skyblue");
});
});
});
for 문 안에서 mouseover ,mouseout 2개의 핸들러를 등록하므로 총 6개의 핸들러가 등록되었다.
이제 마우스를 각 div에 올리면 하늘색으로 변한다.
그러나, 이 과정은 앞서 언급했듯이 비효율적이다.
따라서, 이벤트 버블링을 이용하여 최상위 div인 outer div에만 핸들러를 등록하고 처리하는 것이
바람직하다.
이벤트 위임을 이용하면 각 요소에 핸들러를 등록할 필요가 없다.
최상위 div인 outer div에 핸들러를 등록하고
전달 받은 이벤트를 처리하면 된다.
이 과정을 도식화 하면 다음과 같다.
이제 코드로 outer div에만 핸들러를 등록하여 버블링된 이벤트를 처리해보자.
// event.js
const $outerDiv = document.querySelector(".outer");
$outerDiv.addEventListener("mouseover", (e) => {
if (e.target !== e.currentTarget) {
e.target.classList.add('skyblue')
}
});
$outerDiv.addEventListener("mouseout", (e) => {
if (e.target !== e.currentTarget) {
e.target.classList.remove('skyblue')
}
});
여기서 e.target 과 e.currentTarget 을 구분할 필요가 있다.
e.target 은 이벤트가 발생한 요소이다.
e.currentTarget 은 핸들러가 등록된 요소이다. 즉, 이 예제에서는 무조건 outer div이다.
만약 e.target == e.currentTarget 이면 outer div에서 발생한 이벤트를 처리하는 로직이 된다.
그러나, 우리는 그 하위 div에서 발생한 이벤트를 처리할 것이므로
e.target !== e.currentTarget 으로 해야 된다.
이때 e.target 은 div-1, div-2, div-3 중 하나일 것이다.
이 밖에 이벤트 위임을 활용할 때 Element.closest(선택자) 메소드도 자주 사용되니
추가적으로 알아볼 필요가 있다.
[JavaScript] Element.closest 메소드 분석
이전 포스팅에서 이벤트의 전반적인 흐름에 대해 알아보았다. [JavaScript] 이벤트 버블링(Bubbling), 이벤트 위임(Delegation)프로젝트 진행 중 DOM API 를 활용하여 이벤트를 다룰 일이 생겼다.모든 컴포
junhee1203.tistory.com
글을 요약하자면
- 이벤트는 상위 요소로 전달되며 최상위 window 노드까지 전달된다. (버블링)
- 이벤트 버블링은 e.stopPropogation() 메소드로 중단할 수 있다.
- 모든 요소에 이벤트 핸들러를 등록하는 것은 비효율적이다.
- 하위 요소에서 발생한 이벤트는 상위 요소에 이벤트를 등록하여 처리할 수 있다. (이벤트 위임)
- 이벤트 위임을 사용할 때 e.target , e.currentTarget, Element.closest(선택자) 를 자주 사용한다.
추가적으로 이벤트를 상위 요소에서 하위 요소로 전달 시킬 수도 있다.
이것을 이벤트 캡처링이라 한다.
그러나, 이벤트 버블링에 비해 사용 빈도가 낮으므로 이 글에서 다루지는 않겠다.
이벤트 버블링을 이해했다면 그 반대 과정일 뿐, 어렵지 않은 개념이다.
'JavaScript' 카테고리의 다른 글
[JavaScript] drag (드래그) 관련 이벤트 전격 분석 (0) | 2024.09.10 |
---|---|
[JavaScript] Element.closest 메소드 분석 (0) | 2024.08.31 |
[JavaScript] 모듈 시스템 CommonJS vs ES6 비교 (0) | 2024.08.25 |
[JavaScript] 콜백 함수와(Callback) 비동기(Asyncronous) 처리 (0) | 2024.08.11 |
[JavaScript] 싱글 쓰레드와 비동기(Asyncronous) (0) | 2024.07.27 |