본 글은 자바스크립트 스터디를 진행하면서 [모던자바스크립트 DeepDive] 책을 바탕으로 작성된 글입니다.
이벤트 드리븐 프로그래밍
- 브라우저는 스스로 감지해서 특정 사건이 발생하면 해당 이벤트를 발생시킨다.
- 특정 이벤트에 대해 어떤 일을 하고 싶다면 우리는 브라우저에게 특정 이벤트가 발생 했을 때 호출될 함수를 알려주어 브라우저에게 호출을 위임한다
- 왜냐하면 우리는 특정 이벤트가 언제 발생할지 알 수 없으므로, 이벤트를 감지할 수 있는 브라우저에게 전적으로 위임하는 것
- 이벤트 핸들러 : 이벤트가 발생했을 때 호출될 함수
- 이벤트 핸들러 등록 : 브라우저에게 이벤트 핸들러의 호출을 위임하는 것
- 이벤트와 이벤트 핸들러를 통해 사용자와 애플리케이션은 상호작용한다
- 프로그램의 흐름을 이벤트 중심으로 제어하는 프로그램이 방식을 이벤트 드리븐 프로그래밍 이라고 한다.
이벤트 타입
- 이벤트의 종류를 나타내는 문자열
이벤트 핸들러 등록
- 이벤트 핸들러를 등록하는 방법은 총 3가지
1️⃣ 이벤트 핸들러 어트리뷰트 방식
- 이벤트 핸들러 어트리뷰트 이름 : on + 이벤트 타입 ex) onchange
- 이벤트 핸들러 어트리뷰트에 함수 호출문을 할당하면 이벤트 핸들러가 등록된다.
<!DOCTYPE html>
<html>
<body>
<button onclick="handleClick()">버튼</button>
<script>
handleClick = () => console.log('Click');
</script>
</body>
</html>
- 함수 호출문을 할당하면 함수 호출문의 평가 결과가 이벤트 핸들러로 등록된다.
- 이 때 어트리뷰트의 값인 handleClick()은 암묵적으로 생성될 이벤트 핸들러의 함수 몸체를 의미
- onclick="handleClick( )" 어트리뷰트는 파싱되어 암묵적으로 함수를 생성한 뒤 , 해당 요소의 onclick 이벤트 핸들러 프로퍼티(이벤트 핸들러 어트리뷰트의 이름과 동일한 )에 암묵적으로 생성한 함수를 할당함
function onclick(){
console.log('Click');
}
// 이런 함수가 암묵적으로 생성이 되고 요소의 onclick 프로퍼티에 바인딩
- 이렇게 동작하는 이유는 이벤트 핸들러에 인수를 전달하기 위해서
// 인수를 전달할 수 없다
<button onclick="handleClick">버튼</button>
- 바닐라 자바스크립트에서는 이벤트 핸들러 어트리뷰트 방식을 사용하지 않는 것이 좋다.
관심사가 다른 HTML과 자바스크립트를 혼용하기 때문에 - 하지만 React/Vue/Angular/Svelte/의 경우 이벤트 핸들러 어트리뷰트 방식으로 이벤트를 처리한다.
Component Based Development 방식에서는 HTML,CSS,자바스크립트 모두 뷰를 구성하기 위한 구성요소로 보기 때문에 관심사가 같다고 봄
{ /* React */ }
<button onClick={handleClick}>버튼</button>
2️⃣ 이벤트 핸들러 프로퍼티 방식
- window 객체, Document, HTMLElement 타입의 DOM 노드 객체에는 이벤트 핸들러 프로퍼티가 있다.
- 이벤트 핸들러 프로퍼티 키 : on + 이벤트타입 ex) onclick
- 이벤트 핸들러 프로퍼티에 함수를 바인딩하면 이벤트 핸들러가 등록된다.
<!DOCTYPE html>
<html>
<body>
<button>버튼</button>
<script>
const $btn = document.querySelector('button');
$btn.onclick = () => console.log('click!');
</script>
</body>
</html>
이벤트 타깃(이벤트를 발생시킬 객체)은 button 요소
이벤트 타입은 'click'
- 이벤트 핸들러는 대부분 이벤트를 발생시킬 이벤트 타깃에 바인딩 하지만
- 이벤트 위임의 경우 해당 이벤트 흐름에 있는 모든 DOM 요소에서 이벤트를 캐치할 수 있기 때문에 이벤트 타깃이 아닌 상위 DOM 요소에 이벤트 핸들러를 바인딩한다.
- 이벤트 핸들러 프로퍼티에는 단 하나의 이벤트 핸들러만 바인딩 가능
<!DOCTYPE html>
<html>
<body>
<button>버튼</button>
<script>
const $btn = document.querySelector('button');
$btn.onclick = () => console.log('click!'); // 실행 X
$btn.onclick = () => console.log('재할당 click!');
</script>
</body>
</html>
3️⃣ addEventListener 메서드 방식
- DOM Level 2 에서 도입
- EventTarget.prototype.addEventListener 메서드
- EventTarget.addEventListener('이벤트 타입', 이벤트 핸들러[,true,false])
1번째 인수인 이벤트 타입은 on 없이 ex) click
3번째 인수는 이벤트를 캐치할 전파 단계를 설정
- false,생략 : 버블링 단계에서 이벤트 캐치
- true : 캡처링 단계에서 이벤트 캐치
<!DOCTYPE html>
<html>
<body>
<button>버튼</button>
<script>
const $btn = document.querySelector('button');
$btn.addEventListener('click',()=>console.log('click'));
</script>
</body>
</html>
- addEventListener 메서드로 등록한 이벤트 핸들러는 이벤트 핸들러 프로퍼티에 바인딩된 이벤트 핸들러와 별개이다.
- 즉 둘 다 등록 되어있다면 둘 다 실행된다
- 또한 중복해서 addEventListener로 여러개를 등록하면 등록된 순서대로 모두 호출된다.
- but 참조가 동일한 함수를 중복 등록하면 한개만 등록
<!DOCTYPE html>
<html>
<body>
<button>버튼</button>
<script>
const $btn = document.querySelector('button');
$btn.onclick = () => console.log('.onclick 프로퍼티에 등록');
$btn.addEventListener('click',()=>console.log('addEventListner로 등록'));
$btn.addEventListener('click',()=>console.log('addEventListner로 등록 2'));
$btn.addEventListener('click',()=>console.log('addEventListner로 등록 3'));
$btn.addEventListener('click',handleClick);
$btn.addEventListener('click',handleClick); // 중복 등록 X
function handleClick() {
console.log('handleClick');
}
</script>
</body>
</html>
이벤트 핸들러 제거
- EventTarget.prototype.removeEventListener
- addEventListener로 등록할 때 전달한 인수를 동일하게 전달해야만 제거된다.
- 등록 시 무명 함수를 이벤트 핸들러 인수로 전달했다면 삭제 불가
- 등록과 동시에 삭제해서 딱 1번만 호출되는 이벤트 핸들러를 만들 수도 있다.
- 이벤트 핸들러 프로퍼티로 등록한 이벤트 핸들러는 null을 프로퍼티에 할당하여 제거
<!DOCTYPE html>
<html>
<body>
<button>버튼</button>
<script>
const $btn = document.querySelector('button');
// 딱 1번만 호출되는 이벤트 핸들러
$btn.addEventListener('click',function handleClick(){
console.log('handleClick');
$btn.removeEventListener('click',handleClick);
})
$btn.onclick = () => console.log('click'); // 등록
$btn.onclick = null; // 제거
</script>
</body>
</html>
이벤트 객체
- 이벤트 발생 시 해당 이벤트에 관한 정보를 담고 있는 이벤트 객체가 생성되고 이벤트 핸들러의 첫 번째 인수로 전달된다.
- 따라서 이벤트 객체를 전달받으려면 이벤트 핸들러를 정의할 때 이벤트 객체를 받을 매개변수를 선언해야함
<!DOCTYPE html>
<html>
<body>
<p>클릭한 화면의 좌표값을 알려드림슨</p>
<p id="coordi"></p>
<script>
const $p = document.getElementById('coordi');
const printCoordi = (e) => {
$p.textContent = ` x = ${e.clientX}, y = ${e.clientY}`;
}
document.addEventListener('click',printCoordi);
</script>
</body>
</html>
- 이벤트 핸들러 어트리뷰트로 이벤트 핸들러를 등록했다면
- 어트리뷰트 값으로 할당하는 문의 매개변수 이름을 event로 해야만 이벤트 객체를 받을 수 있음
<!DOCTYPE html>
<html onclick="printCoordi(event)"> // event가 아니면 이벤트 객체 못 받음
<body>
<p>클릭한 화면의 좌표값을 알려드림슨</p>
<p id="coordi"></p>
<script>
const $p = document.getElementById('coordi');
const printCoordi = (e)=>{
$p.textContent = ` x = ${e.clientX}, y = ${e.clientY}`;
}
</script>
</body>
</html>
📙 이벤트 객체의 상속 구조
- 이벤트 타입에 따라 다양한 이벤트 객체가 생성됨
- 위 사진 모두 생성자 함수이므로 생성자 함수를 호출하여 이벤트 객체 생성 가능
- 이벤트가 발생하면 생성되는 이벤트 객체도 생성자 함수에 의해 생성됨
- CustomEvent 객체는 인위적으로 생성해야 생김
📙 이벤트 객체의 공통 프로퍼티
공통 프로퍼티설명타입
type | 이벤트 타입 | string |
target | 이벤트를 발생시킨 DOM 요소 | DOM 요소 노드 |
currentTarget | 이벤트 핸들러가 바인딩된 DOM 요소 | DOM 요소 노드 |
eventPhase | 이벤트 전파 단계0:이벤트 없음, 1: 캡처링, 2:타깃 , 3:버블링 | number |
bubbles | 이벤트를 버블링으로 전파하는지 여부 | boolean |
cancelable | preventDefault()로 기본 동작 취소 가능 여부 | boolean |
defaultPrevented | preventDefault 호출 여부 | boolean |
isTrusted | 사용자에 의해 발생한 이벤트면 true | boolean |
timeStamp | 이벤트 발생 시각 (1970.1.1.00:00:0 부터 경과한 ms) | number |
이벤트 전파
- 이벤트 객체가 생성되면 window부터 시작해서 이벤트 타깃방향으로 전파된다
캡처링 단계 - 이벤트 객체가 이벤트를 발생시킨 이벤트 타깃에 도달함
타깃 단계 - 다시 이벤트 타깃부터 window 방향으로 전파
버블링 단계
이벤트 위임
- 다수의 하위 DOM 요소에 각각 이벤트 핸들러를 등록하지 말고 그 요소들을 포함하는 상위 DOM 요소에 이벤트 핸들러를 딱 한번 등록하는 것
✅ 주의사항
내가 원하지 않았던 요소에서 이벤트가 발생할 수도 있음
ex) 여러개의 li태그에서의 click이벤트를 처리하기위해 ul태그에 이벤트를 위임 했을 때 li태그의 영역이 아닌 ul태그의 영역에서 click하게되면 원하지 않는 상황이 발생한다
따라서 이벤트 핸들러에서 이벤트가 발생한 요소를 한번 더 체크해야한다.
<!DOCTYPE html>
<html>
<head>
<style>
.red{
color: red;
}
.blue{
color: blue;
}
li{
cursor: pointer;
}
</style>
</head>
<body>
<ul id="table">
<li>1번</li>
<li>2번</li>
<li>3번</li>
<li>4번</li>
<li>5번</li>
</ul>
<p></p>
<script>
const table = document.getElementById('table');
table.addEventListener('click',e=>{
if (! e.target.matches('#table > li')) return // 이벤트 타깃 검증
document.querySelector('p').textContent = `${e.target.firstChild.nodeValue} 선택`;
[...table.children].forEach(li =>{
if (li===e.target) li.className = 'red';
else li.className = 'blue';
});
});
</script>
</body>
</html>
DOM 요소의 기본 동작 조작
📙 DOM 요소의 기본 동작 중단
- 이벤트 객체의 preventDefault 메서드는 DOM 요소의 기본 동작을 중단시킨다.
<!-- <a> 클릭해도 네이버 홈페이지로 이동하지 않음 -->
<!DOCTYPE html>
<html>
<body>
<a href="https://www.naver.com">Naver</a>
<script>
const atag = document.querySelector('a');
atag.addEventListener('click', e => e.preventDefault());
</script>
</body>
</html>
📙 이벤트 전파 방지
- 이벤트 객체의 stopPropagation 메서드는 이벤트 전파를 중지시킨다.
- 하위 DOM 요소의 이벤트를 개별적으로 처리하기 위해 이벤트 전파를 중단시킬 때 사용한다.
<!-- 1번은 클릭해도 빨간색으로 변하지 않는다 -->
<!DOCTYPE html>
<html>
<body>
<ul>
<li>1번</li>
<li>2번</li>
<li>3번</li>
</ul>
<script>
const table = document.querySelector('ul');
table.addEventListener('click',e=>{
if (! e.target.matches('ul > li')) return
e.target.style.color = 'red';
});
table.firstElementChild.addEventListener('click', e => e.stopPropagation());
</script>
</body>
</html>
이벤트 핸들러 내부의 this
📙 이벤트 핸들러 어트리뷰트 방식
- 일반 함수로 호출되기 때문에 this는 전역 객체
- 어트리뷰트 값에 함수 호출문 할당할 때 인수로 this를 전달하면 이벤트를 바인딩한 DOM 요소를 가르킨다.
<!DOCTYPE html>
<html>
<body>
<button onclick="handleClick(this)">버튼</button>
<script>
function handleClick(that){
console.log(this); // window
console.log(that); // <button>
}
</script>
</body>
</html>
📙 이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식
- 이벤트 핸들러 내부의 this = 이벤트를 바인딩한 DOM 요소
- 이벤트 핸들러 내부의 this = 이벤트 객체의 currentTarget 프로퍼티
<!DOCTYPE html>
<html>
<body>
<button class="btn1">버튼1</button>
<button class="btn2">버튼2</button>
<button class="btn3">버튼3</button>
<script>
const $btn1 = document.querySelector('button[class="btn1"]');
const $btn2 = document.querySelector('button[class="btn2"]');
const $btn3 = document.querySelector('button[class="btn3"]');
$btn1.onclick = function(e){
console.log(this);
console.log(e.currentTarget === this); // true
}
$btn2.addEventListener('click',function(e){
console.log(this);
console.log(e.currentTarget === this); // true
});
$btn3.addEventListener('click', e => {
console.log(this); // 화살표 함수는 this바인딩 X, 상위 스코프의 this 참조
console.log(e.currentTarget === this); // false
});
</script>
</body>
</html>
이벤트 핸들러에 인수 전달
- 이벤트 핸들러 어트리뷰트 방식은 함수 호출문을 할당하기 때문에 인수 전달 가능하다.
<!DOCTYPE html>
<html>
<body>
<button type="text" onclick="handleClick(2,4)">버튼</button>
<script>
const handleClick = (a,b) => alert(`${a}+${b} = ${a+b}`);
</script>
</body>
</html>
- 이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식의 경우 함수 자체를 할당하기 때문에 인수 전달이 불가능 할 것 같지만 아래와 같이 인수 전달이 가능하다.
- 이벤트 핸들러 내부에서 함수를 호출
<!DOCTYPE html>
<html>
<body>
<button type="text">버튼</button>
<script>
const $btn = document.querySelector('button');
const handleClick = (a,b) => alert(`${a}+${b} = ${a+b}`);
$btn.addEventListener('click',()=>{
handleClick(2,4);
});
</script>
</body>
</html>
- 이벤트 핸들러를 반환하는 함수를 호출
<!DOCTYPE html>
<html>
<body>
<button type="text">버튼</button>
<script>
const $btn = document.querySelector('button');
const 이벤트핸들러뱉는함수 = (a,b) => () => alert(`${a}+${b} = ${a+b}`)
$btn.addEventListener('click', 이벤트핸들러뱉는함수(2,4));
</script>
</body>
</html>
커스텀 이벤트
📙 커스텀 이벤트 생성
- 생성자 함수를 호출하여 명시적으로 생성한 이벤트 객체는 이벤트 타입 지정 가능
- 이 때 생성자 함수의 첫 번째 인수는 이벤트 타입을 나타내는 문자열
- 기존의 이벤트 타입이 아니라면 CustomEvent 생성자 함수를 사용
- 커스텀 이벤트 객체는 버블링 X, preventDefault 로 취소 불가
- 두번째 인수를 통해 기존 이벤트 타입의 고유 프로퍼티 지정 가능
- isTrusted 프로퍼티는 언제나 false
const keyboardEvent = new KeyboardEvent('keyup');
const customEvent = new CustomEvent('myType');
console.log(customEvent.bubbles); // false
console.log(customEvent.cancelable); // false
// 이벤트 타입의 따른 고유 프로퍼티를 지정하려면 3번째 인수로 객체를 넘긴다
const customMouseEvent = new MouseEvent('click',{
bubbles : true,
cancelable : true,
clientX : 50,
clientY : 100
});
📙 커스텀 이벤트 디스패치
- 디스패치 = 이벤트를 발생시키는 행위
- dispatchEvent(이벤트 객체) 메서드로 커스텀 이벤트를 발생시킬 수 있음
- 일반적인 이벤트 핸들러는 비동기 처리 방식이지만 dispatchEvent는 동기
- dispatchEvent 메서드를 호출하면 커스텀 이벤트에 바인딩된 이벤트 핸들러를 직접 호출하는 것이기 때문에 커스텀 이벤트를 처리할 이벤트 핸들러를 등록한 이후 디스패치 해야함
- 커스텀 이벤트 타입은 이벤트 핸들러 등록시 addEventListner 메서드만 가능
요소 노드에는 당연히 내가 만든 이벤트 타입에 해당하는 프로퍼티가 없으니까
<!DOCTYPE html>
<html>
<body>
<button type="text">버튼</button>
<script>
const $btn = document.querySelector('button');
$btn.addEventListener('myType', e => {
alert(e.detail.message);
});
const customEvent = new CustomEvent('myType',{
detail : {message : 'Hi'}
});
$btn.dispatchEvent(customEvent);
</script>
</body>
</html>
728x90
반응형
'JavaScript' 카테고리의 다른 글
[모던자바스크립트 DeepDive] 45장 Promise (0) | 2023.02.18 |
---|---|
[모던자바스크립트 DeepDive] 41장. 타이머 & 43장. Ajax (0) | 2023.02.18 |
[모던자바스크립트 DeepDive] 42장 비동기 프로그래밍 (0) | 2023.02.12 |
[모던자바스크립트 DeepDive] 37장 Set과 Map (0) | 2023.02.12 |
[모던자바스크립트 DeepDive] 35. 스프레드 문법 & 36. 디스트럭쳐링 할당 (0) | 2023.02.04 |