<!DOCTYPE html>
<html>
<head>
<style>
#fruits {
display: flex;
list-style-type: none;
padding: 0;
}
#fruits li {
width: 100px;
cursor: pointer;
}
#fruits .active {
color: red;
text-decoration: underline;
}
</style>
</head>
<body>
<nav>
<ul id="fruits">
<li id="apple" class="active">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
</nav>
<div>선택된 내비게이션 아이템: <em class="msg">apple</em></div>
<script>
const $fruits = document.getElementById('fruits');
const $msg = document.querySelector('.msg');
// 사용자 클릭에 의해 선택된 내비게이션 아이템(li 요소)에 active 클래스를 추가하고
// 그 외의 모든 내비게이션 아이템의 active 클래스를 제거한다.
function activate({ target }) {
[...$fruits.children].forEach($fruit => {
$fruit.classList.toggle('active', $fruit === target);
$msg.textContent = target.id;
});
}
// 모든 내비게이션 아이템(li 요소)에 이벤트 핸들러를 등록한다.
document.getElementById('apple').onclick = activate;
document.getElementById('banana').onclick = activate;
document.getElementById('orange').onclick = activate;
</script>
</body>
</html>
- 위 예제는 모든 내비게이션 아이템에 이벤트 핸들러
activate
를 등록함. - 만약 100개라면 이벤트 핸들러는 100개, 10,000,000개라면 이벤트 핸들러는 10,000,000개가 생길 것이다.(매우 비효율적)
- 성능 저하 뿐만 아니라, 유지보수에도 매우 힘들다.
이러한 점을 해결하기 위한 방법으로 이벤트 위임
이라는 방식이 있다.
이벤트위임은 상위 DOM 요소에 이벤트를 등록하고 이벤트 전파를 통해 이벤트를 감지하여 처리하는 방식이다.
<!DOCTYPE html>
<html>
<head>
<style>
#fruits {
display: flex;
list-style-type: none;
padding: 0;
}
#fruits li {
width: 100px;
cursor: pointer;
}
#fruits .active {
color: red;
text-decoration: underline;
}
</style>
</head>
<body>
<nav>
<ul id="fruits">
<li id="apple" class="active">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
</nav>
<div>선택된 내비게이션 아이템: <em class="msg">apple</em></div>
<script>
const $fruits = document.getElementById('fruits');
const $msg = document.querySelector('.msg');
// 사용자 클릭에 의해 선택된 내비게이션 아이템(li 요소)에 active 클래스를 추가하고
// 그 외의 모든 내비게이션 아이템의 active 클래스를 제거한다.
function activate({ target }) {
// 이벤트를 발생시킨 요소(target)가 ul#fruits의 자식 요소가 아니라면 무시한다.
if (!target.matches('#fruits > li')) return;
[...$fruits.children].forEach($fruit => {
$fruit.classList.toggle('active', $fruit === target);
$msg.textContent = target.id;
});
}
// 이벤트 위임: 상위 요소(ul#fruits)는 하위 요소의 이벤트를 캐치할 수 있다.
$fruits.onclick = activate;
</script>
</body>
</html>
- 이벤트 위임을 통해 하위 DOM 요소에서 발생한 이벤트를 처리할 때 주의할 점은 상위 요소에 이벤트 핸들러를 등록하기 때문에, 실제로 발생시킨 DOM 요소가 기대한 DOM 요소가 아닐 수 있으므로 확인이 필요하다.
Element.prototype.matches
메서드로 전달된 선택자에 의해 특정 노드를 탐색 가능한지 확인할 수 있다.
function activate({ target }) {
// 이벤트를 발생시킨 요소(target)이 ul#fruits의 자식 요소가 아니라면 무시한다.
if (!target.matches('#fruits > li')) return;
...
일반적으로 이벤트 객체의 target 프로퍼티와 currentTarget 프로퍼티는 동일한 DOM 요소를 가리키지만, 이벤트 위임을 통해 상위 DOM 요소에 이벤트를 바인딩한 경우 target !== currentTaget
일 수도 있다.
$fruits.onclick = activate;
이 때, 이벤트 객체의 currentTarget 프로퍼티는 언제나 $fruits
를 가리키지만 target 프로퍼티는 이벤트를 발생시킨 DOM 요소를 가리킨다.
-
DOM 요소는 저마다 기본 동작을 가지고 있는데, 이벤트 객체의 preventDefault 메서드는 이러한 DOM 요소의 기본 동작을 중단시킨다.
<!DOCTYPE html> <html> <body> <a href="https://www.google.com">go</a> <input type="checkbox"> <script> document.querySelector('a').onclick = e => { // a 요소의 기본 동작을 중단한다. e.preventDefault(); }; document.querySelector('input[type=checkbox]').onclick = e => { // checkbox 요소의 기본 동작을 중단한다. e.preventDefault(); }; </script> </body> </html>
<!DOCTYPE html>
<html>
<body>
<div class="container">
<button class="btn1">Button 1</button>
<button class="btn2">Button 2</button>
<button class="btn3">Button 3</button>
</div>
<script>
// 이벤트 위임. 클릭된 하위 버튼 요소의 color를 변경한다.
document.querySelector('.container').onclick = ({ target }) => {
if (!target.matches('.container > button')) return;
target.style.color = 'red';
};
// .btn2 요소는 이벤트를 전파하지 않으므로 상위 요소에서 이벤트를 캐치할 수 없다.
document.querySelector('.btn2').onclick = e => {
e.stopPropagation(); // 이벤트 전파 중단
e.target.style.color = 'blue';
};
</script>
</body>
</html>
- 상위 DOM 요소인 container 요소에 click 이벤트를 위임했다. 따라서 하위 요소에서 발생한 이벤트를 container가 캐치하여 처리한다.
- 하지만, btn2 요소는 stopPropagation을 통해 이벤트 전파를 중단시킨다. 따라서 container는 btn2의 이벤트를 감지할 수 없다.
<!DOCTYPE html>
<html>
<body>
<button onclick="handleClick()">Click me</button>
<script>
function handleClick() {
console.log(this); // window
}
</script>
</body>
</html>
-
handleClick 함수는 이벤트 핸들러에 의해 일반 함수로 호출된다. 따라서 일반 함수 내부의 this는 window를 가리킨다.
-
단, 이벤트 핸들러를 호출할때 인수로 전달한 this는 이벤트를 바인딩한 DOM 요소를 가리킨다.
<!DOCTYPE html> <html> <body> <button onclick="handleClick(this)">Click me</button> <script> function handleClick(button) { console.log(button); // 이벤트를 바인딩한 button 요소 console.log(this); // window } </script> </body> </html>
-
이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식 모두 이벤트 핸들러 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리킨다. 즉, 이벤트 핸들러 내부의 this는 이벤트 객체의 currentTarget 프로퍼티와 같다.
<!DOCTYPE html> <html> <body> <button class="btn1">0</button> <button class="btn2">0</button> <script> const $button1 = document.querySelector('.btn1'); const $button2 = document.querySelector('.btn2'); // 이벤트 핸들러 프로퍼티 방식 $button1.onclick = function (e) { // this는 이벤트를 바인딩한 DOM 요소를 가리킨다. console.log(this); // $button1 console.log(e.currentTarget); // $button1 console.log(this === e.currentTarget); // true // $button1의 textContent를 1 증가시킨다. ++this.textContent; }; // addEventListener 메서드 방식 $button2.addEventListener('click', function (e) { // this는 이벤트를 바인딩한 DOM 요소를 가리킨다. console.log(this); // $button2 console.log(e.currentTarget); // $button2 console.log(this === e.currentTarget); // true // $button2의 textContent를 1 증가시킨다. ++this.textContent; }); </script> </body> </html>
-
화살표 함수로 정의한 이벤트 핸들러 내부의 this는 상위 스코프의 this를 가리킨다. 화살표 함수는 자체 this 바인딩을 갖지 않는다.
<!DOCTYPE html> <html> <body> <button class="btn1">0</button> <button class="btn2">0</button> <script> const $button1 = document.querySelector('.btn1'); const $button2 = document.querySelector('.btn2'); // 이벤트 핸들러 프로퍼티 방식 $button1.onclick = e => { // 화살표 함수 내부의 this는 상위 스코프의 this를 가리킨다. console.log(this); // window console.log(e.currentTarget); // $button1 console.log(this === e.currentTarget); // false // this는 window를 가리키므로 window.textContent에 NaN(undefined + 1)을 할당한다. ++this.textContent; }; // addEventListener 메서드 방식 $button2.addEventListener('click', e => { // 화살표 함수 내부의 this는 상위 스코프의 this를 가리킨다. console.log(this); // window console.log(e.currentTarget); // $button2 console.log(this === e.currentTarget); // false // this는 window를 가리키므로 window.textContent에 NaN(undefined + 1)을 할당한다. ++this.textContent; }); </script> </body> </html>
-
클래스에서 이벤트 핸들러를 바인딩하는 경우 this에 주의해야 한다.
<!DOCTYPE html> <html> <body> <button class="btn">0</button> <script> class App { constructor() { this.$button = document.querySelector('.btn'); this.count = 0; // increase 메서드를 이벤트 핸들러로 등록 this.$button.onclick = this.increase; } increase() { // 이벤트 핸들러 increase 내부의 this는 DOM 요소(this.$button)를 가리킨다. // 따라서 this.$button은 this.$button.$button과 같다. this.$button.textContent = ++this.count; // -> TypeError: Cannot set property 'textContent' of undefined } } new App(); </script> </body> </html>
- 위 예제는 increase 메서드 내부의 this는 클래스가 생성할 인스턴스를 가리키지 않는다. 이벤트 핸들러 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리키기 때문에, increase 메서드 내부의 this는 this.$button을 가리킨다.
- 따라서 increase 메서드를 이벤트 핸들러로 바인딩할 때 bind 메서드를 사용해 this를 전달하여 increase 메서드 내부의 this가 클래스가 생성할 인스턴스를 가리키도록 해야 한다.
<!DOCTYPE html> <html> <body> <button class="btn">0</button> <script> class App { constructor() { this.$button = document.querySelector('.btn'); this.count = 0; // increase 메서드를 이벤트 핸들러로 등록 // this.$button.onclick = this.increase; // increase 메서드 내부의 this가 인스턴스를 가리키도록 한다. this.$button.onclick = this.increase.bind(this); } increase() { this.$button.textContent = ++this.count; } } new App(); </script> </body> </html>
-
또는 클래스 필드에 할당한 화살표 함수를 이벤트 핸들러로 등록하여 이벤트 핸들러 내부의 this가 인스턴스를 가리키도록 할 수도 있다. 다만 이때 이벤트 핸들러 increase는 프로토타입 메서드가 아닌 인스턴스 메서드가 된다.
<!DOCTYPE html> <html> <body> <button class="btn">0</button> <script> class App { constructor() { this.$button = document.querySelector('.btn'); this.count = 0; // 화살표 함수인 increase를 이벤트 핸들러로 등록 this.$button.onclick = this.increase; } // 클래스 필드 정의 // increase는 인스턴스 메서드이며 내부의 this는 인스턴스를 가리킨다. increase = () => this.$button.textContent = ++this.count; } new App(); </script> </body> </html>
-
함수에 인수를 전달하려면 함수를 호출할 때 전달해야 한다. 이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식의 경우 이벤트 핸들러를 브라우저가 호출하기 때문에 함수 자체를 등록해야 한다. 따라서 인수를 전달할 수 없지만, 방법이 전혀 없는 것이 아니다.
<!DOCTYPE html> <html> <body> <label>User name <input type='text'></label> <em class="message"></em> <script> const MIN_USER_NAME_LENGTH = 5; // 이름 최소 길이 const $input = document.querySelector('input[type=text]'); const $msg = document.querySelector('.message'); const checkUserNameLength = min => { $msg.textContent = $input.value.length < min ? `이름은 ${min}자 이상 입력해 주세요` : ''; }; // 이벤트 핸들러 내부에서 함수를 호출하면서 인수를 전달한다. $input.onblur = () => { checkUserNameLength(MIN_USER_NAME_LENGTH); }; </script> </body> </html>
-
또는 이벤트 핸들러를 반환하는 함수를 호출하면서 인수를 전달할 수 있다.
<!DOCTYPE html> <html> <body> <label>User name <input type='text'></label> <em class="message"></em> <script> const MIN_USER_NAME_LENGTH = 5; // 이름 최소 길이 const $input = document.querySelector('input[type=text]'); const $msg = document.querySelector('.message'); // 이벤트 핸들러를 반환하는 함수 const checkUserNameLength = min => e => { $msg.textContent = $input.value.length < min ? `이름은 ${min}자 이상 입력해 주세요` : ''; }; // 이벤트 핸들러를 반환하는 함수를 호출하면서 인수를 전달한다. $input.onblur = checkUserNameLength(MIN_USER_NAME_LENGTH); </script> </body> </html>
-
Event, UIEvent, MouseEvent
같은 이벤트 생성자 함수를 호출하여 명시적으로 생성한 이벤트 객체는 임의의 이벤트 타입을 지정할 수 있다. 이처럼 개발자의 의도로 생성된 이벤트를 커스텀 이벤트라고 한다. -
이벤트 생성자 함수는 첫 번째 인수로 이벤트 타입을 나타내는 문자열을 전달받는다. 이벤트 타입을 나타내는 문자열은 기존 이벤트 타입을 사용할 수도 있고, 기존 이벤트 타입이 아닌 임의의 문자열을 사용할 수도 있다.
// KeyboardEvent 생성자 함수로 keyup 이벤트 타입의 커스텀 이벤트 객체를 생성 const keyboardEvent = new KeyboardEvent('keyup'); console.log(keyboardEvent.type); // keyup // CustomEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체를 생성 const customEvent = new CustomEvent('foo'); console.log(customEvent.type); // foo
-
생성된 커스텀 이벤트 객체는 버블링되지 않으며 preventDefault 메서드로 취소할 수도 없다. 즉 커스텀 이벤트 객체는 bubbles와 cancelable 프로퍼티의 값이 false로 기본 설정된다.
// MouseEvent 생성자 함수로 click 이벤트 타입의 커스텀 이벤트 객체를 생성 const customEvent = new MouseEvent('click'); console.log(customEvent.type); // click console.log(customEvent.bubbles); // false console.log(customEvent.cancelable); // false
-
커스텀 이벤트 객체의 bubbles 또는 cancelable 프로퍼티를 true로 설정하려면 이벤트 생성자 함수의 두 번째 인수로 bubbles 또는 cancelable 프로퍼티를 갖는 객체를 전달한다.
// MouseEvent 생성자 함수로 click 이벤트 타입의 커스텀 이벤트 객체를 생성 const customEvent = new MouseEvent('click', { bubbles: true, cancelable: true }); console.log(customEvent.bubbles); // true console.log(customEvent.cancelable); // true
-
커스텀 이벤트 객체에는 bubbles 또는 cancelable 프로퍼티뿐만 아니라 이벤트 타입에 따라 가지는 이벤트 고유의 프로퍼티 값을 지정할 수 있다. 예를 들어, MouseEvent 생성자 함수로 생성한 마우스 이벤트 객체는 마우스 포인터의 좌표 정보를 나타내는 마우스 이벤트 객체 고유의 프로퍼티 screenX/screenY, clientX/clientY, pageX/pageY, offsetX/offsetY와 버튼 정보를 나타내는 프로퍼티 altKey, ctrlKey, shiftKey, button을 갖는다. 이러한 이벤트 객체 고유의 프로퍼티 값을 지정하려면 다음과 같이 이벤트 생성자 함수의 두 번째 인수로 프로퍼티를 갖는다.
// MouseEvent 생성자 함수로 click 이벤트 타입의 커스텀 이벤트 객체를 생성 const mouseEvent = new MouseEvent('click', { bubbles: true, cancelable: true, clientX: 50, clientY: 100 }); console.log(mouseEvent.clientX); // 50 console.log(mouseEvent.clientY); // 100 // KeyboardEvent 생성자 함수로 keyup 이벤트 타입의 커스텀 이벤트 객체를 생성 const keyboardEvent = new KeyboardEvent('keyup', { key: 'Enter' }); console.log(keyboardEvent.key); // Enter
-
이벤트 생성자 함수로 생성한 커스텀 이벤트는 isTrusted 프로퍼티 값이 언제나 false다. 커스텀 이벤트가 아닌 사용자의 행위에 의해 발생한 이벤트에 의해 생성된 이벤트 객체의 isTrusted 프로퍼티 값은 언제나 true다.
// InputEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체를 생성 const customEvent = new InputEvent('foo'); console.log(customEvent.isTrusted); // false
-
생성된 커스텀 이벤트는 dispatchEvent 메서드로 디스패치(dispatch: 이벤트를 발생시키는 행위) 할 수 있다.
-
dispatchEvent 메서드에 이벤트 객체를 인수로 전달하면서 호출하면 인수로 전달한 이벤트 타입의 이벤트가 발생한다.
<!DOCTYPE html> <html> <body> <button class="btn">Click me</button> <script> const $button = document.querySelector('.btn'); // 버튼 요소에 click 커스텀 이벤트 핸들러를 등록 // 커스텀 이벤트를 디스패치하기 이전에 이벤트 핸들러를 등록해야 한다. $button.addEventListener('click', e => { console.log(e); // MouseEvent {isTrusted: false, screenX: 0, ...} alert(`${e} Clicked!`); }); // 커스텀 이벤트 생성 const customEvent = new MouseEvent('click'); // 커스텀 이벤트 디스패치(동기 처리). click 이벤트가 발생한다. $button.dispatchEvent(customEvent); </script> </body> </html>
- 일반적으로 이벤트 핸들러는 비동기처리 방식으로 동작하지만 dispatchEvent 메서드는 이벤트 핸들러를 동기처리 방식으로 호출한다. 다시 말해, dispatchEvent 메서드를 호출하면 커스텀 이벤트에 바인딩된 이벤트 핸들러를 직접 호출하는 것과 같다.
-
기존 이벤트 타입이 아닌 임의의 이벤트 타입을 지정하여 이벤트 객체를 생성하는 경우 일반적으로 CustomEvent 이벤트 생성자 함수를 사용한다.
// CustomEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체를 생성 const customEvent = new CustomEvent('foo'); console.log(customEvent.type); // foo
-
이때 CustomEvent 이벤트 생성자 함수에는 두 번째 인수로 이벤트와 함께 전달하고 싶은 정보를 담은 detail 프로퍼티를 포함하는 객체를 전달할 수 있다. 이 정보는 이벤트 객체의 detail 프로퍼티(e.detail)에 담겨 전달된다.
<!DOCTYPE html> <html> <body> <button class="btn">Click me</button> <script> const $button = document.querySelector('.btn'); // 버튼 요소에 foo 커스텀 이벤트 핸들러를 등록 // 커스텀 이벤트를 디스패치하기 이전에 이벤트 핸들러를 등록해야 한다. $button.addEventListener('foo', e => { // e.detail에는 CustomEvent 함수의 두 번째 인수로 전달한 정보가 담겨 있다. alert(e.detail.message); }); // CustomEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체를 생성 const customEvent = new CustomEvent('foo', { detail: { message: 'Hello' } // 이벤트와 함께 전달하고 싶은 정보 }); // 커스텀 이벤트 디스패치 $button.dispatchEvent(customEvent); </script> </body> </html>
- 기존 이벤트 타입이 아닌 임의의 이벤트 타입을 지정하여 커스텀 이벤트 객체를 생성한 경우 반드시 addEventListener 메서드 방식으로 이벤트 핸들러를 등록해야 한다.
- 그 이유는,
'on + 이벤트 타입'
으로 이루어진 이벤트 핸들러 어트리뷰트/프로퍼티가 요소 노드에 존재하지 않기 때문이다.