29. 이벤트
1. 이벤트 드리븐 프로그래밍(Event Driven Programming)
프로그램의 흐름을 이벤트 중심으로 제어하는 프로그래밍 방식
- 브라우저는 클릭, 키보드 입력, 마우스 이동 등의 어떤 이벤트가 일어나면 이를 감지하여 해당 타입의 이벤트를 발생시킨다.
- 특정 타입의 이벤트에 반응해 어떤 작업을 처리해주고 싶다면 다음의 2가지가 필요하다.
1. 이벤트 핸들러: 이벤트가 발생했을 때 호출될 함수
2. 이벤트 핸들러 등록: 이벤트가 발생했을 때 브라우저에게 이벤트 핸들러의 호출을 위임하는 것
2. 이벤트 타입
이벤트 타입은 이벤트의 종류를 나타낸다.
이벤트 타입은 약 200여 가지가 있다. 다음 mdn 문서에서 다양한 이벤트 타입을 확인 가능하다.
https://developer.mozilla.org/ko/docs/Web/Events
3. 이벤트 핸들러 등록
이벤트 핸들러는 이벤트가 발생하면 브라우저에 의해 호출되는 함수이다. 즉, 이벤트가 발생했을 때 브라우저에게 호출을 위임한 함수이다.
이벤트 핸들러를 등록하는 방법은 3가지다.
1️⃣ 이벤트 핸들러 어트리뷰트 방식
2️⃣ 이벤트 핸들러 프로퍼티 방식
3️⃣ addEventListener 메서드 방식
1️⃣ 이벤트 핸들러 어트리뷰트 방식 (= 인라인 이벤트 핸들러 방식)
- HTML 요소의 이벤트 핸들러 어트리뷰트를 사용하는 방식, HTML 요소에는 이벤트에 대응하는 애트리뷰트가 있다.
- 이벤트 핸들러 어트리뷰트의 이름은 onclick과 같이 on 접두사와 이벤트의 종류를 나타내는 이벤트 타입으로 이루어져 있다.
- 이벤트 핸들러 어트리뷰트 값으로 함수 호출문을 할당하면 이벤트 핸들러가 등록된다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<button onclick="sayHi('Lee')">Click me!</button>
<script>
function sayHi(name) {
console.log(`Hi! ${name}`);
}
</script>
</body>
</html>
여기서 onclick="sayHi('Lee')" 어트리뷰트는 파싱되어 다음의 함수를 암묵적으로 생성한다.
function onclick(event) {
sayHi('Lee');
}
이벤트 핸들러 어트리뷰트 이름과 동일한 키 onclick 이벤트 핸들러 프로퍼티에 할당한다.
결국, 이벤트 핸들러 어트리뷰트 값은 암묵적으로 생성되는 이벤트 핸들러의 함수 몸체이다.
여러개의 함수 호출문을 어트리뷰트 값으로 할당하는 것도 가능하다.
<button onclick="console.log(`Hi! `); console.log('Lee');">Click me!</button>
이벤트 핸들러 어트리뷰트 값으로 함수 호출문을 할당하는 이유?
- 이벤트 핸들러에 인수를 전달하기 위해서다.
- 만약, 이벤트 핸들러 어트리뷰트 값으로 함수 참조를 할당해야 한다면 이벤트 핸들러에 인수를 전달하기 곤란할 것이다.
이벤트 핸들러 어트리뷰트 방식의 문제점
- HTML 코드에 자바스크립트 코드를 포함하면 HTML의 목적에 벗어나고, HTML과 자바스크립트가 분리되지 않는다.
- 가독성이 떨어지고, 유지보수하기가 어렵다.
예전 HTML 문서에서는 이 방식을 사용했기에 알아둘 필요는 있지만 더는 사용하지 않는 것이 좋다.
추가적으로, CBD(Component Based Development) 방식의 Angular/React/Svelte/Vue.js 프레임워크/라이브러리는 이벤트 핸들러 어트리뷰트 방식으로 이벤트를 처리한다. CBD에서는 HTML, CSS, JavaScript를 관심사가 다른 개별적인 요소가 아닌, 뷰를 구성하기 위한 구성 요소로 보아 관심사가 다르다고 생각하지 않기 때문이다.
2️⃣ 이벤트 핸들러 프로퍼티 방식
이벤트 핸들러 어트리뷰트 방식의 HTML와 자바스크립트가 분리되지 않는 문제를 해결할 수 있다.
- window 객체, Document, HTMLElement 타입의 DOM 노드 객체는 이벤트에 대응하는 이벤트 핸들러 프로퍼티를 가지고 있다.
- 이벤트 핸들러 프로퍼티의 키도 on 접두사와 이벤트 타입으로 이루어져 있다.
- 이벤트 핸들러 프로퍼티에 함수를 바인딩하면 이벤트 핸들러가 등록된다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector("button");
// 이벤트 핸들러 프로퍼티에 이벤트 핸들러 바인딩
// $button: 이벤트 타겟, onclick: on이벤트타입
$button.onclick = function () {
console.log("button click");
};
</script>
</body>
</html>
이벤트 핸들러는 대부분 이벤트를 발생시킬 이벤트 타겟에 바인딩한다. 하지만 이벤트 버블링과 이벤트 캡쳐링을 이용하여 이벤트 핸들러를 전파될 이벤트를 캐치할 DOM 노드 객체에 바인딩하기도 한다.
이벤트 핸들러 프로퍼티 방식의 문제점
- 이벤트 핸들러 어트리뷰트 방식의 HTML와 JavaScript가 분리되지 않는 문제를 해결할 수 있다.
- 하지만 이벤트 핸들러 프로퍼티에 하나의 이벤트 핸들러만 바인딩할 수 있다는 단점이 있다. 이는 하나의 이벤트 타겟에 여러 이벤트 핸들러를 바인딩해도 재할당이 되어 여러 이벤트 핸들러가 실행되지 않는 것을 의미한다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector("button");
// 첫 번째로 바인딩된 이벤트 핸들러는 두 번째 바인딩된 이벤트 핸들러에 의해 재할당되어 실행되지 않는다.
$button.onclick = function () {
console.log("button click 1");
};
$button.onclick = function () {
console.log("button click 2");
};
</script>
</body>
</html>
3️⃣ addEventListener 메서드 방식
EventTarget.prototype.addEventListener 메서드
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<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 방식을 모두 사용해 이벤트 핸들러를 등록하면 두 이벤트 핸들러가 모두 호출된다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector("button");
// 이벤트 핸들러 프로퍼티 방식
$button.onclick = function () {
console.log("button click 1");
};
// addEventListener 메서드 방식
$button.addEventListener("click", function () {
console.log("button click 2");
});
</script>
</body>
</html>
- 하나의 요소에서 발생한 동일한 이벤트에 대해 addEventListener 메서드는 하나 이상의 이벤트 핸들러를 등록할 수 있다. 이때, 이벤트 핸들러는 등록된 순서대로 호출된다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector("button");
// addEventListener 메서드 방식
$button.addEventListener("click", function () {
console.log("button click 1");
});
$button.addEventListener("click", function () {
console.log("button click 2");
});
</script>
</body>
</html>
- 단, addEventListener 메서드를 통해 참조가 동일한 이벤트 핸들러를 중복 등록하면 하나의 이벤트 핸들러만 등록된다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector("button");
const handleClick = () => console.log("button click");
$button.addEventListener("click", handleClick);
$button.addEventListener("click", handleClick);
</script>
</body>
</html>
4. 이벤트 핸들러 제거
addEventListener 메서드로 등록한 이벤트 핸들러를 제거하기 위해서 EventTarget.prototype.removeEventListener 메서드를 사용한다.
removeEventListener 메서드 사용시 주의할 점
- removeEventListener 메서드에 전달한 인수가 addEventListener 메서드에 전달한 인수와 동일해야 한다.
- 이벤트 핸들러를 제거하려면 이벤트 핸들러의 참조를 변수나 자료구조에 저장하고 있어야 한다. 즉, 이벤트 핸들러 함수를 따로 선언하는 것이 좋다. 무명 함수를 이벤트 핸들러로 등록한 경우에는 제거할 수 없기 때문이다.
// 이벤트 핸들러 등록
$button.addEventListener("click", () => console.log('buttton click');
// 등록한 이벤트 핸들러를 참조할 수 없다. 따라서 제거 불가능
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector("button");
const handleClick = () => console.log("button click");
$button.addEventListener("click", handleClick);
$button.removeEventListener("click", handleClick);
</script>
</body>
</html>
- 이벤트 핸들러 프로퍼티 방식으로 등록한 이벤트 핸들러를 제거하려면 이벤트 핸들러 프로퍼티에 null을 할당해야 한다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector("button");
const handleClick = () => console.log("button click");
$button.onclick = handleClick;
// $button.removeEventListener("click", handleClick);
$button.onclick = null;
</script>
</body>
</html>
이벤트 객체
- 이벤트가 발생하면 이벤트에 관련한 다양한 정보를 담고 있는 이벤트 객체가 동적으로 생성된다.
- 생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달되어 매개변수에 암묵적으로 할당된다. 브라우저가 이벤트 핸들러를 호출할 때 이벤트 객체를 인수로 전달하기 때문이다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<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.addEventListener("click", showCoords);
</script>
</body>
</html>
- 주의) 이벤트 핸들러 어트리뷰트 방식의 경우 event가 아닌 다른 이름으로는 이벤트 객체를 전달받지 못한다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<style>
html,
body {
height: 100%;
}
</style>
</head>
<body onclick="showCoords(event)">
<p>클릭하세요! 클릭한 곳의 좌표가 표시됩니다.</p>
<em class="message"></em>
<script>
const $msg = document.querySelector(".message");
function showCoords(e) {
$msg.textContent = `clientX: ${e.clientX}, clientY: ${e.clientY}`;
}
</script>
</body>
</html>
❓이벤트 핸들러 어트리뷰트 방식에서는 왜 event가 아닌 다른 이름으로 매개변수를 선언하면 이벤트 객체를 전달받지 못할까?
이벤트 핸들러 어트리뷰트는 파싱되어 이벤트 핸들러 함수를 암묵적으로 생성하여 이벤트 타입의 이벤트 핸들러 프로퍼티에 할당하기 때문이다.
function onclick(event) {
showCoords(event);
}
다음과 같이 암묵적으로 생성된 onclick 이벤트 핸들러의 첫 번째 매개변수의 이름이 event로 암묵적으로 정해진다. 따라서 event가 아닌 다른 이름으로 이벤트 객체를 전달받지 못한다.
이벤트 객체의 상속 구조
이벤트가 발생하면 이벤트 타입에 따라 다양한 타입의 이벤트 객체가 생성된다.
이벤트 객체는 다음과 같은 상속 구조를 갖는다.
위 상속 구조의 Event, UIEvent, MouseEvent 등은 모두 생성자 함수다. 따라서 생성자 함수를 호출하여 이벤트 객체를 생성할 수 있다.
// MouseEvent 생성자 함수를 호출하여 click 이벤트 타입의 MouseEvent 객체를 생성한다.
const e = new MouseEvent("click");
console.log(e);
// Object.getPrototypeOf()
결국 이벤트가 발생하면 생성되는 이벤트 객체도 생성자 함수에 의해 생성된다. 따라서 생성된 이벤트 객체는 생성자 함수와 더불어 생성되는 프로토타입으로 구성된 프로토타입 체인의 일원이 된다.
이벤트 객체의 공통 프로퍼티
- Event.prototype에 정의되어 있는 이벤트 관련 프로퍼티는 모든 이벤트 객체가 상속받는 공통 프로퍼티이다.
공통 프로퍼티 | 설명 | 타입 |
type | 이벤트 타입 | string |
target | 이벤트를 발생시킨 DOM 요소 | DOM 요소 노드 |
currentTarget | 이벤트 핸들러가 바인딩된 DOM 요소 | DOM 요소 노드 |
eventPhase | 이벤트 전파 단계 0: 이벤트 없음, 1: 캡처링 단계, 2: 타겟 단계, 3: 버블링 단계 |
number |
bubbles | 이벤트를 버블링으로 전파하는지 여부. 다음의 이벤트는 bubbles:false로 버블링하지 않는다. - 포커스 이벤트: focus/blur - 리소스 이벤트: load/unload/abort/error - 마우스 이벤트: mouseenter/mouseleave |
boolean |
cancelable | preventDefault 메서드를 호출하여 이벤트의 기본 동작 취소할 수 있는지 여부 다음 이벤트는 cancelable: false로 취소할 수 없다. - 포커스 이벤트: focus/blur - 리소스 이벤트: load/unload/abort/error - 마우스 이벤트: dbclick/mouseenter/mouseleave |
boolean |
defaultPrevented | preventDefault 메서드를 호출하여 이벤트를 취소했는지 여부 | boolean |
isTrusted | 사용자의 행위에 의해 발생한 이벤트인지 여부. 예를 들어, click 메서드 또는 dispatchEvent 메서드를 통해 인위적으로 발생시킨 이벤트인 경우 isTruted는 false다. | boolean |
timeStamp | 이벤트가 발생한 시각(1970/01/01/00:00:0부터 경과한 밀리초 | number |
ex) 체크박스 요소의 체크 상태가 변경되면 현재 체크 상태를 출력해보도록 하자.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<input type="checkbox" />
<em class="message">off</em>
<script>
const $checkbox = document.querySelector("input[type=checkbox]");
const $msg = document.querySelector(".message");
$checkbox.addEventListener("change", (e) => {
console.log(e);
console.log(Object.getPrototypeOf(e) === Event.prototype);
console.log(e.target === e.currentTarget);
$msg.textContent = e.target.checked ? "on" : "off";
});
</script>
</body>
</html>
체크 상태가 변경되면 checked 프로퍼티의 값이 변경되고, change 이벤트가 발생한다.
마우스 정보 취득
click, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseleave 이벤트가 발생하면 생성되는 MouseEvent타입의 이벤트 객체는 다음의 고유 프로퍼티를 갖는다.
- 마우스 포인터의 좌표 정보를 나타내는 프로퍼티: screenX/screenY, clientX/clientY, pageX/pageY, offsetX/offsetY
- 버튼 정보를 나타내는 프로퍼티: altKey, ctrlKey, shiftKey, button
ex) DOM 요소를 드래그하여 이동시키기
🤔 어떻게 구현해야 할까?
드래그는 마우스 버튼을 누른 상태(mouseDown 이벤트가 발생한 상태)에서 마우스를 이동하는 것으로 시작(mousemove 이벤트가 발생한 시점에 시작)하고, 마우스 버튼을 떼면(mouseup 이벤트가 발생한 시점)에 종료한다.
전체 코드
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>드래그하여 요소 이동시키기</title>
<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};
// offset: 이동할 거리
const offset = {x: 0, y: 0};
// mousemove 이벤트 핸들러
const move = (e) => {
offset.x = e.clientX - initialMousePos.x;
offset.y = e.clientY - initialMousePos.y;
box.style.transform = `translate3d(${offset.x}px, ${offset.y}px, 0)`;
};
// 1. mousedown 이벤트가 발생하면 드래그 시작 지점의 마우스 포인터 좌표를 저장한다.
box.addEventListener("mousedown", (e) => {
initialMousePos.x = e.clientX - offset.x;
initialMousePos.y = e.clientY - offset.y;
// 2. mousedown 이벤트가 발생한 상태에서 mousemove 이벤트가 발생하면 box 요소를 이동시킨다.
document.addEventListener("mousemove", move);
});
// 3. mouseup 이벤트가 발생하면 mousemove 이벤트를 제거해 이동을 멈춘다.
document.addEventListener("mouseup", () => {
document.removeEventListener("mousemove", move);
});
</script>
</body>
</html>