📌 [모던 자바스크립트 13주 뿌시기] 40. 이벤트
![](/assets/img/JavaScript/2022-11-10/bookcover.png)
40. 이벤트
이벤트 드리븐 프로그래밍
이벤트 핸들러: 이벤트가 발생했을 때 호출될 함수
이벤트 핸들러 등록: 이벤트가 발생했을 때 브라우저에게 이벤트 핸들러의 호출을 위임하는 것.
이벤트 드리븐 프로그래밍: 프로그램의 흐름을 이벤트 중심으로 제어하는 프로그래밍 방식.
이벤트 타입
이벤트 타입: 이벤트의 종류를 나타내는 문자열.
이벤트 타입에는 약 200여 가지가 있으며 아래 링크에서 상세 목록을 확인할 수 있다.
https://developer.mozilla.org/en-US/docs/Web/Events
이벤트 핸들러 등록 방식
1. 이벤트 핸들러 어트리뷰트 방식
HTML 요소의 어트리뷰트에 이벤트 핸들러 어트리뷰트를 추가하고 어트리뷰트 값으로 함수 호출문 등의 문을 할당하여 등록하는 방법
<!DOCTYPE html>
<html>
<body>
<button onclick="sayHi('kim')">버튼</button>
<script>
function sayHi(name) {
console.log(`Hi ${name}`);
}
// 다음과 같이 암묵적으로 파싱된다.
function onclick(event){
sayHi('kim');
}
</script>
</body>
</html>
어트리뷰트 값은 함수 참조가 아닌 함수 호출문 등의 문을 할당한다.
때문에 인수를 전달할 수 있지만, 어트리뷰트 값은 암묵적으로 생성되는 이벤트 핸들러의 함수로 파싱된다.
해당 방법은 HTML과 JS가 혼용되어 있기 때문에 해당 방법으로 이벤트 핸들러를 등록하지 않는 것이 좋다.
2. 이벤트 핸들러 프로퍼티 방식
이벤트 핸들러 프로퍼티에 함수를 바인딩하여 등록하는 방식이다.
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');
// 이벤트 핸들러 프로퍼티에 이벤트 핸들러를 바인딩
$button.onclick = function () {
console.log('button click');
};
</script>
</body>
</html>
이벤트 핸들러 프로퍼티 방식에는 세 가지를 지정해야 한다.
- 이벤트 타깃: 이벤트를 발생시킬 객체 (
$button
) - 이벤트 타입: 이벤트의 종류를 나타내는 문자열 (
onclick
) - 이벤트 핸들러 (
function
)
해당 방식은 하나의 이벤트에 하나의 이벤트 핸들러만 바인딩할 수 있다.
3. addEventListener 메서드 방식
EventTarget.prototype.addEventListener
메서드를 사용하여 등록하는 방식이다.
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');
// addEventListener 메서드 방식
$button.addEventListener('click', function() {
console.log('button click');
});
</script>
</body>
</html>
이벤트 타깃: $button
이벤트 타입(첫 번째 매개변수): 'click'
이벤트 핸들러(두 번째 매개변수): function() {...}
세 번째 매개변수: 이벤트를 캐치할 이벤트 전파 단계를 지정한다. 이는 생략할 수 있다. false
거나 생략한 경우에는 버블링 단계에서 이벤트를 캐치하고 true
를 지정하면 캡처링 단계에서 이벤트를 캐치한다.
2. 이벤트 핸들러 프로퍼티 방식
과addEventListener
메서드 방식을 같이 사용하여 이벤트 핸들러를 등록한다면 두 개의 이벤트 핸들러가 동일하게 호출된다.addEventListener
메서드 방식은 여러 이벤트 핸들러를 등록할 수 있으며 등록된 순서대로 호출된다.- 참조가 동일한 이벤트 핸들러를 중복 등록하면 하나의 이벤트 핸들러만 등록된다.
이벤트 핸들러 제거
EventTarget.prototype.removeEventListener
메서드를 사용하여 addEventListener
메서드로 등록한 이벤트 핸들러를 제거할 수 있다. 이때 removeEventListener
메서드에 전달할 인수는 addEventListener
의 인수와 같다.
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');
const handleClick = () => console.log('button click');
// 이벤트 핸들러 등록
$button.addEventListener('click', handleClick);
// 이벤트 핸들러 제거
// addEventListener 메서드에 전달한 인수와 removeEventListener 메서드에
// 전달한 인수가 일치하지 않으면 이벤트 핸들러가 제거되지 않는다.
$button.removeEventListener('click', handleClick, true); // 실패
$button.removeEventListener('click', handleClick); // 성공
</script>
</body>
</html>
- 무명 함수를 이벤트 핸들러로 등록한 경우 제거할 수 없다. (
addEventListener
메서드에 인수로 전달한 이벤트 핸들러와 동일한 함수여야 한다.) - 이벤트 핸들러 프로퍼티 방식으로 등록한 이벤트 핸들러는
removeEventListener
메서드로 제거할 수 없다. 이는 이밴트 핸들러 프로퍼티에null
을 할당하여 제거할 수 있다.
이벤트 객체
이벤트 객체: 이벤트에 관련한 다양한 정보를 담고 있는 객체.
이벤트가 발생하면 이벤트 객체가 동적으로 생성된다.
생성된 이벤트 객체는 브라우저가 핸들러의 첫 번째 인수로 전달된다.
<!DOCTYPE html>
<html>
<body>
<p>클릭 좌표 반환</p>
<em class="message"></em>
<script>
const $msg = document.querySelector('.message');
// 첫 번째 인수(e)로 이벤트 객체가 전달된다.
function showCoords(e) {
$msg.textContent = `x: ${e.clientX}, y: ${clientY}`;
}
document.onclick = showCoords;
</script>
</body>
</html>
- 이벤트 객체를 전달받기 위해서는 매개변수를 명시적으로 선언해야 한다.
- 이벤트 핸들러 어트리뷰트 방식으로 이벤트 객체를 전달받기 위해서는 매개변수 이름이 반드시
event
이어야 한다.
이벤트 객체의 상속 구조
이벤트 객체는 다음과 같은 상속 구조를 가진다.
![](/assets/img/JavaScript/2023-02-12/eventobject.png)
Event
,UIEvent
,MouseEvent
등은 생성자 함수이다. 호출하여 이벤트 객체를 생성할 수 있다.- 이벤트를 호출하여 암묵적으로 생성되는 이벤트 객체도 위와 같은 생성자 함수에 의해 생성된다.
- 생성된 이벤트 객체는 생성자 함수와 더불어 생성되는 프로토타입으로 구성된 프로토타입 체인의 일원이 된다.
- 이벤트 객체의 프로퍼티는 발생한 이벤트 타입에 따라 달라진다.
이벤트 객체의 공통 프로퍼티
Event.prototype
의 이벤트 관련 프로퍼티는 모든 이벤트 객체가 상속받는 공통 프로퍼티다.
이벤트 객체의 공통 프로퍼티
공통 프로퍼티 | 설명 | 타입 |
---|---|---|
type | 이벤트 타입 | string |
target | 이벤트를 발생시킨 DOM 요소 | DOM 요소 노드 |
currntTarget | 이벤트 핸들러가 바인딩된 DOM 요소 | DOM 요소 노드 |
eventPhase | 이벤트 전파 단계 | number |
bubbles | 이벤트 버블링으로 전파하는지 여부 | boolean |
cancelable | preventDefault 메서드로 이벤트 기본 동작을 취소할 수 있는지 여부 | boolean |
defaultPrevented | preventDefault 메서드로 이벤트를 취소했는지 여부 | boolean |
isTrusted | 사용자의 행위에 의해 발생한 이벤트인지 여부 | boolean |
timeStamp | 이벤트가 발생한 시각(1970/01/01/00:00:0부터 경과한 밀리초) | number |
마우스 정보 취득
MouseEvent
타입의 이벤트 객체는 아래와 같은 고유의 프로퍼티를 가진다.
screenX
/screenY
,clientX
/clientY
,pageX
/pageY
,offsetX
/offsetY
- 드래그는
mousedown
이벤트가 발생한 상태에서mousemove
이벤트가 발생한 시점에서 시작하고mouseup
이벤트가 발생한 시점에 종료된다. clientX
,clientY
프로퍼티는 뷰포트(웹 페이지의 가시 영역) 기준으로 마우스 포인터 좌표를 나타낸다.
키보드 정보 취득
KeyboardEvent
타입의 이벤트 객체는 아래와 같은 고유의 프로퍼티를 가진다.
altKey
,ctrlKey
,shiftKey
,metaKey
,key
,keyCode
key
프로퍼티는keyup
이벤트가 발생하면 이벤트 객체에 입력한 키 값을 문자열로 반환한다.key
프로퍼티 값의 대응관계는 https://www.toptal.com/developers/keycode 에서 확인할 수 있다.
이벤트 전파
이벤트 전파: DOM 트리 상에 존재하는 DOM 요소 노드에서 발생한 이벤트가 DOM 트리를 통해 전파되는 현상.
이벤트 객체는 이벤트를 발생시킨 DOM 요소인 이벤트 타깃을 중심으로 DOM 트리를 통해 전파된다.
여기서 이벤트 객체가 전파되는 방향에 따라 3단계로 구분할 수 있다.
- 캡처링 단계 : 이벤트가 상위 요소에서 하위 요소 방향으로 전파
- 타깃 단계 : 이벤트가 이벤트 타깃에 도달
- 버블링 단계 : 이벤트가 하위 요소에서 상위 요소 방향으로 전파
<!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}`);
console.log(`이벤트 타깃: ${e.target}`);
console.log(`커런트 타깃: ${e.currentTarget}`);
});
</script>
</body>
</html>
캡처링 단계
- li 요소를 클릭하면 클릭 이벤트가 발생한다.
- 클릭 이벤트 객체가 생성된다.
- 클릭된 li 요소는 이벤트 타깃이 된다.
- 클릭 이벤트 객체는
window
에서 시작하여 이벤트 타깃 방향으로 전파된다.
타깃 단계
- 이벤트 객체는 이벤트를 발생시킨 이벤트 타깃에 도달한다.
버블링 단계
- 이벤트 객체는 이벤트 타깃에서 시작해서
window
방향으로 전파된다.
- 이벤트 핸들러 어트리뷰트/프로퍼티 방식으로 등록한 이벤트 핸들러는 타깃 단계, 버블링 단계의 이벤트만 캐치할 수 있다.
addEventListener
메서드 방식으로 등록한 이벤트 핸들러는 타깃 단계와 버블링 단계뿐만 아니라 캡처링 단계의 이벤트도 선별적으로 캐치할 수 있다. (3번째 인수로true
를 전달해야 함.)- DOM 트리를 통해 전파되는 이벤트는 이벤트 패스에 위치한 모든 DOM 요소에서 캐치할 수 있다.
- 대부분의 이벤트는 캡처링과 버블링을 통해 전파된다.
- 전파되지 않는 이벤트:
focus
/blur
,load
/unload
/abort
/error
,mouseenter
/mouseleave
- 전파되지 않는 이벤트에 대한 대체 이벤트:
fucusin
/focusout
,mouseover
/mouseout
이벤트 위임
이벤트 위임: 여러 개의 하위 DOM 요소에 각각 이벤트 핸들러를 등록하는 대신 하나의 상위 DOM 요소에 이벤트 핸들러를 등록하는 방법.
즉, 하위 DOM 요소를 전부 포함하는 DOM 요소에 이벤트 하나만 준다면 하위 DOM 요소에 이벤트를 각각 여러 번 등록할 필요가 없다.
- 상위 요소에 이벤트 핸들러를 등록하기 때문에 이벤트 타깃이 개발자가 기대한 DOM 요소가 아닐 수 있다.
Element.prototype.matches
메서드를 통해 인수로 전달된 선택자에 의해 특정 노드를 탐색 가능한지 확인할 수 있다.
DOM 요소의 기본 동작 조작
DOM 요소의 기본 동작 중단
이벤트 객체의 preventDefault
메서드는 DOM 요소의 기본 동작을 중단시킨다.
<!DOCTYPE html>
<html>
<body>
<a id="link" href="http://www.google.com">go</a>
<script>
document.getElementById('link').onclick = e => {
// a 요소의 기본 동작을 중단한다.
e.preventDefault();
};
</script>
</body>
</html>
이벤트 전파 방지
이벤트 객체의 stopPropagation
메서드는 이벤트 전파를 중지시킨다.
<!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 요소는 이벤트 전파X, 상위 요소에서 이벤트 캐치 불가능
document.querySelector('.btn2').onclick = e => {
e.stopPropagation(); // 이벤트 전파 중단
e.target.style.color = 'blue';
};
</script>
</body>
</html>
이벤트 핸들러 내부의 this
이벤트 핸들러 어트리뷰트 방식
<!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>
해당 예시에서 함수 내의 this
는 전역 객체 window
를 가리킨다.
handleClick
는 이벤트 핸들러에 의해 일반 함수로 호출되므로, 일반 함수로서 호출되는 함수 내부의 this
는 전역 객체 window
를 가리킨다.
해당 예시에서 어트리뷰트 값의 인수 this
는 이벤트 핸들러를 호출할 때 바인딩한 DOM 요소를 가리킨다.
이벤트 핸들러 프로퍼티 방식, 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) {
console.log(this); // $button1
console.log(e.currentTarget); // $button1
console.log(this === e.currentTarget); // true
++this.textContent;
};
// addEventListener 메서드 방식
$button2.addEventListener('click', function(e) {
console.log(this); // $button2
console.log(e.currentTarget); // $button2
console.log(this === e.currentTarget); // true
++this.textContent;
});
</script>
</body>
</html>
- 화살표 함수로 정의한 이벤트 핸들러 내부의
this
는 상위 스코프의this
를 가리킨다. (화살표 함수는 함수 자체의this
바인딩을 갖지 않음) - 클래스에서 이벤트 핸들러를 바인딩하는 경우, 특히 클래스 내부의 메서드를 이벤트 핸들러로 바인딩할 때,
bind
메서드를 사용하여 this가 클래스가 생성할 인스턴스를 가리키도록 해야 한다.
이벤트 핸들러에 인수 전달
- 함수에 인수 전달하는 방법: 함수를 호출할 때 전달.
- 이벤트 핸들러 어트리뷰트 방식: 함수 호출문을 사용하므로 전달 가능.
- 이벤트 핸들러 프로퍼티 방식,
addEventListener
메서드 방식: 이벤트 핸들러를 브라우저가 호출하므로 함수 호출문이 아닌 함수 자체를 등록해야 하므로 인수를 전달할 수 없다.
이벤트 핸들러 프로퍼티 방식, addEventListener
메서드 방식으로 인수 전달하기
// 첫 번째 방법: 이벤트 핸들러 내부에서 함수를 호출하면서 인수 전달
document.querySelector('input').onblur = () => {
EventFunction(argument);
};
// 두 번째 방법: 이벤트 핸들러를 반환하는 함수를 호출하면서 인수 전달
document.querySelector('input').onblur = EventFunction(argument);
커스텀 이벤트
커스텀 이벤트 생성
암시적 이벤트 객체 생성: 이벤트 발생 > 이벤트 객체 생성 > 이벤트 타입 결정됨.
명시적 이벤트 객체 생성(생성자 함수 사용): 임의의 이벤트 타입을 지정 가능.
CustomEvent
이벤트 생성자 함수를 사용하여 새로운 이벤트 타입을 지정할 수 있다.
const customEvent = new CustomEvent('foo');
console.log(customEvent.type); // foo
- 커스텀 이벤트 객체는 버블링되지 않으며,
preventDefault
메서드로 취소 불가능 - 커스텀 이벤트 객체는
bubbles
와cancelable
프로퍼티의 기본값이false
이다.true
로 설정하기 위해서는 두 번째 인수로bubbles
또는cancelable
프로퍼티를 갖는 객체를 전달한다. - 이벤트 타입에 따라 가지는 고유의 프로퍼티 값을 지정할 수 있다. 이를 위해 두 번째 인수로 프로퍼티를 전달한다.
- 커스텀 이벤트는
isTrusted
의 프로퍼티 값이 언제나false
.
커스텀 이벤트 디스패치
디스패치: 이벤트를 발생시키는 행위.
dispatchEvent
메서드를 통해 이벤트 객체를 인수로 전달하면서 호출하면 커스텀 이벤트를 디스패치할 수 있다.
- 이벤트 핸들러는 비동기 처리 방식이지만
dispatchEvent
메서드는 동기 처리 방식이다. dispatchEvent
메서드는 동기 처리 방식이므로 디스패치 전에 커스텀 이벤트를 처리할 이벤트 핸들러를 등록해야 한다.CustomEvent
이벤트 생성자 함수에 두 번째 인수로 전달하고 싶은 정보를 담은detail
프로퍼티를 포함하는 객체를 전달할 수 있다.
출처
위키북스, 『모던 자바스크립트 Deep Dive』, 이웅모
https://www.tutorialstonight.com/js/js-dom-navigation