프론트엔드/JavaScript
[40장 이벤트-1] 모던 자바스크립트 Deep Dive
의현
2025. 3. 13. 21:09
(754p ~ 778p까지 정리내용입니다.)
이벤트 드리븐 프로그래밍
- 브라우저는 처리해야 할 특정 사건이 발생하면 이벤트(ex: 클릭, 키보드 입력, 마우스 이동 등)를 발생시킨다.
- 만약 애플리케이션 특정 타입의 이벤트에 대해 반응하여 어떤 일을 하고 싶다면 해당하는 타입의 이벤트가 발생했을 때 호출될 함수를 브라우저에게 알려 호출을 위임한다.
- 이벤트가 발생했을 때 호출될 함수를 이벤트 핸들러라하고, 이벤트가 발생했을 때 브라우저에게 이벤트 핸들러의 호출을 위임하는 것을 이벤트 핸들러 등록이라 한다.
- 즉 함수를 언제 호출할지 알 수 없으므로 개발자가 명시적으로 함수를 호출하는 것이 아니라 브라우저에게 함수 호출을 위임하는 것이다.
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');
// 사용자가 버튼을 클릭하면 함수를 호출하도록 요청
$button.onclick = () => { alert('button click'); };
</script>
</body>
</html>
- 이처럼 이벤트와 그에 대응하는 함수(이벤트 핸들러)를 통해 사용자와 애플리케이션은 상호작용을 할 수 있다.
- 이와 같이 프로그램의 흐름을 이벤트 중심으로 제어하는 프로그래밍 방식을 이벤트 드리븐 프로그래밍이라 한다.
이벤트 타입
- 이벤트 타입은 이벤트의 종류를 나타내는 문자열이다.
- 이벤트 타입은 약 200여 가지가 있고 상세 목록은 MDN의 Event reference에서 확인할 수 있다.
- 사용 빈도가 높은 이벤트를 살펴보자.
- 마우스 이벤트 : click, dbclick, moustdown, mouseup, mouseenter, mouseover, mouseleave, mouseout
- 키보드 이벤트 : keydown, keypress, keyup
- 포커스 이벤트 : focus, blur, focusin, focusout
- focusin, focusout 이벤트 핸들러를 이벤트 핸들러 프로퍼티 방식으로 등록하면 크롬, 사파리에서 정상 동작하지 않는다.
- focusin, focusout 이벤트 핸들러는 addEventListener 메서드 방식을 사용해 등록해야 한다.
- 폼 이벤트 : submit, reset
- 값 변경 이벤트 : input, change, readystatechange
- DOM 뮤테이션 이벤트 : DOMContentLoaded
- 뷰 이벤트 : resize, scroll
- 리소스 이벤트 : load, unload, abort, error
이벤트 핸들러 등록
- 이벤트 핸들러(event handler 또는 event listener)는 이벤트가 발생했을 때 브라우저에게 호출을 위임한 함수다.
- 이벤트 핸들러를 등록하는 방법은 3가지다.
1. 이벤트 핸들러 어트리뷰트 방식
- HTML 요소의 어트리뷰트 중에는 이벤트에 대응하는 이벤트 핸들러 어트리뷰트가 있다.
- 이벤트 핸들러 어트리뷰트의 이름은 onclick과 같이 on 접두사와 이벤트 종류를 나타내는 이벤트 타입으로 이루어져 있다.
- 이벤트 핸들러 어트리뷰트 값으로 함수 호출문 등의 문을 할당하면 이벤트 핸들러가 등록된다.
<!DOCTYPE html>
<html>
<body>
<button onclick="sayHi('Lee')">Click me!</button>
<script>
function sayHi(name) {
console.log(`Hi! ${name}.`);
}
</script>
</body>
</html>
- 이벤트 핸들러를 등록할 때 콜백 함수와 마찬가지로 함수 참조를 등록해야 브라우저가 이벤트 핸들러를 호출할 수 있다.
- 이벤트 핸들러 어트리뷰트 값은 사실 암묵적으로 생성될 이벤트 핸들러의 함수 몸체를 의미한다.
- 즉, onclick="sayHi('Lee')" 어트리뷰트는 파싱되어 다음과 같은 함수를 암묵적으로 생성하고, 이벤트 핸들러 어트리뷰트 이름과 동일한 키 onclick 이벤트 핸들러 프로퍼티에 할당한다.
function onclick(event) {
sayHi('Lee');
}
- 이처럼 동작하는 이유는 이벤트 핸들러에 인수를 전달하기 위해서다.
- CBD(Component Based Development) 방식의 Angular/React/Svelte/Vue.js 같은 프레임워크/라이브러리에서는 이벤트 핸들러 어트리뷰트 방식으로 이벤트를 처리한다.
<!-- Angular -->
<button (click)="handleClick($event)">Save</button>
{ /* React */ }
<button onClick={handleClick}>Save</button>
<!-- Svelte -->
<button on:click={handleClick}>Save</button>
<!-- Vue.js -->
<button v-on:click="handleClick($event)">Save</button>
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>
- 이벤트 핸들러를 등록하기 위해서는 이벤트를 발생시킬 객체인 이벤트 타깃과 이벤트의 종류를 나타내는 문자열인 이벤트 타입 그리고 이벤트 핸들러를 지정할 필요가 있다.
- 이벤트 핸들러는 대부분 이벤트를 발생시킬 이벤트 타깃에 바인딩한다.
- 하지만 반드시 이벤트 타깃에 이벤트 핸들러를 바인딩해야 하는 것은 아니다.
- 이벤트 핸들러는 이벤트 타깃 또는 전파된 이벤트를 캐치할 DOM 노드 객체에 바인딩한다.
- "이벤트 핸들러 프로퍼티 방식"은 "이벤트 핸들러 어트리뷰트 방식"의 HTML과 자바스크립트가 뒤섞이는 문제를 해결 할 수 있다.
- 하지만 이벤트 핸들러 프로퍼티에 하나의 이벤트 핸들러만 바인딩할 수 있다는 단점이 있다.
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');
// 이벤트 핸들러 프로퍼티 방식은 하나의 이벤트에 하나의 이벤트 핸들러만을 바인딩할 수 있다.
// 첫 번째로 바인딩된 이벤트 핸들러는 두 번째 바인딩된 이벤트 핸들러에 의해 재할당되어
// 실행되지 않는다.
$button.onclick = function () {
console.log('Button clicked 1');
};
// 두 번째로 바인딩된 이벤트 핸들러
$button.onclick = function () {
console.log('Button clicked 2');
};
</script>
</body>
</html>
3. addEventListener 메서드 방식
- DOM Level 2에서 도입된 EventTarget.prototype.addEventListener 메서드를 사용하여 이벤트 핸들러를 등록할 수 있다.
- 앞서 살펴본 방식은 Level 0 부터 제공되던 방식이다.
- 이벤트 종류를 나타내는 문자열인 이벤트 타입을 전달할 때 on 접두사를 붙이지 않는다.
- 마지막 매개변수에는 이벤트를 캐치할 이벤트 전파 단계(캡처링 또는 버블링)를 지정한다.
- 생략하거나 false를 지정하면 버블링 단계에서 이벤트를 캐치하고,
- true를 지정하면 캡처링 단계에서 이벤트를 캐치한다.
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');
// 이벤트 핸들러 프로퍼티 방식
// $button.onclick = function () {
// console.log('button click');
// };
// addEventListener 메서드 방식
$button.addEventListener('click', function () {
console.log('button click');
});
</script>
</body>
</html>
- 이벤트 프로퍼티 방식과 addEventListener 방식을 모두 사용하여 이벤트를 등록하면 어떻게 될까?
- addEventListener 방식은 이벤트 프로퍼티 방식에 영향을 주지 않는다.
- 즉, 2개의 이벤트 핸들러가 모두 호출된다.
- addEventListener 메서드는 하나 이상의 이벤트 핸들러를 등록할 수 있다.
- 이때 이벤트 핸들러는 등록된 순서대로 호출된다.
- 단, 참조가 동일한 이벤트 핸들러를 중복 등록하면 하나의 이벤트 핸들러만 등록된다.
이벤트 핸들러 제거
- addEventListener 메서드로 등록한 이벤트 핸들러를 제거하려면 EventTarget.prototype.removeEventListener 메서드를 사용한다.
- 단, 전달한 인수가 정확하게 일치하지 않으면 이벤트 핸들러가 제거되지 않는다.
<!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>
- 무명 함수를 이벤트 핸들러로 등록한 경우 제거할 수 없다.
- 이벤트 핸들러를 제거하려면 이벤트 핸들러의 참조를 변수나 자료구조에 저장하고 있어야 한다.
// 이벤트 핸들러 등록
$button.addEventListener('click', () => console.log('button click'));
// 등록한 이벤트 핸들러를 참조할 수 없으므로 제거할 수 없다.
- 단, 기명 이벤트 핸들러 내부에서 removeEventListener 메서드를 호출하여 이벤트 핸들러를 제거하는 것은 가능하다.
- 이때 이벤트 핸들러는 단 한번만 호출된다.
- 다음 예제의 경우 버튼을 여러 번 클릭해도 단 한 번만 이벤트 핸들러가 호출된다.
// 무명 함수를 이벤트 핸들러로 등록
$button.addEventListener('click', function () {
console.log('button click');
// 이벤트 핸들러를 제거한다. 따라서 이벤트 핸들러는 단 한 번만 호출된다.
// arguments.callee는 호출된 함수, 즉 함수 자신을 가리킨다.
$button.removeEventListener('click', arguments.callee);
});
- 무명 함수로 이벤트 핸들러를 등록한 경우 제거해야 한다면, 함수 자신을 가리키는 arguments.callee 를 사용할 수도 있다.
- 단, strict mode에서 사용 불가능
// 무명 함수를 이벤트 핸들러로 등록
$button.addEventListener('click', function () {
console.log('button click');
// 이벤트 핸들러를 제거한다. 따라서 이벤트 핸들러는 단 한 번만 호출된다.
// arguments.callee는 호출된 함수, 즉 함수 자신을 가리킨다.
$button.removeEventListener('click', arguments.callee);
});
- 이벤트 핸들러 프로퍼티 방식으로 등록한 이벤트 핸들러는 removeEventListener 메서드로 제거할 수 없다.
- 제거하려면 이벤트 핸들러 프로퍼티에 null을 할당한다.
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');
const handleClick = () => console.log('button click');
// 이벤트 핸들러 프로퍼티 방식으로 이벤트 핸들러 등록
$button.onclick = handleClick;
// removeEventListener 메서드로 이벤트 핸들러를 제거할 수 없다.
$button.removeEventListener('click', handleClick);
// 이벤트 핸들러 프로퍼티에 null을 할당하여 이벤트 핸들러를 제거한다.
$button.onclick = null;
</script>
</body>
</html>
이벤트 객체
- 이벤트가 발생하면 이벤트에 관련한 다양한 정보를 담고 있는 이벤트 객체가 동적으로 생성된다.
- 생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달된다.
<!DOCTYPE html>
<html>
<body>
<p>클릭하세요. 클릭한 곳의 좌표가 표시됩니다.</p>
<em class="message"></em>
<script>
const $msg = document.querySelector('.message');
// 클릭 이벤트에 의해 생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달된다.
function showCoords(e) {
$msg.textContent = `clientX: ${e.clientX}, clientY: ${e.clientY}`;
}
document.onclick = showCoords;
</script>
</body>
</html>
- 클릭 이벤트에 의해 생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달되어 매개변수 e에 암묵적으로 할당된다.
- 이는 브라우저가 이벤트 핸들러를 호출할 때 이벤트 객체를 인수로 전달하기 때문인다.
- 따라서 이벤트 객체를 전달받으려면 매개변수를 명시적으로 선언해야 한다.
- 이벤트 어트리뷰트 방식으로 이벤트 핸들러를 등록했다면 다음과 같이 event를 통해 이벤트 객체를 전달받을 수 있다.
- 첫 번째 매개변수 이름이 반드시 event이어야 한다.
- 암묵적으로 매개변수가 event인 함수를 생성하여 이벤트 핸들러 프로퍼티에 할당하기 때문이다.
<body onclick="showCoords(event)">
...
</body>
1. 이벤트 객체의 상속 구조
- 이벤트가 발생하면 이벤트 타입에 따라 다양한 타입의 이벤트 객체가 생성된다.
- 이벤트 객체는 다음과 같은 상속 구조를 갖는다.
2. 이벤트 객체의 공통 프로퍼티
- Event 인터페이스의 이벤트 관련 프로퍼티는 모든 이벤트 객체가 상속받는 공통 프로퍼티다.
- 이벤트 객체의 공통 프로퍼티는 다음과 같다.
3. 마우스 정보 취득
- MouseEvent 타입의 이벤트 객체는 다음과 같은 고유의 프로퍼티를 갖는다.
1. 마우스 포인터의 좌표 정보를 나타내는 프로퍼티 : screenX/screenY, clientX/clientY, pageX/pageY, offsetX/offsetY
2. 버튼 정보를 나타내는 프로퍼티 : altKey, ctrlKey, shiftKey, button
더보기
- 예를 들어, DOM 요소를 드래그하여 이동시키는 예제를 만들어보자.
- 드래그가 시작되면 드래그 시작 지점, 즉 mousedown 이벤트가 발생했을 때의 마우스 포인터 좌표와 드래그를 하고 있는 지점, 즉 mouseover 이벤트가 발생할 때마다 마우스 포인터 좌표를 비교하여 드래그 대상의 이동 거리를 계산한다.
- 그리고 clientX/clientY는 뷰포트, 즉 웹페이지의 가시 영역을 기준으로 마우스 포인터 좌표를 나타낸다.
<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 100px;
height: 100px;
background-color: #fff700;
border: 5px solid orange;
cursor: pointer;
}
</style>
</head>
<body>
<div class="box"></div>
<script>
// 드래그 대상 요소
const $box = document.querySelector('.box');
// 드래그 시작 시점의 마우스 포인터 위치
const initialMousePos = { x: 0, y: 0 };
// 오프셋: 이동할 거리
const offset = { x: 0, y: 0 };
// mousemove 이벤트 핸들러
const move = e => {
// 오프셋 = 현재(드래그하고 있는 시점) 마우스 포인터 위치 - 드래그 시작 시점의 마우스 포인터 위치
offset.x = e.clientX - initialMousePos.x;
offset.y = e.clientY - initialMousePos.y;
// translate3d는 GPU를 사용하므로 absolute의 top, left를 사용하는 것보다 빠르다.
// top, left는 레이아웃에 영향을 준다.
$box.style.transform = `translate3d(${offset.x}px, ${offset.y}px, 0)`;
};
// mousedown 이벤트가 발생하면 드래그 시작 시점의 마우스 포인터 좌표를 저장한다.
$box.addEventListener('mousedown', e => {
// 이동 거리를 계산하기 위해 mousedown 이벤트가 발생(드래그를 시작)하면
// 드래그 시작 시점의 마우스 포인터 좌표(e.clientX/e.clientY: 뷰포트 상에서 현재
// 마우스의 포인터 좌표)를 저장해 둔다. 한번 이상 드래그로 이동한 경우 move에서
// translate3d(${offset.x}px, ${offset.y}px, 0)으로 이동한 상태이므로
// offset.x와 offset.y를 빼주어야 한다.
initialMousePos.x = e.clientX - offset.x;
initialMousePos.y = e.clientY - offset.y;
// mousedown 이벤트가 발생한 상태에서 mousemove 이벤트가 발생하면
// box 요소를 이동시킨다.
document.addEventListener('mousemove', move);
});
// mouseup 이벤트가 발생하면 mousemove 이벤트를 제거해 이동을 멈춘다.
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', move);
});
</script>
</body>
</html>
4. 키보드 정보 취득
- keyboardEvent 타입의 이벤트 객체는 altKey, ctrlKey, shiftKey, metaKey, key, keyCode 같은 고유 프로퍼티를 갖는다.
더보기
- 예를 들어, input 요소의 입력 필드에 엔터 키가 입력되면 현재까지 입력 필드에 입력된 값을 출력하는 예제를 만들어보자.
- keyup 이벤트가 발생되면 생성되는 keyboardEvent 타입의 객체는 입력한 키 값을 문자열로 반환하는 key 프로퍼티를 제공한다.
- 엔터 키의 경우 key 프로퍼티는 'Enter'를 반환한다.
- 참고로 input 요소의 입력 필드에 한글을 입력하고 엔터 키를 누르면 keyup 이벤트 핸들러가 두 번 호출되는 현상이 발생한다.
- 이 같은 문제를 회피하려면 keydown 이벤트를 캐치한다.
<!DOCTYPE html>
<html>
<body>
<input type="text" />
<em class="message"></em>
<script>
const $input = document.querySelector('input[type=text]');
const $msg = document.querySelector('.message');
$input.onkeyup = e => {
// e.key는 입력한 키 값을 문자열로 반환한다.
// 입력한 키가 'Enter', 즉 엔터 키가 아니면 무시한다.
if (e.key !== 'Enter') return;
// 엔터키가 입력되면 현재까지 입력 필드에 입력된 값을 출력한다.
$msg.textContent = e.target.value;
e.target.value = '';
};
</script>
</body>
</html>