해당 포스팅은 위키북스의 모던 자바스크립트 Deep Dive라는 책을 독학하며 기록하는 글입니다.
브라우저는 처리해야 할 특정 사건이 발생하면 이를 감지하여 이벤트를 발생시킨다. 만약 어플리케이션이 특정 타입의 이벤트에 대해 반응하여 어떤 일을 하고 싶다면 해당하는 타입의 이벤트가 발생했을 때 호출될 함수를 브라우저에게 알려 호출을 위임한다. 이때 이벤트가 발생했을 때 호출될 함수를 이벤트 핸들러, 브라우저에게 이벤트 핸들러의 호출을 위임하는 것을 이벤트 핸들러 등록이라한다.
브라우저에게 이벤트 핸들러의 호출을 위임하는 이유는 어떤 특정 이벤트가 발생할지 특정되어 있지 않기 때문이다. 즉, '나는 이벤트가 언제 호출될지 모르니까 브라우저야 니가 이벤트가 발생하면 그때 대신 호출시켜'라고 하는 것이다. 이처럼 프로그램의 흐름을 이벤트 중심으로 제어하는 프로그래밍 방식을 이벤트 드리븐 프로그래밍이라한다.
이벤트 타입은 이벤트의 종류를 나타내는 문자열이다. 이벤트 타입은 약 200여 가지가 있으며 MDN의 이벤트 레퍼런스에서 모두 확인할 수 있다. 다음은 자주 쓰이는 이벤트 타입들이다.
분류 | 이벤트 타입 | 이벤트 발생 시점 |
---|---|---|
마우스 | click | 마우스 버튼을 클릭했을 때 |
dbclick | 마우스 버튼을 더블클릭했을 때 | |
mousedown | 마우스 버튼을 눌렀을 때 | |
mouseup | 누르고 있던 마우스 버튼을 놓았을 때 | |
mousemove | 마우스 커서를 움직였을 때 | |
mouseenter | 마우스 커서를 HTML 요소 안으로 이동했을 때(버블링X) | |
mouseover | 마우스 커서를 HTML 요소 안으로 이동했을 때(버블링O) | |
mouseleave | 마우스 커서를 HTML 요소 밖으로 이동했을 때(버블링X) | |
mouseout | 마우스 커서를 HTML 요소 밖으로 이동했을 때(버블링O) | |
키보드 | keydown | 모든 키를 눌렀을 때 발생(문자, 숫자, 특수문자, enter 키는 연속적으로 발생하지만 그 외 키는 한 번만 발생) |
keypress | 문자 키를 눌렸을 떄 연속적으로 발생(문자, 숫자, 특수문자, enter키, 폐지되었으므로 사용하지 않을 것을 권장) | |
keyup | 누르고 있던 키를 놓았을 때 한 번만 발생 | |
포커스 | fouse | HTML 요소가 포커스를 받았을 때(버블링X) |
blur | HTML 요소가 포커스를 잃었을 때(버블링X) | |
fousein | HTML 요소가 포커스를 받았을 때(버블링O) | |
fouseout | HTML 요소가 포커스를 잃었을 때(버블링O) | |
폼 | submit | form 요소 내의 submit 버튼을 클릭했을 때 |
reset | form 요소 내의 reset 버튼을 클릭했을 때 | |
값 변경 | input | input(text, checkbox, radio), select, textarea 요소의 값이 입력되었을 때 |
change | input(text, checkbox, radio), select, textarea 요소의 값이 변경되었을 때 여기서 변경여부는 해당 HTML요소가 포커스를 잃었을 때 측정된다. | |
readystatechange | HTML 문서의 로드와 파싱 상태를 나타내는 document.readyState 프로퍼티 값이 번경될 때 | |
DOM | DOMContentLoaded | HTML 문서의 로드와 파싱이 완료되어 DOM 생성이 완료되었을 때 |
뷰 | resize | 브라우저 윈도우의 크리를 리사이즈할 때 연속적으로 발생(오직 window 객체에서만 발생) |
scroll | 웹페이지 또는 HTML 요소를 스크롤할 때 연속적으로 발생 | |
리소스 | load | DOMContentLoaded 이벤트가 발생한 이후, 모든 리소스의 로딩이 완료되었을 때 |
unload | 리소스가 언로드될 때(주로 새로운 웹페이지를 요청한 경우) | |
abort | 리소스 로딩이 중단되었을 때 | |
error | 리소스 로딩이 실패했을 때 |
onclick
과 같이 on
접두어와 이벤트 타입을 이어서 나타낸다.<button onclick="console.log("Hi")" class="btn"></button>
on
접두어와 이벤트 타입을 이어서 나타낸다. 해당 방식은 이벤트 핸들러 프로퍼티에 단 하나의 이벤트 핸들러만 등록할 수 밖에 없다는 단점이 있다.const btn = document.querySelector('.btn');
btn.onclick = function() {
console.log("Hi");
}
addEventListener
메서드를 사용해 이벤트 핸들러를 등록하는 방식이다. 하나 이상의 이벤트 핸들러를 등록할 수 있으며 호출은 등록된 순서대로 호출된다. 앞선 두 방식과는 다르게 on
접두어를 빼고 이벤트 타입명만 사용하며 사용법은 다음과 같다.이벤트타켓.addEventListener('이벤트타입', 이벤트핸들러[, useCapture(기본값은 false)]);
const btn = document.querySelector('.btn');
const sayHi = function() {
console.log("Hi");
}
btn.addEventListener('click', sayHi);
null
을 할당한다.const btn = document.querySelector('.btn');
btn.onclick = function() {
console.log("Hi");
}
btn.onclick = null;
const btn = document.querySelector('.btn');
const sayHi = function() {
console.log("Hi");
}
btn.addEventListener('click', sayHi);
btn.removeEventListener('click', sayHi);
이벤트가 발생하면 이벤트에 관련한 다양한 정보를 담고 있는 이벤트 객체가 동적으로 생성된다. 생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달된다. 이때 어트리뷰트 방식으로 이벤트 핸들러를 등록했다면 이벤트 객체를 전달받은 매개변수의 이름이 무조건 event
여야 한다.
이벤트 객체는 발생한 이벤트 타입에 따라 가지고 있는 정보가 조금씩 다르지만 공통적으로 가지고 있는 프로퍼티가 있다.
이 외에 마우스 이벤트는 마우스 포인터의 좌표 정보를 나타내는 프로퍼티나 버튼 버튼 정보를 나타내는 프로퍼티를 추가로 가지고 있고, 키보드 이벤트인 경우에도 어떤 키가 눌렀는지, 특수 키가 같이 눌렸는지에 대한 프로퍼티를 따로 가지고 있다.
screenX/Y
, clientX/Y
, offsetX/Y
, altKey
, ctrlKey
, shiftKey
, button
altKey
, ctrlKey
, shiftKey
, metaKey
, key
, keyCode
마우스 이벤트와 스크롤 이벤트의 경우 이벤트 객체에 존재하는 프로퍼티들을 이용해 인터렉티브한 효과를 많이 만들어 낼 수 있다.
사실 이벤트는 이벤트를 일으키고 싶은 요소 자체에서만 일어나는게 아니다. 특정한 요소에 이벤트 핸들러를 등록해서 이벤트를 일으키면 브라우저는 해당 이벤트가 일어났다는 사실을 최상위 객체인 window
객체에서부터 실제 이벤트가 일어난 요소까지 아래 방향으로 전달(캡쳐링 단계)을 하고, 실제로 이벤트를 일으키 요소는 해당 사실을 전달받은 다음(타켓 단계), 다시 최단위 단계인 window
객체로 전달(버블링 단계)한다.
기본적으로 대부분의 이벤트는 캡처링 단계에서의 이벤트는 캐치하지 않고 버블링 단계의 이벤트를 캐치하지만 addEventListener 메서드를 사용할 때 3번째 인수인 useCapture
의 값을 true
로 주면 버블링 단계가 아닌 캡쳐링 단계의 이벤트를 캐치한다.
그럼 이제 상상해보자. 만약 ul
태그 아래 li
태그가 100개가 있는 문서가 있고, 각 리스트들이 눌릴때마다 이벤트를 발생시키고 싶을 때 우리는 100개의 li
태그에 이벤트 핸들러를 모두 등록해줘야 할까?? 생각만해도 비효율적이다. 이럴 때 사용하는 것이 이벤트 위임이다.
이벤트는 앞에서 얘기한 것과 같이 꼭 이벤트가 일어난 요소에서만 캐치할 수 있는 것이 아니다. 따라서 실제 이벤트가 일어난 요소가 아닌 그 상위 요소에게 하위 요소의 이벤트를 처리하게끔 이벤트 핸들러를 등록하는 것을 이벤트 위임
이라고 한다.
이때 유의해야 할 점은 실제 이벤트가 일어난 요소를 가리키는 이벤트 객체의 프로퍼티는 target
이라는 것이고, 이벤트 핸들러가 등록된 요소를 가리키는 이벤트 객체의 프로퍼티는 currentTarget
이라는 것이다. 또한 상위 요소에 이벤트 위임을 시키고 하위 요소에서 실제로 이벤트를 일으킨 요소가 개발자가 의도한 하위 요소인지를 확인하는 과정을 넣어줘야 예기치 못한 오류를 방지할 수 있다. 다음은 여러 개의 li
태그를 가진 ul
태그에서 실제로 눌린 li
태그의 색만 바꾸는 이벤트를 위임하는 코드이다.
const lists = document.querySelector('ul');
const chageColor = function({target}) {
if(!target.matches('lists > li')) return;
[...lists.children].forEach((ele) => {
ele.classList.toggle('textRed');
});
}
lists.addEventListener('click', chageColor);
어떤 DOM 요소는 저마다의 기본 동작을 가지고 있다. 예를 들어 a
태그는 href
어트리뷰트에 지정된 링크로 이동하고 submit
태그는 폼을 초기화시킨다. 이러한 DOM 요소들의 기본 동작을 막아주는 메서드는 preventDefault
이다.
const aBtn = document.querySelector('a');
// a태그를 클릭했을때 일어나는 기본동작을 막아준다.
aBtn.addEventListener('click', e => {
e.preventDefault();
});
또한 본인의 상위 요소로 이벤트 전파를 막은 stopPropagation
메서드도 있다. 해당 메서드를 등록하면 해당 요소의 상위 요소로 더 이상 이벤트가 전파되지 않는다.
const list = document.querySelector('li');
// a태그를 클릭했을때 일어나는 기본동작을 막아준다.
list.addEventListener('click', e => {
e.stopPropagation();
});
이벤트 핸들러를 등록할 때 프로퍼티 방식이나 addEventListener
메서드를 통해 등록하는 방식을 사용한다면 이벤트 핸들러 함수 자체를 등록하는 것이지 호출하는 것이 아니기 때문에 인수를 함께 전달할 수가 없다. 하지만 아래의 방법을 통해 인수를 전달해서 필요한 메서드를 사용할 수 있다.
// 이벤트 핸들러로 화살표 함수를 등록하고 그 안에서 필요한 메서드를 인자와 같이 사용
DOM요소.addEventListener('이벤트타입', () => {
필요한메서드(필요한인수);
}
// 이벤트 핸들러를 반환하는 메서드를 호출하면서 인수를 전달
const 이벤트핸들러를반환하는메서드 = (필요한인수) => e => {
실제함수내용
}
DOM요소.on이벤트타입 = 이벤트핸들러를반환하는메서드(필요한인수);