<!DOCTYPE html>
<html lang="en">
<head>
<title>Event</title>
</head>
<body>
<div onclick="divClickHandler()">
<button type="button" id="testButton" onclick="buttonClickHandler()">click</button>
</div>
<script>
function divClickHandler() {
console.log('div clicked');
}
function buttonClickHandler() {
console.log('button clicked');
}
</script>
</body>
</html>
위와 같은 코드의 실행 결과는 어떻게 될까?
버튼을 클릭했을 때 버튼 클릭에 대한 콜백함수가 실행되고 button clicked만 출력되어야 할 것 같다.
하지만, 결과는 아래와 같이 div의 클릭 이벤트도 함꼐 실행된다.
1. 버블링과 캡처링
버블링은 이름에서 알 수 있듯이 이벤트가 버블(거품)처럼 위로 올라가는 성질이다.
위의 예시에서 button을 클릭하면 클릭 이벤트가 발생하여 button clicked가 출력된다.
이후, 클릭 이벤트는 버블링을 통해 상위 요소로 이동하며 div의 클릭 이벤트도 실행되어 div clicked가 실행된다.
캡처링은 이와 반대로 상위 요소의 이벤트가 하위 요소로 전달되는 것을 의미한다.
addEventListener의 세번째 인자로 true를 설정한 경우 캡처링을 사용할 수 있다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Event</title>
</head>
<body>
<div id="testDiv">
<button type="button" id="testButton">click</button>
</div>
<script>
const allElement = document.querySelectorAll('*');
for (let element of allElement) {
element.addEventListener('click', (e) => console.log(element.tagName), true);
}
</script>
</body>
</html>
이와 같이 캡처링을 사용하게 되면 모든 이벤트가 발생한 요소의 모든 HTML 상위 요소부터 이벤트가 내려온다.
위 코드의 실행 결과는 아래와 같다.
캡처링을 사용하지 않는 경우의 코드는 아래와 같고 결과는 반대가 될 것이다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Event</title>
</head>
<body>
<div id="testDiv">
<button type="button" id="testButton">click</button>
</div>
<script>
const allElement = document.querySelectorAll('*');
for (let element of allElement) {
element.addEventListener('click', (e) => console.log(element.tagName));
}
</script>
</body>
</html>
버블링에 비해 캡처링은 자주 사용되지 않는다고 한다.
논리적으로 생각해볼 때 문제가 발생한 부분이 있다면 해당 부분부터 문제의 범위를 넓혀가는 것이 적절할 것이다.
1-1. 버블링을 멈추는 방법
버블링이 발생하는 것을 원치 않는 경우 event.stopPropagation()
을 사용해 멈출 수 있다.
하지만, 위의 방법을 사용해 버블링을 막는 행동은 권장되지 않는다고 한다.
현재는 상위 요소에 사용되지 않는다고 해도 추후에 버블링이 필요한 경우가 발생할 수 있기 때문이다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Event</title>
</head>
<body>
<div id="testDiv">
<button type="button" id="testButton">click</button>
</div>
<script>
const allElement = document.querySelectorAll('*');
for (let element of allElement) {
element.addEventListener('click', (e) => {
event.stopPropagation();
console.log(element.tagName);
});
}
</script>
</body>
</html>
2. 데이터 속성과 이벤트 위임
그렇다면 버블링이 왜 필요한 것일까?
특정 요소를 클릭하면 해당 요소가 정해진 동작을 하는 것이 당연한 것이 아닐까?
이벤트 위임은 비슷한 방식으로 여러 요소를 다뤄야 할 때 사용된다.
이는 다시 말해 요소마다 핸들러를 등록하지 않고 요소 공통의 조상에 이벤트 핸들러를 하나만 등록해도 된다는 것이다.
유사한 기능을 하는 여러 요소에 일일이 이벤트 등록을 하지 않을 수 있다는 장점이 있다.
2-1. 데이터 속성
JavaScript에서는 데이터 속성이라는 기능을 제공한다.
데이터 속성과 버블링을 활용하면 코드의 중복을 줄일 수 있다.
아래 예시를 보자.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Event</title>
</head>
<body>
<div id="testDiv">
<button type="button" data-action="button1">click</button>
<button type="button" data-action="button2">click</button>
</div>
<script>
const buttonElements = document.getElementsByTagName('button');
console.log(buttonElements)
console.log(buttonElements[0].dataset.action)
console.log(buttonElements[1].dataset.action)
</script>
</body>
</html>
어느 요소 안에서든 data-
로 시작하는 속성을 사용할 수 있다.
이를 이용해 추가적인 정보를 요소에 담을 수 있다.
JavaScript를 이용해 이 정보에 접근하기 위해서는 dataset
속성을 사용하면 된다.
또한, JavaScript 뿐만 아니라 CSS에서도 사용할 수 있다.
button[data-action="button1"] {
background-color: red;
}
주의해야 할 점은 보여야 하거나 접근 가능해야하는 내용을 저장하면 안된다는 것이다.
접근 보조 기술이 접근할 수 없기 때문에 이를 데이터 속성에 저장하면 안된다.
또한, 검색 크롤러가 데이터 속성의 값을 찾지 못하는 문제점도 있다.
마지막으로, IE 11버전 이상은 표준을 지원하지만 이전 버전은 dataset을 지원하지 않는다.
IE 10버전 이하에서는 getAttribute()
를 통해 데이터 속성에 접근해야 한다.
2-2. 이벤트 위임
이벤트 위임은 말 그대로 "이벤트를 위임한다"는 개념이다.
어디로 위임을 하는 것일까?
앞서 보았듯이 이벤트는 버블링을 통해 상위 요소로 전파된다.
이를 활용하여 자식의 이벤트를 부모로 위임할 수 있다.
이벤트 위임을 활용하면 중복된 코드를 줄일 수 있을 것이다.
아래 예시를 보자.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Event</title>
</head>
<body>
<div id="testDiv">
<button type="button" data-action="add">추가</button>
<button type="button" data-action="delete">삭제</button>
</div>
<script>
function handleClick(event) {
console.log(event.target);
console.log(event.target.dataset.action);
}
const divElement = document.getElementById('testDiv');
divElement.addEventListener('click', handleClick);
</script>
</body>
</html>
이벤트는 각각의 버튼이 아닌 div에만 등록되어 있다.
현재는 단순히 어떤 요소가 클릭되었는지, 해당 요소의 데이터 속성이 무엇인지 출력만 하고 있지만 데이터 속성에 따른 분기처리를 한다면 더 다양한 로직을 수행할 수 있을 것이다.
2-2-1. 문제점
위의 예시에서 살펴봤듯이 event.target
을 사용하면 어떤 요소에서 이벤트가 발생했는지 확인할 수 있다.
단, 아래와 같은 경우 event.target
으로 접근한다면 안에 있는 span이 선택된다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Event</title>
</head>
<body>
<div id="testDiv">
<button type="button" data-action="add"><span>추가</span></button>
<button type="button" data-action="delete"><span>삭제</span></button>
</div>
<script>
function handleClick(event) {
console.log(event.target);
console.log(event.target.dataset.action);
}
const divElement = document.getElementById('testDiv');
divElement.addEventListener('click', handleClick);
</script>
</body>
</html>
따라서, 만약 데이터 옵션을 지정한다면 가장 하위의 자식에게 할당하는 것이 좋은 방법일 것이다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Event</title>
</head>
<body>
<div id="testDiv">
<button type="button"><span data-action="add">추가</span></button>
<button type="button" data-action="delete"><span>삭제</span></button>
</div>
<script>
function handleClick(event) {
console.log(event.target);
console.log(event.target.dataset.action);
}
const divElement = document.getElementById('testDiv');
divElement.addEventListener('click', handleClick);
</script>
</body>
</html>
또 다른 방법으로는 closest
메소드를 사용하는 방법이 있다.
closest
를 사용하면 상위 요소 중 selector와 일치하는 가장 근접한 조상 요소를 반환한다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Event</title>
</head>
<body>
<div id="testDiv">
<button type="button" data-action="add">
<span id="depth1">
<span id="depth2">
<span id="depth3">
<span id="depth4">버튼</span>
</span>
</span>
</span>
</button>
</div>
<script>
function handleClick(event) {
console.log(event.target)
}
const divElement = document.getElementById('testDiv');
divElement.addEventListener('click', handleClick);
</script>
</body>
</html>
위와 같이 버튼안에 4번 중첩된 요소가 있을 때 버튼을 클릭한다면 아래와 같은 결과가 출력된다.
function handleClick(event) {
const button = event.target.closest('button');
console.log(button)
}
기존의 handleClick
을 약간만 수정하면 원하는 결과를 얻을 수 있다.
조금 더 구체적으로 작성한다면 아래와 같이 작성할 수 있다.
function handleClick(event) {
const divElement = document.getElementById('testDiv');
const button = event.target.closest('button');
// event.target이 button 안에 없다면 아무 작업도 하지 않는다
if (!button) return;
// 해당 요소가 원하는 위치에 없다면 아무 작업도 하지 않는다
// 이 경우 divElement 안에 있는 버튼이 아닌 경우
if (!divElement.contains(button)) return;
// 원하는 작업을 수행
doSomething();
}
2-3. 예시
위의 내용을 종합한 예시 코드는 아래와 같다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Event</title>
</head>
<body>
<div id="testDiv">
<button type="button" data-action="toggle">토글</button>
<form id="testForm" hidden>
<input type="text" name="ID" id="ID" placeholder="ID 입력" />
<button type="submit">제출</button>
</form>
</div>
<script>
function handleClick(event) {
const target = event.target;
if (target.tagName !== 'BUTTON') return; // button을 클릭한 경우가 아니라면 아무 행동도 하지 않음
toggleForm();
}
function toggleForm() {
const formElement = document.getElementById('testForm');
formElement.hidden = !formElement.hidden;
}
const divElement = document.getElementById('testDiv');
divElement.addEventListener('click', handleClick);
</script>
</body>
</html>
간단한 토글에 관한 코드이며 실행 결과는 아래와 같다.
[참고자료]
'개발 > JavaScript' 카테고리의 다른 글
[JavaScript] 클로저 (0) | 2023.09.16 |
---|---|
[JavaScript] querySelectorAll VS getElementsByClassName (0) | 2023.09.10 |
[JavaScript] addEventListener과 onclick의 차이 (0) | 2023.09.03 |
[NodeJS] 싱글 스레드와 이벤트 루프 (0) | 2023.07.29 |
[JavaScript] 싱글 스레드와 비동기 (0) | 2023.07.23 |