브라우저는 처리해야 할 특정 사건이 발생하면 이를 감지하여 이벤트(=evnet)를 발생(=trigger)시킨다.
예를들어, 클릭, 키보드 입력, 마우스 이동 등이 일어나면 브라우저는 이를 감지하여 특정한 타입의 이벤트를 발생시킨다.
만약 애플리케이션이 특정 타입 이벤트에 대해 반응하여 어떤 일을 하고 싶다면 해당 타입의 이벤트가 발생했을 때 호출될 함수를 브라우저에게 알려 호출을 위임한다.
이때 호출될 함수를 ① 이벤트 핸들러(= event handler) 라 하고, 이벤트가 발생했을 때 브라우저에게 이벤트 핸들러의 호출을 위임하는 것을 ② 이벤트 핸들러 등록이라 한다.
예를 들어, 사용자가 버튼을 클릭했을 때 함수를 호출하여 어떤 처리를 하고 싶다고 가정해보자.
이때 문제는 '언제 함수를 호출해야 하는가'이다. 사용자가 언제 버튼을 클릭할지 알 수 없으므로 언제 함수를 호출해야 할 지 알 수 없기 때문이다.
다행히 브라우저는 사용자의 버튼을 감지하여 클릭 이벤트를 발생할 수 있다. 함수를 언제 호출할지 알 수 없으므로 개발자가 명시적으로 호출하는 것이 아니라 브라우저에게 함수 호출을 위임하는 것 이다. 이렇게 프로그램의 흐름을 이벤트 중심으로 제어하는 프로그래밍 방식을 이벤트 드리븐 프로그래밍(= event-driven programming)이라 한다.
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector("button");
/*
이벤트를 발생시키는 방법은 총 3가지이다.
이에 대해서는 뒤에서 알아본다
*/
$button.onclick = () => {
alert("button click");
};
</script>
</body>
</html>
이벤트 타입은 이벤트의 종류를 나타내는 문자열이다. 상세 목록은 MDN의 Event reference에서 확인할 수 있다.
이벤트 타입 | 이벤트 발생 시점 |
---|---|
click | 마우스 버튼을 클릭했을 때 |
dbclick | 마우스 버튼을 더블 클릭했을 때 |
mousedown | 마우스 버튼을 누르고 있을 때 |
mouseup | 누르고 있던 마우스 버튼을 뗄 때 |
mousemove | 마우스 커서를 움직일 때 |
mouseenter | 마우스 커서를 HTML 요소 안으로 이동했을 때 (버블링 x) |
mouseover | 마우스 커서를 HTML 요소 안으로 이동했을 때 (버블링 o) |
mouseleave | 마우스 커서를 HTML 요소 밖으로 이동했을 때(버블링x) |
mouseout | 마우스 커서를 HTML 요소 밖으로 이동했을 때(버블링o) |
이벤트 타입 | 이벤트 발생 시점 |
---|---|
keydown | 키를 누르고 있을 때 |
keypress | 키를 누르고 뗏을 때 (폐지되었으므로 사용 x) |
keyup | 누르고 있던 키를 뗄 때 |
이벤트 타입 | 이벤트 발생 시점 |
---|---|
focus | 요소가 포커스를 얻었을 때 (버블링 x) |
blur | 요소가 포커스를 잃었을 때 (버블링 x) |
focusin | 요소가 포커스를 얻었을 때 (버블링 o) |
foucusout | 요소가 포커스를 잃었을 때 (버블링 o) |
이벤트 타입 | 이벤트 발생 시점 |
---|---|
submit | form을 submit할 때 (버튼 또는 키) |
reset | reset 버튼을 클릭할 때 (최근에는 사용 안함) |
이벤트 타입 | 이벤트 발생 시점 |
---|---|
input | input 또는 textarea 요소의 값이 변경되었을 때 |
change | select box, checkbox, radio button의 상태가 변경되었을 때 |
이벤트 타입 | 이벤트 발생 시점 |
---|---|
DOMContentLoaded | HTML 문서의 로드와 파싱이 완료되어 DOM 생성이 완료되었을 때 |
이벤트 타입 | 이벤트 발생 시점 |
---|---|
resize | 브라우저 윈도우의 크기를 리사이즈할 때 연속적으로 발생 |
scroll | 웹피이지(document) 또는 HTML 요소를 스코롤할 때 연속적으로 발생 |
이벤트 타입 | 이벤트 발생 시점 |
---|---|
load | DOMContentLoaded 이후, 모든 리소스의 로딩이 완료되었을 때 |
unload | 리소스가 언로드 될 때 (주로 새로운 웹페이지를 요청한 경우) |
abort | 리소스 로딩이 중단되었을 때 |
error | 리소스 로딩이 실패했을 때 |
이벤트 핸들러는 특정 이벤트가 발생했을 때 브라우저에 호출을 위임한 함수다. 다시 말해, 이벤트가 발생하면 브라우저에 호출될 함수가 이벤트 핸들러다.
이벤트가 발생했을 때 브라우저에게 이벤트 핸들러의 호출을 위임하는 것을 이벤트 핸들러 등록이라 한다. 이벤트 핸들러를 등록하는 방법은 3가지다.
① 이벤트 핸들러 어트리뷰트 방식
② 이벤트 핸들러 프로퍼티 방식
③ addEventListener 메서드 방식
이벤트 핸들러 어트리뷰트 방식은 ① on 접두사와 이벤트 종류를 나타내는 ② 이벤트 타입으로 이루어져 있다.
<!DOCTYPE html>
<html>
<body>
<button onclick="sayHi('Lee')">Click me!</button>
<script>
function sayHi(name) {
console.log(`Hi! ${name}.`);
}
</script>
</body>
</html>
이벤트 핸들러 어트리뷰트 방식으로 함수를 호출할 때는 반드시 참조 형식이 아닌(sayHi), 함수 호출문(sayHi('Lee')) 형태로 호출해야 한다.
<!-- 이벤트 핸들러에 인수를 전달하기 곤란하다. -->
<button onclick="sayHi">Click me!</button>
핸들러 내부에 한가지의 코드만 사용할 수 있는 것은 아니다.
<button onclick="console.log('Hi! '); console.log('Lee');">Click me!</button>
이벤트 핸들러 프로퍼티 방식은 ① on 접두사와 이벤트 종류를 나타내는 ② 이벤트 타입으로 이루어져 있다는 것에서 이벤트 핸들러 어트리뷰트 방식과 유사하다. 하지만 요소의 어트리뷰트 내에서 직접 호출하는 것이 아닌, 이벤트 핸들러 프로퍼티에 함수를 바인딩하면 이벤트 핸들러가 등록되는 형식이다.
<!DOCTYPE html>
<html>
<body>
<!--
① 이벤트 핸들러 어트리뷰트 방식
<button>Click me!</button>
-->
<button>Click me!</button>
<script>
const $button = document.querySelector("button");
// 이벤트 핸들러 프로퍼티에 이벤트 핸들러를 바인딩 (익명 함수로 가능)
$button.onclick = function () {
console.log("button click");
};
</script>
</body>
</html>
하지만 이벤트 핸들러 프로퍼티에 하나의 이벤트 핸들러만 바인딩할 수 있다는 단점이 있다. 하나의 이벤트 핸들러에 두 개의 핸들러를 바인딩할 경우 재할당을 통해 마지막으로 바인딩 된 핸들러 함수가 호출된다.
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<em></em>
<script>
const $button = document.querySelector("button");
const $em = document.querySelector("em");
// 이벤트 핸들러 프로퍼티 방식은 하나의 이벤트에 하나의 이벤트 핸들러만을 바인딩할 수 있다.
// 첫 번째로 바인딩된 이벤트 핸들러는 두 번째 바인딩된 이벤트 핸들러에 의해 재할당되어
// 실행되지 않는다.
$button.onclick = function () {
$em.innerHTML = "Button Cliked 1";
};
// 두 번째로 바인딩된 이벤트 핸들러
$button.onclick = function () {
$em.innerHTML = "Button Cliked 2";
};
</script>
</body>
</html>
addEventListener 메서드 방식은 추가적으로 파라미터에 버블링과 캡처링 여부를 넣어줄 수 있다. 생략하거나 false를 지정하면 버블링 단계에서 이벤트를 캐치하고, true를 지정하면 캡처링 단계에서 이벤트를 캐치한다.
<html>
<body>
<button>Click me!</button>
<em></em>
<script>
const $button = document.querySelector("button");
const $em = document.querySelector("em");
$button.addEventListener("click", function () {
$em.innerHTML = "Button Cliked 1";
});
$button.addEventListener("click", function () {
$em.innerHTML = "Button Cliked 2";
});
</script>
</body>
</html>
addEventListener 메서드는 하나 이상의 이벤트 핸들러를 등록할 수 있다. 이때 등록한 이벤트 핸들러는 순서대로 호출된다.
Button Cliked 1
Button Cliked 2
③ 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"));
// 등록한 이벤트 핸들러를 참조할 수 없으므로 제거할 수 없다
// 기명 함수를 이벤트 핸들러로 등록
$button.addEventListener("click", function foo() {
console.log("button click");
// 이벤트 핸들러를 제거한다. 따라서 이벤트 핸들러는 단 한 번만 호출된다.
$button.removeEventListener("click", foo);
});
② 이벤트 핸들러 프로퍼티 방식으로 등록한 이벤트 핸들러는 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}`;
}
/*
마치 생성자 함수나 클래스 선언문에서 this를 바인딩하는 것과 같은 느낌입니다
*/
document.onclick = showCoords;
</script>
</body>
</html>
클릭 이벤트에 의해 생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달되어 매개변수 e에 암묵적으로 할당된다.
이는 브라우저가 이벤트 핸들러를 정의할 때 객체를 인수로 전달하기 때문이다. 따라서 이벤트 객체를 전달받으려면 이벤트 핸들러를 정의할 때 이벤트 객체가 전달받을 매개변수를 명시적으로 선언해야 한다.
위 예제에서 'e' 라는 문자열로 매개변수를 선언했으나 다른 이름을 가지는 문자열로 사용해도 상관없다
만약 ① 이벤트 핸들러 어트리뷰트 방식으로 이벤트 핸들러를 등록했다면 event라는 변수를 선언하여 이벤트 객체를 전달해야 한다. onclick 이벤트 핸들러의 첫 번째 매개변수의 이름이 event로 암묵적으로 명명되기 때문이다.
이벤트 핸들러 어트리뷰트 방식
<!DOCTYPE html>
<html>
<head>
<style>
html,
body {
height: 100%;
}
</style>
</head>
<!-- 이벤트 핸들러 어트리뷰트 방식의 경우 event가 아닌 다른 이름으로는 이벤트 객체를
전달받지 못한다. -->
<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>
버블링과 캡처링에 관한 이벤트 전파는 다음 챕터로 끊어서 다루겠습니다!