40.6 이벤트 전파
이벤트 전파(event propagation)
- DOM 트리 상에 존재하는 DOM 요소 노드에서 발생하는 이벤트는 DOM 트리를 통해 전파
- 생성된 이벤트 객체는 이벤트를 발생시킨 DOM 요소의 이벤트 타깃(event target)을 중심으로 DOM 트리를 통해 전파
- 이벤트는 이벤트를 발생시킨 이벤트 타깃은 물론 상위 DOM 요소에서도 캐치 가능
- DOM 트리를 통해 전파되는 이벤트는 이벤트 패스에 위치한 모든 DOM 요소에서 캐치 가능
캡처링 단계 (capturing phase)
- 이벤트가 상위 요소에서 하위 요소 방향으로 전파
타깃 단계 (target phase)
- 이벤트가 이벤트 타깃에 도달
버블링 단계(bubbling phase)
- 이벤트가 하위 요소에서 상위 요소 방향으로 전파
ex)
- ul 요소에 이벤트 핸들러를 바인딩하고, ul 요소의 하위 요소인 li 요소를 클릭하여 이벤트 발생
- 이벤트 타깃(event target)은 li 요소, 커런트 타깃(event.currentTarget)은 ul 요소
<!DOCTYPE html>
<html>
<body>
<ul id = "fruits">
<li id = "apple"> Apple</li>
<li id = "banana"> Banana</li>
<li id = "orange"> Orange</li>
</ul>
<script>
const $fruits = document.getElementById('fruits');
//#fruits 요소의 하위 요소인 li 요소를 클릭한 경우
$fruits.addEventListener('click', e => {
console.log(`이벤트 단계 : ${e.eventPhase}`); //3: 버블링 단계
console.log(`이벤트 타깃 : ${e.target}`); // [Object HTMLIElement]
console.log(`커런트 타깃 : ${e.currentTarget}`); // [object HTMLUListElment]
});
</script>
</body>
</html>
- li 요소를 클릭하면 클릭 이벤트가 발생하여 클릭 이벤트 객체가 생성되고 클릭된 li 요소가 이벤트 타깃이 됨
- 클릭 이벤트 객체는 window에서 시작해서 이벤트 타깃 방향으로 전파 (캡처링 단계)
- 이벤트 객체는 이벤트를 발생시킨 이벤트 타깃에 도달 ( 타깃 단계)
- 이벤트 객체는 이벤트 타깃에서 시작해서 window 방향으로 전파 (버블링 단계)
- 이벤트 핸들러 어트리뷰트/프로퍼티 방식으로 등록한 이벤트 핸들러는 타깃 단계와 버블링 단계의 이벤트만 캐치 가능
- addEventListener 메서드 방식으로 등록한 이벤트 핸들러는 타깃 단계와 버블링 단계 뿐만 아니라 캡처링 단계 이벤트도 선별적으로 캐치
<!DOCTYPE html>
<html>
<body>
<ul id = "fruits">
<li id = "apple"> Apple</li>
<li id = "banana"> Banana</li>
<li id = "orange"> Orange</li>
</ul>
<script>
const $fruits = document.getElementById('fruits');
const $banana = document.getElementById('banana');
//#fruits 요소의 하위 요소인 li 요소를 클릭한 경우 캡처링 단계의 이벤트를 캐치
$fruits.addEventListener('click', e => {
console.log(`이벤트 단계 : ${e.eventPhase}`); //1: 캡처링 단계
console.log(`이벤트 타깃: ${e.target}`); // [object HTMLLIElement]
console.log(`커런트 타깃: ${e.currentTarget}`); // [object HTMLUListElement]
});
//타깃 단계의 이벤트를 캐치
$banana.addEventListener('click', e => {
console.log(`이벤트 단계 : ${e.eventPhase}`); //2.타깃 단계
console.log(`이벤트 타깃: ${e.target}`); // [object HTMLLIElement]
console.log(`커런트 타깃: ${e.currentTarget}`); // [object HTMLLIElement]
});
//버블링 단계의 이벤트를 캐치
$fruits.addEventListener('click', e => {
console.log(`이벤트 단계 : ${e.eventPhase}`); //3: 버블링 단계
console.log(`이벤트 타깃: ${e.target}`); // [object HTMLLIElement]
console.log(`커런트 타깃: ${e.currentTarget}`); // [object HTMLUListElement]
});
</script>
</body>
</html>
- 캡처링 단계의 이벤트와 버블링 단계의 이벤트를 캐치하는 이벤트 핸들러가 혼용되는 경우
<!DOCTYPE html>
<html>
<head>
<style>
html, body { height : 100%; }
</style>
</head>
<body>
<p>버블링과 캡처링 이벤트 <button>버튼</button></p>
<script>
//버블링 단계의 이벤트를 캐치
document.body.addEventListenr('click', () => {
console.log('Handler for body');
});
//캡처링 단계의 이벤트를 캐치
document.querySelector('p').addEventListener('click', () => {
console.log('Handler for paragraph');},true);
//타깃 단계의 이벤트를 캐치
document.querySelector('button').addEventListener('click', () => {
console.log('Handler for button');
});
</script>
</body>
</html>
- body 요소는 버블링 단게의 이벤트만을 캐치하고, p 요소는 캡처링 단계의 이벤트만 캐치
- 이벤트는 캡처링 - 타깃 - 버블링 단계로 전파되므로
1) button 요소에서 클릭 이벤트 발생
- 캡처링 단계를 캐치하는 p 요소의 이벤트핸들러가 호출
- 버블링 단계의 이벤트를 캐치하는 body 요소의 이벤트가 단계적으로 호출
Handler for paragraph
Handler for button
Handler for body
2) p 요소에서 클릭 이벤트 발생
- 캡처링 단계를 캐치하는 p 요소의 이벤트 핸들러가 호출
- 버블링 단계를 캐치하는 body 요소의 이벤트가 순차적으로 호출
Handler for paragraph
Handler for body
40.7 이벤트 위임
이벤트 위임 (event delegation)
- 여러 개의 하위 DOM 요소에 각각 이벤트 핸들러를 등록하는 대신 하나의 DOM 요소에 이벤트 핸들러를 등록
- 이벤트 위임을 통해 상위 DOM 요소에 이벤트 핸들러를 등록하면 여러 개의 하위 DOM 요소에 이벤트 핸들러를 등록할 필요가 없음
ex)
- 사용자가 네비게이션 아이템(li 요소)을 클릭하여 선택하면 현재 선택된 네비게이션 아이템에 active 클래스를 추가하고 그 외의 모든 네비게이션의 아이템의 active 클래스는 제거
<!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 = "orage">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($fruits => {
$fruits.classList.toggle('active', $fruits === target);
$msg.textContent = target.id;
});
}
//이벤트 위임: 상위 요소(ul#fruits)는 하위 요소의 이벤트를 캐치할 수 있다
$fruits.onclick = activate;
</script>
</body>
</html>
- 이벤트 위임을 통해 상위 DOM 요소에 이벤트를 바인딩한 경우 이벤트 객체의 target 프로퍼티와 currentTarget 프로퍼티가 다른 DOM 요소를 가리킬 수 있음
40.8 DOM 요소의 기본 동작 조작
40.8.1 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>
40.8.2 이벤트 전파 방지
- 이벤트 객체의 stopPropagation 메서드는 이벤트 전파를 중지시킴
ex)
- 상위 DOM 요소인 container 요소에 이벤트 위임
- 하위 DOM 요소에서 발생한 클릭 이벤트를 상위 DOM 요소인 container 요소가 캐치하여 이벤트 처리
- 하위 요소중에서 btn2 요소는 자체적으로 이벤트 처리
- btn2 요소는 자신이 발생시킨 이벤트가 전파되는 것을 중단하여 자신에게 바인딩된 이벤트 핸들러만 실행되도록 함
<!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>
40.9 이벤트 핸들러 내부의 this
40.9.1 이벤트 핸들러 어트리뷰트 방식
- handleClick 함수 내부의 this는 전역 객체 window를 가리킴
- handleClick 함수는 이벤트 핸들러에 의해 일반 함수로 호출
- 일반 함수로서 호출되는 함수 내부의 this는 전역객체를 가리킴
<!DOCTYPE html>
<html>
<body>
<button onclick="handleClick()">Click me</button>
<script>
function handleClick() {
console.log(this); //window
}
</script>
</body>
</html>
- 이벤트 핸들러를 호출할 때 인수로 전달한 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>
40.9.2 이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식
- 이벤트 핸들러 프로퍼티 방식과 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는 이벤트를 바인딩한 DOM 요소를 가리킴
console.log(this); //window
console.log(e.currentTarget); //$button1
console.log(this === e.currentTarget); //false
}
//this는 window를 가리키므로 window.textContent에 NaN을 할당
++this.textContent;
//addEventListener 메서드 방식
$button2.addEventListener('click', e => {
//this는 이벤트를 바인딩한 DOM 요소를 가리킴
console.log(this); //window
console.log(e.currentTarget); //$button2
console.log(this === e.currentTarget); //false
});
//this는 window를 가리키므로 window.textContent에 NaN을 할당
++this.textContent;
</script>
</body>
</html>
- 클래스에서 이벤트 핸들러를 바인딩하는경우 this에 주의
-> 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가 인스턴스를 가리키도록
this.$button.onclick = this.increae.bind(this);
}
increase() {
this.$button.textContent = ++this.count;
}
}
</script>
</body>
</html>
40.10 이벤트 핸들러에 인수 전달
- 이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식의 경우 이벤트 핸들러를 브라우저가 호출하기 떄문에 함수 호출문이 아닌 함수자체를 등록 -> 인수 전달 불가
1) 이벤트 핸들러 내부에서 함수를 호출하면서 인수 전달 가능
<!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>
2) 이벤트 핸들러를 반환하는 함수를 호출하면서 인수 전달 가능
<!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>
40.11 커스텀 이벤트
40.11.1 커스텀 이벤트 생성
- Event,UIEvent,MouseEvent 같은 이벤트 생성자 함수를 호출하여 명시적으로 생성한 이벤트 객체는 임의의 이벤트 타입을 지정할 수 있음
- 개발자의 의도로 생성된 이벤트를 커스텀 이벤트라 함
- 이벤트 생성자 함수는 첫 번째 인수로 이벤트 타입을 나타내는 문자열 전달받음
1) 기존 이벤트 타입 사용
//KeyboardEvent 생성자 함수로 keyup 이벤트 타입의 커스텀 이벤트 객체를 생성
const keyboardEvent = new KeyboardEvent('keyup');
console.log(KeyboardEvent.type); //keyup
2) 기존 이벤트 타입이 아닌 임의의 문자열 사용
- CustomEvent 이벤트 생성자 함수 사용
//CustomEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체 생성
const customEvent = new CustomEvent('foo');
console.log(customEvent.type); //foo
- 생성된 커스텀 이벤트 객체는 버블링 되지 않으며, preventDefault 메서드로 취소할 수 없음
- 이벤트 생성자 함수로 생성한 커스텀 이벤트는 isTrusted 프로퍼티의 값이 언제나 false임
40.11.2 커스텀 이벤트 디스패치
- 커스텀 이벤트 객체를 생성한 후 커스텀 이벤트를 사용하려면 이벤트 객체를 전달하는 과정 필요
- 생성된 커스텀 이벤트는 dispatchEvent 메서드로 디스패치(dispatch, 이벤트를 발생시키는 행위)할 수 있음
- dispatchEvent 메서드에 이벤트 객체를 인수로 전달하면서 호출하면 인수로 전달한 이벤트 타입의 이벤트가 발생
- 일반적으로 이벤트 핸들러는 비동기(asynchronous) 처리 방식으로 동작하지만 dispatchEvent 메서드는 이벤트 핸들러를 동기(Synchronous)처리 방식으로 호출
- dispatchEvent 메서드를 호출하면 커스텀 이벤트에 바인딩된 이벤트 핸들러를 직접 호출하는 것과 같음
-> 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,...}
alert(`${e} Clicked!`);
});
//커스텀 이벤트 생성
const customEvent = new MouseEvent('click');
//커스텀 이벤트 디스패치(동기 처리). click 이벤트가 발생
$button.dispatchEvent(customEvent);
</script>
</body>
</html>
- CustomEvent 이벤트 생성자 함수에는 두 번째 인수로 이벤트와 함께 전달하고 싶은 정보를 담은 detail 프로퍼티를 포함하는 객체 전달 가능
- 이 정보는 이벤트 객체의 detail 프로퍼티(e.detail)에 담겨 전달
- 기존 이벤트 타입이 아닌 임의의 이벤트 타입을 지정하여 커스텀 이벤트 객체를 생성한 경우 반드시 addEventListener 메서드 방식으로 이벤트 핸들러를 등록해야함
-> 'onfoo'라는 핸들러 어트리뷰트/프로퍼티가 요소 노드에 존재하지 않기 떄문에 이벤트 핸들러 어트리뷰트/프로퍼티 방식으로는 이벤트 핸들러를 등록할 수 없음
<!DOCTYPE html>
<html>
<body>
<button class="btn">Click me</button>
<script>
const $button = document.querySelector('.btn');
//버튼 요소에 foo 커스텀 이벤트 핸들러 등록
//커스텀 이벤트를 디스패치하기 이전에 이벤트 핸들러를 등록해야 함
$button.addEventListener('foo', e => {
//e.detail에는 CustomeEvent 함수의 두 번째 인수로 전달한 정보가 담겨있음
alert(e.detail.message);
});
//CustomEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체를 생성
const customEvent = new CustomEvent('foo', {
detail: {message:'Hello'} //이벤트와 함께 전달하고 싶은 정보
});
//커스텀 이벤트 디스패치
$button.dispatchEvent(customEvent);
</script>
</body>
</html>
'JavaScript' 카테고리의 다른 글
[Deep dive] 42장 비동기 프로그래밍 (0) | 2023.08.23 |
---|---|
[Deep dive] 41장 타이머 (0) | 2023.08.22 |
[Deep dive] 40장 이벤트(1) (2) | 2023.08.22 |
[Deep dive] 39장 DOM (3) (0) | 2023.08.21 |
[Deep dive] 39장 DOM (2) (0) | 2023.08.21 |