해당 포스팅은 위키북스의 모던 자바스크립트 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누르고 있던 키를 놓았을 때 한 번만 발생
포커스fouseHTML 요소가 포커스를 받았을 때(버블링X)
blurHTML 요소가 포커스를 잃었을 때(버블링X)
fouseinHTML 요소가 포커스를 받았을 때(버블링O)
fouseoutHTML 요소가 포커스를 잃었을 때(버블링O)
submitform 요소 내의 submit 버튼을 클릭했을 때
resetform 요소 내의 reset 버튼을 클릭했을 때
값 변경inputinput(text, checkbox, radio), select, textarea 요소의 값이 입력되었을 때
changeinput(text, checkbox, radio), select, textarea 요소의 값이 변경되었을 때
여기서 변경여부는 해당 HTML요소가 포커스를 잃었을 때 측정된다.
readystatechangeHTML 문서의 로드와 파싱 상태를 나타내는 document.readyState 프로퍼티 값이 번경될 때
DOMDOMContentLoadedHTML 문서의 로드와 파싱이 완료되어 DOM 생성이 완료되었을 때
resize브라우저 윈도우의 크리를 리사이즈할 때 연속적으로 발생(오직 window 객체에서만 발생)
scroll웹페이지 또는 HTML 요소를 스크롤할 때 연속적으로 발생
리소스loadDOMContentLoaded 이벤트가 발생한 이후, 모든 리소스의 로딩이 완료되었을 때
unload리소스가 언로드될 때(주로 새로운 웹페이지를 요청한 경우)
abort리소스 로딩이 중단되었을 때
error리소스 로딩이 실패했을 때

이벤트 핸들러 등록과 삭제

이벤트 핸들러 등록

  1. 어트리뷰트 방식
    HTML 요소의 어트리뷰트로 이벤트 핸들러를 등록하는 방식으로 어트리뷰트의 이름은 onclick과 같이 on접두어와 이벤트 타입을 이어서 나타낸다.
<button onclick="console.log("Hi")" class="btn"></button>
  1. 프로퍼티 방식
    자바스크립트상에서 이벤트를 등록할 요소를 가져와 프로퍼티 방식으로 이벤트 핸들러를 등록한다. 프로퍼티의 이름은 어트리뷰트명과 같이 on접두어와 이벤트 타입을 이어서 나타낸다. 해당 방식은 이벤트 핸들러 프로퍼티에 단 하나의 이벤트 핸들러만 등록할 수 밖에 없다는 단점이 있다.
const btn = document.querySelector('.btn');

btn.onclick = function() {
  console.log("Hi");
}
  1. addEventListener 방식
    가장 많이 쓰이는 방법으로 이벤트 핸들러를 등록시킬 이벤트 타켓 요소에 addEventListener 메서드를 사용해 이벤트 핸들러를 등록하는 방식이다. 하나 이상의 이벤트 핸들러를 등록할 수 있으며 호출은 등록된 순서대로 호출된다. 앞선 두 방식과는 다르게 on 접두어를 빼고 이벤트 타입명만 사용하며 사용법은 다음과 같다.
이벤트타켓.addEventListener('이벤트타입', 이벤트핸들러[, useCapture(기본값은 false)]);

const btn = document.querySelector('.btn');

const sayHi = function() {
  console.log("Hi");
}

btn.addEventListener('click', sayHi);

이벤트 핸들러 삭제

  1. 프로퍼티 방식으로 등록한 경우
    해당 프로퍼티에 null을 할당한다.
const btn = document.querySelector('.btn');

btn.onclick = function() {
  console.log("Hi");
}

btn.onclick = null;
  1. addEventListener 방식으로 등록한 경우
    반대로 removeEventListener 메서드를 사용한다. 이때 addEventListener 메서드를 사용할 때 사용한 인수를 그대로 전달해야 삭제가 가능하며 이벤트 핸들러가 무명함수라면 삭제가 불가능하다. 따라서 이벤트 핸들러로 사용할 함수는 변수나 자료구조에 저장해서 사용하는 편이 좋다.
const btn = document.querySelector('.btn');

const sayHi = function() {
  console.log("Hi");
}

btn.addEventListener('click', sayHi);
btn.removeEventListener('click', sayHi);

이벤트 객체

이벤트가 발생하면 이벤트에 관련한 다양한 정보를 담고 있는 이벤트 객체가 동적으로 생성된다. 생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달된다. 이때 어트리뷰트 방식으로 이벤트 핸들러를 등록했다면 이벤트 객체를 전달받은 매개변수의 이름이 무조건 event여야 한다.

이벤트 객체는 발생한 이벤트 타입에 따라 가지고 있는 정보가 조금씩 다르지만 공통적으로 가지고 있는 프로퍼티가 있다.

  • type : 이벤트 타입
  • target : 이벤트를 발생시킨 DOM 요소
  • currentTarget : 이벤트 핸들러가 바인딩된 DOM 요소
  • eventPhase : 이번트 전파 단계 (0: 이벤트 없음, 1: 캡쳐링, 2: 타깃, 3: 버블링)
  • bubbles : 이벤트를 버블링으로 전파하는지 여부
  • cancelable : preventDefault 메서드를 호출해 이벤트의 기본 동작을 취소할 수 있는지 여부
  • defaultPrevented : preventDefault 메서드를 호출하여 이벤트를 취소했는지 여부
  • isTrusted : 사용자의 행위에 의해 발생한 이벤트인지에 대한 여부 (사용자가 발생시킨 경우 false)
  • timeStamp : 이벤트가 발생한 시각 (1970/01/01/00:00:00부터 현재까지를 밀리초로 표시)

이 외에 마우스 이벤트는 마우스 포인터의 좌표 정보를 나타내는 프로퍼티나 버튼 버튼 정보를 나타내는 프로퍼티를 추가로 가지고 있고, 키보드 이벤트인 경우에도 어떤 키가 눌렀는지, 특수 키가 같이 눌렸는지에 대한 프로퍼티를 따로 가지고 있다.

  • 마우스 이벤트 : 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 요소의 기본 동작 조작

어떤 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이벤트타입 = 이벤트핸들러를반환하는메서드(필요한인수);
profile
I Will be Relaxed Person

0개의 댓글