blog logo
Published on

이벤트

이벤트 드리븐 프로그래밍

브라우저는 클릭, 키보드 입력, 마우스 이동이 일어나면 이를 감지하여 특정한 타입의 이벤트를 발생시킨다. 그래서 이 이벤트 함수를 호출할 떄 이벤트 핸들러(event handler)라 하고, 이 함수의 호출을 위임하는 것을 이벤트 핸들러 등록이라고 한다.

사용자의 인터랙션에 따라 함수를 호출하여 결과를 처리해야 하는데, 이 때 중요한 것은 "언제 이벤트 함수를 호출할 것인가"다. 사용자가 언제 버튼을 클릭할지 정확히 알 수 없으므로 언제 호출해야 할지는 중요한 문제이다.

하지만 다행히도 브라우저는 사용자의 인터랙션(ex. 버튼 클릭을 했을 때)을 감지하여 이벤트를 발생시킬 수 있다. 즉, 개발자가 명시적으로 이벤트 함수를 호출하는 것이 아니라, 특정 요소에 이벤트 함수를 호출할 수 있도록 위임을 해놓고 사용자의 인터랙션에 따라 호출할 수 있도록 구현할 수 있다는 것이다.

코드로 표현하면 다음과 같다.

index.html
<script>
  const $button = document.querySelector("button");
  $button.onClick = () => {
    alert("button click");
  };
</script>

위와 같은 코드처럼 onClick과 같은 이벤트 핸들러 프로퍼티에 함수를 할당하면 해당 이벤트가 발생했을 때 할당한 함수가 브라우저에 의해 호출된다. 그리고 이벤트 중심으로 제어하는 프로그래밍 방식을 이벤트 드리븐 프로그래밍(event-driven-programming)이라 한다.



이벤트 타입

1. 마우스 이벤트

이벤트 타입이벤트 발생 시점
click마우스 버튼을 클릭했을 때
dbclick더블 클릭
mousedown마우스 버튼을 눌렀을 때
mouseup누르고 있던 마우스를 놓았을 때
mousemove커서를 움직였을 때
mouseenter마우스 커서를 HTML 요소 안으로 이동했을 때 (버블링 X)
mouseover마우스 커서를 HTML 요소 안으로 이동했을 때 (버블링 O)
mouseleave마우스 커서를 HTML 요소 밖으로 이동했을 때 (버블링 X)
mouseout마우스 커서를 HTML 요소 안으로 이동했을 때 (버블링 O)

2. 키보드 이벤트

이벤트 타입이벤트 발생 시점
keydown모든 키를 눌렀을 때 발생한다.
단, 문자, 숫자, 특수문자, enter 키를 눌렀을 때는 연속적으로 발생하지만 그 외의 키를 눌렀을 때는 한 번만 발생한다.
keypress문자 키를 눌렀을 때 연속적으로 발생한다.
단, 문자, 숫자, 특수문자, enter 키를 눌렀을 때만 발생한다.
(이 이벤트는 페지되었으므로 사용하지 않을 것을 권장.)
keyup누르고 있던 키를 놓았을 때 한 번만 발생한다.
단, 문자, 숫자, 특수문자, enter 키를 놓았을 때만 발생한다.

3. 포커스 이벤트

이벤트 타입이벤트 발생 시점
focusHTML 요소가 포커스를 받았을 때(버블링 X)
blurHTML 요소가 포커스를 잃었을 때(버블링 X)
focusinHTML 요소가 포커스를 받았을 때(버블링 O)
focusoutHTML 요소가 포커스를 잃었을 때(버블링 O)

4. 폼 이벤트

이벤트 타입이벤트 발생 시점
submitHTML 요소 내의 submit 버튼을 클릭했을 때
resetHTML 요소 내의 reset 버튼을 클릭했을 때 (최근에는 사용 안 함)

5. 값 변경 이벤트

이벤트 타입이벤트 발생 시점
inputinput(text, checkbox, radio), select, textarea 요소의 값이 입력되었을 때
changeinput(text, checkbox, radio), select, textarea 요소의 값이 변경되었을 때,
change 이벤트는 input 이벤트와 달리, 입력이 종료되어 값이 변경될 때 발생한다.
readystatechangeHTML 문서의 로드와 파싱 상태를 나타내는 document.readyState 프로퍼티 값('loading', 'interactive', 'complete')이 변경될 때

6. DOM 뮤테이션 이벤트

이벤트 타입이벤트 발생 시점
DOMContentLoadedHTML 문서의 로드와 파싱이 완료되어 DOM 생성이 완료되었을 때

7. 뷰 이벤트

이벤트 타입이벤트 발생 시점
resize브라우저 윈도우(window)의 크기를 리사이즈할 때 연속적으로 발생한다.
오직 window 객체에서만 발생한다.
scroll웹페이지(document)또는 HTML 요소를 스크롤할 때 연속적으로 발생한다.

8.리소스 이벤트

이벤트 타입이벤트 발생 시점
loadDOMContentLoaded 이벤트가 발생한 후, 모든 리소스(이미지, 폰트 등)의 로딩이 완료되었을 때(주로 window 객체에서 발생)
unload리소스가 언로드될 때(주로 새로운 웹페이지를 요청한 경우)
abort리소스 로딩이 중단되었을 때
error리소스 로딩이 실패했을 때


이벤트 핸들러 등록

이벤트 핸들러는 이벤트가 발생했을 때 브라우저에게 호출을 위임한 함수이다. 그리고 사용자 인터랙션에 따라 이벤트가 발생함을 감지하기 위해 우리는 브라우저에게 이벤트 핸들러의 호출을 위임을 하고, 위임하는 것을 이벤트 핸들러 등록이라고 한다.

이벤트 핸들러 어트리뷰트 방식

HTML 요소의 어트리뷰트 중에서 이벤트에 대응하는 이벤트 핸들러 어트리뷰트가 있다. onClick과 같이 on 접두사와 이벤트의 종류를 나타내는 이벤트 타입으로 이루어져있다.

<button onClick="sayHi('Ayaan')">click me!</button>
<script>
  function sayHi(name: string) {
    console.log(`Hi! ${name}`);
  }
</script>

주의할 점은 이벤트 핸들러 어트리뷰트 값으로 함수 참조가 아닌 함수 호출문 등의 문으로 할당한다는 것이다. 만약 함수 참조가 아니라 함수 호출문을 등록하면 함수 호출문의 평가 결과가 이벤트 핸들러로 등록된다. 그리고 함수가 아닌, 값을 반환하는 함수 호출문을 이벤트 핸들러로 등록하면 브라우저가 이벤트 핸들러를 호출할 수 없다.

하지만 위의 예제에서는 이벤트 핸들러 어트리뷰트 값으로 함수 호출문을 할당했다. 이 때 이벤트 핸들러 어트리뷰트 값은 사실 암묵적으로 생성될 이벤트 핸들러의 함수 몸체를 의미한다.

즉, onClick="sayHi('Ayaan')" 어트리뷰트는 파싱되어 아래와 같은 함수를 암묵적으로 생성하고, 이벤트 핸들러 어트리뷰트 이름과 동일한 키 onclick 이벤트 핸들러 프로퍼티에 할당한다.

function onclick(event) {
  sayHi("Ayaan");
}

이벤트 핸들러 프로퍼티와 어트리뷰트

이처럼 동작하는 이유는 이벤트 핸들러에 인수를 전달하기 위해서다. 만약 이벤트 핸들러 어트리뷰트 값으로 함수 참조를 할당해야 한다면 이벤트 핸들러에 인수를 전달하기 곤란하다.

// 이벤트 핸들러에 인수를 전달하기 곤란하다.
<button onclick="sayHi">click me!</button>


이벤트 핸들러 프로퍼티 방식

이벤트 핸들러의 프로퍼티 키는 이벤트 핸들러 어트리뷰트와 마찬가지로 onclick과 같이 on 접두사와 이벤트의 종류를 나타내는 이벤트 타입으로 이루어져 있다. 이벤트 핸들러 프로퍼티에 함수를 바인딩하면 이벤트 핸들러가 등록된다.

<script>
  const $button = document.querySelector("button");

  // 이벤트 핸들러 프로퍼티에 이벤트 핸들러를 바인딩
  $button.onclick = function () {
    console.log("button click");
  };
</script>

이벤트 핸들러 어트리뷰트 방식과 프로퍼티 방식은 결국 DOM 노드 객체의 이벤트 핸들러 프로퍼티로 변환되므로 결과적으로 이벤트 핸들러 프로퍼티 방식과 동일하다고 할 수 있다.

이벤트 핸들러 프로퍼티 방식은 어트리뷰트 방식의 HTML과 자바스크립트가 뒤섞이는 문제를 해결할 수 있지만, 하나의 이벤트에 하나의 핸들러만 바인딩해야 한다는 단점이 있다.



addEventListener 메서드 방식

addEventListener 메서드

addEventListener의 첫 번째 매개변수에는 on 접두사를 붙이지 않는다. 그리고 두 번째 매개변수에 이벤트 핸들러를 전달한다. 마지막 매개변수는 이벤트를 캐치할 이벤트 전파 단계(캡처링 또는 버블링)를 지정한다. 기본값은 false다.

<button>Click Me!</button>
<script>
  const $button = document.querySelector("button");

  $button.onclick = function () {
    console.log("button click, 프로퍼티 방식");
  };

  $button.addEventListener("click", function () {
    console.log("button click, addEventListener 방식");
  });
</script>

프로퍼티 방식과 addEventListener 방식은 서로에게 영향을 주지 않는다. 따라서 위의 예시에서는 2개의 이벤트 핸들러가 모두 호출된다. 그리고 addEventListener는 프로퍼티 방식과 다르게 하나의 요소에 여러 개의 이벤트 핸들러를 등록할 수 있다.

<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>


이벤트 핸들러 제거

이벤트 핸들러 할당과 반대로 removeEventListener를 통해 이벤트 핸들러를 제거할 수도 있다. addEventListener와 매개변수는 일치하지만, addEventListener에서 전달한 인수와 removeEventListener에 전달한 인수가 일치하지 않으면 이벤트 핸들러가 제거되지 않는다.

<button>Click Me!</button>
<script>
  const $button = document.querySelector("button");

  const handleClick = () => console.log("button click");

  $button.addEventListener("click", handleClick);

  $button.removeEventListener("click", handleClick); // 성공
  $button.removeEventListener("click", handleClick, true); // 실패
</script>

그리고 addEventListener 메서드에 인수로 전달한 등록 이벤트 핸들러와 동일한 함수이어야 한다. 무명 함수일 경우 이벤트 핸들러를 제거할 수 없다. 제거하려면 이벤트 핸들러의 참조를 변수나 자료구조에 저장해서 사용해야 한다.

<script>
  $button.removeEventListener("click", () => console.log("??"));
  // 이름 없는 함수이므로 제거 불가능
</script>

하지만 이름이 있는 이벤트 핸들러 내부에서 removeEventListener 메서드를 호출하여 이벤트 핸들러를 제거하는 것은 가능하다. 그리고 이 때 이벤트 핸들러는 단 한 번만 호출된다.

$button.removeEventListener("click", function foo() {
  console.log("button click");

  $button.removeEventListener("click", foo);
});

기명 함수를 이벤트 핸들러로 등록할 수 없다면 arguments.callee를 사용할 수도 있다.

$button.removeEventListener("click", function foo() {
  console.log("button click");

  $button.removeEventListener("click", arguments.callee);
});

하지만 callee는 코드 최적화를 방해하므로 strict mode에서 사용이 금지된다. 따라서 가급적이면 이벤트 핸들러의 참조 변수나 자료구조에 저장해서 제거하는 편이 좋다.



이벤트 객체

이벤트가 발생하면 이벤트에 관련한 다양한 정보를 담고 있는 이벤트 객체가 동적으로 생성된다. 생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달된다.

<!-- 이벤트 핸들러 어트리뷰트 방식의 경우 event가 아닌 다른 이름으로는 이벤트 객체를 전달받지 못한다. -->
<body onclick="showCoords(event)">
  <!-- 생략 -->
  <script>
    const $message = document.querySelector("message");

    // 클릭 이벤트에 의해 생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달된다.
    function showCoords(e: MouseEvent): void {
      $message.textContent = `clientX: ${e.clientX} clientY: ${e.clientY}`;
    }
  </script>
</body>


이벤트 객체의 상속 구조

이벤트가 발생하면 이벤트 타입에 따라 다양한 타입의 이벤트 객체가 생성된다. Event, UIEvent, MouseEvent 등 모두 생성자 함수다. 따라서 다음과 같이 생성자 함수를 호출하여 이벤트 객체를 생성할 수 있다.

<script>
  // Event 생성자 함수를 호출하여 foo 이벤트 타입의 Event 객체를 생성한다.
  let e = new Event("foo");

  // MouseEvent 생성자 함수를 호출하여 click 이벤트 타입의 MouseEvent 객체를 생성한다.
  e = new MouseEvent("click");

  // KeyboardEvent 생성자 함수를 호출하여 click 이벤트 타입의 KeyboardEvent 객체를 생성한다.
  e = new KeyboardEvent("keyup");

  // InputEvent 생성자 함수를 호출하여 click 이벤트 타입의 InputEvent 객체를 생성한다.
  e = new InputEvent("change");
</script>

이처럼 이벤트가 발생하면 암묵적으로 생성되는 이벤트 객체도 생성자 함수에 의해 생성된다. 그리고 생성된 이벤트 객체는 생성자 함수와 더불어 생성되는 프로토타입으로 구성된 프로토타입 체인의 일원이 된다.

이벤트 객체 중 일부는 사용자의 행위에 의해 생성된 것이고 일부는 자바스크립트 코드에 의해 인위적으로 생성된 것이다. 예를 들어 MouseEvent 타입의 이벤트 객체는 사용자가 마우스를 클릭하거나 이동했을 때 생성되는 이벤트 객체이며, CustomEvent 타입의 이벤트 객체는 자바스크립트 코드에 의해 인위적으로 생성한 이벤트 객체이다.

Event 인터페이스에는 모든 이벤트 객체의 공통 프로퍼티가 정의되어 있다. 즉, 다음 예제와 같이 이벤트 객체의 프로퍼티는 발생한 이벤트의 타입에 따라 달라진다.

<input type="text" />
<input type="checkbox" />
<button>Click me!</button>

<script>
  const $input = document.querySelector("input[type=text]");
  const $checkbox = document.querySelector("input[type=checkbox]");
  const $button = document.querySelector("button");

  // load 이벤트가 발생하면 Event 타입의 이벤트 객체가 생성된다.
  window.onload = console.log;

  // change 이벤트가 발생하면 Event 타입의 이벤트 객체가 생성된다.
  checkbox.onchange = console.log;

  // focus 이벤트가 발생하면 Event 타입의 이벤트 객체가 생성된다.
  input.onfocus = console.log;

  // click 이벤트가 발생하면 Event 타입의 이벤트 객체가 생성된다.
  button.onclick = console.log;
</script>

이벤트 타입에 따라 생성되는 이벤트 객체


이벤트 객체의 공통 프로퍼티

Event.prototype에 정의되어 있는 이벤트 관련 프로퍼티는 UIEvent, CustomEvent, MouseEvent 등 모든 파생 이벤트 객체에 상속된다. 즉, Event 인터페이스의 이벤트 관련 프로퍼티는 모든 이벤트 객체가 상속받는 공통 프로퍼티다.

공통 프로퍼티설명타입
type이벤트 타입string
target이벤트를 발생시킨 DOM요소DOM 요소 노드
currentTarget이벤트 핸들러가 바인딩된 DOM요소DOM 요소 노드
eventPhase이벤트 전파 단계
0:이벤트 없음, 1: 캡처링 단계, 2: 타깃 단계, 3: 버블링 단계
number
bubbles이벤트를 버블링으로 전파하는지 여부
다음 이벤트는 bubbles: false로 버블링 하지 않는다.
- 포커스 이벤트
- 리소스 이벤트: load/unload/abort/error
- 마우스 이벤트: mouseenter/mouseleave
boolean
cancelablepreventDefault 메서드를 호출하여 이벤트의 기본 동작을 취소할 수 있는지 여부
다음 이벤트는 cancelable: false로 취소할 수 없다.
- 포커스 이벤트
- 리소스 이벤트: load/unload/abort/error
- 마우스 이벤트: dbclick/mouseenter/mouseleave
boolean
defaultPreventedpreventDefault 메서드를 호출하여 이벤트를 취소했는지 여부boolean
isTrusted사용자의 행위에 의해 발생한 이벤트인지 여부. 예를들어, click 메서드 또는 dispatchEvent 메서드를 통해 인위적으로 발생시킨 이벤트인 경우 isTrusted는 false다.boolean
timeStamp이벤트가 발생한 시각number

체크박스 요소의 체크 상태가 변경되면 현재 체크 상태를 출력해보자.

<input type="checkbox" />

<script>
  const $checkbox = document.querySelector("input[type=checkbox]");

  $checkbox.onchage = (e) => {
    console.log(Object.getPrototypeOf(e) === Event.prototype); // true
  }

사용자의 입력에 의해 체크 박스 요소의 체크 상태가 변경되면 checked 프로퍼티의 값이 변경되고 change 이벤트가 발생한다. 이때 Event 타입의 이벤트 객체가 생성된다. 이벤트 객체의 target 프로퍼티는 이벤트를 발생시킨 객체를 나타낸다. 따라서 target 프로퍼티가 가리키는 객체는 change 이벤트를 발생시킨 $checkbox이다.

currentTarget 프로퍼티는 이벤트 핸들러가 바인딩된 DOM 요소를 가리킨다. 위의 예제에서는 이벤트를 발생시킨 DOM 요소와 이벤트 핸들러가 바인딩된 DOM 요소는 모두 $checkbox다. 따라서 target 프로퍼티와 currentTarget 프로퍼티는 동일한 객체 $checkbox를 가리킨다.

$checkbox.onchage = (e) => {
  console.log(e.target === e.currentTarget); // true
};


마우스 정보 취득

마우스 이벤트가 발생하면 생성되는 MouseEvent 타입의 이벤트 객체는 다음과 같은 고유의 프로퍼티를 갖는다.

  • 마우스 포인터의 좌표 정보: screenX/screenY, clientX/clientY, pageX/pageY, offsetX/offesetY
  • 버튼 정보: altKey, ctrlKey, shiftKey, button

드래그 이벤트를 예시로 들면, mousedown 이벤트가 발생한 상태에서 mousemove 이벤트가 발생한 시점에 시작하고, mouseup 이벤트가 발생한 시점에 종료한다.

mousemove 이벤트가 발생할 때마다의 마우스 포인터 좌표를 비교하여 드래그 대상의 이동 거리를 계산한다. mouseup 이벤트가 발생하면 드래그가 종료된 것이다.

<head>
  <style>
    .box {
      width: 100px;
      height: 100px;
      background-color: #fff700;
      cursor: pointer;
    }
  </style>
</head>
<div class="box" />
<script>
  // 드래그 대상 요소
  const $box = document.querySelector(".box");
  // 드래그 시작점
  const initailMousePos = { x: 0, y: 0 };
  // 오프셋: 이동할 거리
  const offset = { x: 0, y: 0 };

  // mousemove 이벤트 핸들러
  const move = (e) => {
    offset.x = e.clientX - initailMousePos.x;
    offset.y = e.clientY - initailMousePos.y;

    // translate3d는 GPU를 사용하므로 absolute의 top, left를 사용하는 것보다 빠르다.
    // top, left는 레이아웃에 영향을 준다.
    $box.style.transform = `translate3d(${offset.x}px, ${offset.y}px, 0)`;
  };

  // mousedown 이벤트가 발생하면 드래그 시작 시점의 마우스 포인터 좌표를 지정
  $box.addEventListener("mousedown", (e) => {
    initailMousePos.x = e.clientX - offset.x;
    initailMousePos.y = e.clientY - offset.y;

    // mousedown 이벤트가 발생한 상태에서 mousemove 이벤트가 발생하면 box 요소를 이동시킨다.
    document.addEventListener("mousemove", move);
  });

  // mouseup 이벤트가 발생하면 mousemove 이벤트를 제거해 이동을 멈춘다.
  document.addEventListener("mouseup", () => {
    document.removeEventListener("mousemove", move);
  });
</script>

마우스 정보 취득 구현 예시


키보드 정보 취득

keydown, keyup, keypress 이벤트가 발생하면 생성되는 keyboardEvent 타입의 이벤트 객체를 알아보기 위해 input 요소의 입력 필드에 엔터 키가 입력되면 현재까지 입력 필드에 입력된 값을 출력하는 예제를 만들어보자.

<input type="text" />
<em class="message" />
<script>
  const $input = document.querySelector("input[type=text]");
  const $message = document.querySelector(".message");

  $input.onkeyup = (e) => {
    // e.key는 입력한 키 값을 문자열로 반환한다.
    // 입력한 키가 'Enter', 즉 엔터 키가 아니면 무시한다.
    if (e.key !== "Enter") {
      return;
    }

    // 엔터키가 입력되면 현재까지 입력 필드에 입력된 값을 출력한다.
    $message.textContent = e.target.value;
    e.target.value = "";
  };
</script>

입력한 키와 key 프로퍼티의 대응 관계는 https://keycode.info를 참고하면 된다.

참고로 input 요소의 입력 필드에 한글을 입력하고 엔터 키를 누르면 keyup 이벤트 핸들러가 두 번 호출되는 현상이 발생한다. 이 같은 문제를 회피하려면 keyup 이벤트 대신 keydown 이벤트를 캐치한다.



이벤트 전파

DOM 트리 상에 존재하는 DOM 요소 노드에서 발생한 이벤트는 DOM 트리를 통해 전파된다.

<ul id="fruits">
  <li id="apple">apple</li>
  <li id="banana">banana</li>
</ul>

ul 요소의 두 번째 자식 요소인 li 요소를 클릭하면 클릭 이벤트가 발생한다. 이때 `생성된 이벤트 객체는 이벤트를 발생시킨 DOM 요소인 이벤트 타깃을 중심으로 DOM 트리를 통해 전파된다.


이벤트 전파

다음 예제를 보면서 이벤트를 발생시켜 보자.


<ul id="fruits">
  <li id="apple">apple</li>
  <li id="banana">banana</li>
</ul>
<script>
  const $fruits = document.getElementById("fruits");

  // $fruits 요소의 하위 요소인 li 요소를 클릭한 경우
  $fruits.addEventListener("click", (e: MouseEvnet) => {
    console.log(`이벤트 단계: ${e.eventPhase}`); // 3: 버블링 단계
    console.log(`이벤트 타깃: ${e.target}`); // [object HTMLLIElement]
    console.log(`커렌트 타깃: ${e.currentTarget}`); // [object HTMLLIElement]
  });
</script>

위 예제에서 li 요소가 이벤트 타깃이 되고, 클릭 이벤트 객체는 window에서 시작해서 이벤트 타깃 방향으로 전파된다.(이벤트 캡쳐링) 이후 이벤트 객체는 이벤트를 발생시킨 이벤트 타깃에 도착한다.(이벤트 타깃 단계) 이후 객체는 이벤트 타깃에서 시작해서 window 방향으로 전파된다.(이벤트 버블링)

<ul id="fruits">
  <li id="apple">apple</li>
  <li id="banana">banana</li>
</ul>
<script>
  const $fruits = document.getElementById("fruits");
  const $banana = document.getElementById("banana");

  // $fruits 요소의 캡쳐링 단계의 이벤트를 캐치
  $fruits.addEventListener(
    "click",
    (e: MouseEvnet) => {
      console.log(`이벤트 단계: ${e.eventPhase}`); // 1: 캡쳐링 단계
      console.log(`이벤트 타깃: ${e.target}`); // [object HTMLLIElement]
      console.log(`커렌트 타깃: ${e.currentTarget}`); // [object HTMLLIElement]
    },
    true,
  );

  // 타깃 단계의 이벤트를 캐치
  $banana.addEventListener("click", (e: MouseEvnet) => {
    console.log(`이벤트 단계: ${e.eventPhase}`); // 2: 타깃 단계
    console.log(`이벤트 타깃: ${e.target}`); // [object HTMLLIElement]
    console.log(`커렌트 타깃: ${e.currentTarget}`); // [object HTMLLIElement]
  });

  // $fruits 요소의 캡쳐링 단계의 이벤트를 캐치
  $fruits.addEventListener("click", (e: MouseEvnet) => {
    console.log(`이벤트 단계: ${e.eventPhase}`); // 3: 버블링 단계
    console.log(`이벤트 타깃: ${e.target}`); // [object HTMLLIElement]
    console.log(`커렌트 타깃: ${e.currentTarget}`); // [object HTMLLIElement]
  });
</script>

이처럼 이벤트는 이벤트를 발생시킨 이벤트 타깃은 물론 상위 DOM 요소에서도 캐치할 수 있다. 즉, DOM 트리를 통해 전파되는 이벤트는 모든 DOM 요소에서 모두 캐치할 수 있다.

대부분의 이벤트는 캡쳐링과 버블링을 통해서 전파된다. 하지만 다음 이벤트는 버블링을 통해 전파되지 않는다. 아래 이벤트들은 전부 evnet.bubbles의 값이 false다.

  • 포커스 이벤트: focus/blur
  • 리소스 이벤트: load/unload/abort/error
  • 마우스 이벤트: mouseenter/mouseleave

위 이벤트는 버블링되지 않으므로 상위 요소에서 위 이벤트를 캐치하려면 캡처링 단계의 이벤트를 캐치해야 한다. 하지만 상위에서 캐치할 필요없이 대체 가능한 이벤트가 있다. focusin/focusout, mouseouver/mouseout 등은 버블링을 통해 전파된다.

그리고 각 요소마다 이벤트 핸들러를 등록하면 성능 저하의 원인이 되므로, 이벤트 위임을 통해 상위 DOM 요소에 이벤트 핸들러를 등록하면 여러 개의 하위 DOM 요소에 이벤트 핸들러를 등록할 필요가 없다.

<ul id="fruits">
  <li id="apple">apple</li>
  <li id="banana">banana</li>
</ul>
<em class="message" />
<script>
  const $fruits = document.getElementById("fruits");
  const $message = document.getElementById(".message");

  function activate({ target }) {
    // 이벤트를 발생시킨 DOM 요소가 아니라면 무시한다.
    if (!target.matches("#fruits > li")) {
      return;
    }

    [...$fruits.children].forEach($fruit => {
      $fruit.classList.toggle('active, $fruit === target);
      $message.textContent = target.id;
    })
  }

  // 이벤트 위임: 상위 요소(ul#fruits)는 하위 요소의 이벤트를 캐치할 수 있다.
  $fruits.onclick = activate
</script>

$fruits.onclick = activate 이 부분에서 이벤트 객체의 currentTarget 프로퍼티는 언제나 변함없이 $fruits 요소를 가리키지만 이벤트 객체의 target 프로퍼티는 실제로 이벤트를 발생시킨 DOM 요소를 가리킨다.

$fruits에서 클릭 이벤트가 발생하면 target === currentTarget 이지만, 만약 $fruits 하위 요소에서 클릭이벤트가 발생한다면 currentTarget과 target은 다른 DOM 요소를 가리킨다.



DOM 요소의 기본 동작 조작

DOM 요소는 저마다 기본 동작이 있다. a 요소는 href 어트리뷰트에 지정된 링크로, checkbox 또는 radio 요소를 클릭하면 체크 또는 해제된다.

이벤트 객체의 preventDefault 메서드는 이러한 DOM 요소의 기본 동작을 중단시킨다.

<div class="container">
  <a href="https://www.useonglee.dev">go go</a>
</div>
<script>
  document.querySelector("a").onclick = (e: MouseEvent) => {
    // a 요소의 기본동작을 중단한다.
    e.preventDefault();
  };
</script>


이벤트 전파 방지

<div class="container">
  <button class="button1">button 1</button>
</div>
<script>
  document.querySelector(".button1").onclick = (e: MouseEvent) => {
    // 이벤트 전파 중단
    e.stopPropagation();
  };
</script>

.button2 요소는 자신이 발생시킨 이벤트가 전파되는 것을 중단하여 자신에게 바인딩된 이벤트 핸들러만 실행되도록 한다. 이처럼 stopPropagation 메서드는 하위 DOM 요소의 이벤트를 개별적으로 처리하기 위해 이벤트의 전파를 중단시킨다.



이벤트 핸들러 내부의 this

handleclick 함수 내부의 this는 전역 객체 window를 가리킨다.

<button onclick="handleclick()">button</button>
<script>
  function handleclick() {
    console.log(this); // window
  }
</script>

handleclick 함수는 이벤트 핸들러에 의해 일반 함수로 호출된다. 그리고 일반 함수 내부의 this는 전역 객체를 가리키기 때문에 handleclick 내부의 this는 window를 가리킨다.

하지만 이벤트 핸들러를 호출할 때 인수로 전달한 this는 이벤트를 바인딩한 DOM 요소를 가리킨다.

<button onclick="handleclick(this)">button</button>
<script>
  function handleclick(button) {
    console.log(button); // 이벤트를 바인딩한 button 요소
    console.log(this); // window
  }
</script>


이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식

이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식 모두 이벤트 핸들러 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리킨다. 즉, 이벤트 핸들러 내부의 this는 이벤트 객체의 currentTarget 프로퍼티와 같다.

<button class="button1">0</button>
<button class="button2">0</button>
<script>
  const $button1 = document.querySelector(".button1");
  const $button2 = document.querySelector(".button2");

  // 이벤트 핸들러 프로퍼티 방식
  $button1.onclick = function (e) {
    console.log(this); // $button1
    console.log(e.currentTarget); // $button1
  };

  // aaddEventListener 메서드 방식
  $button2.addEventListener("click", function (e) {
    // this는 이벤트를 바인딩한 DOM 요소를 가리킨다.
    console.log(this); // $button2
    console.log(e.currentTarget); // $button2
  });
</script>

하지만 화살표 함수의 경우 이벤트 핸들러의 내부 this는 상위 스코프의 this를 가리킨다. 화살표 함수는 함수 자체의 this 바인딩을 갖지 않는다.

<button class="button1">0</button>
<button class="button2">0</button>
<script>
  const $button1 = document.querySelector(".button1");
  const $button2 = document.querySelector(".button2");

  // 이벤트 핸들러 프로퍼티 방식
  $button1.onclick = (e) => {
    console.log(this); // window
    console.log(e.currentTarget); // $button1
  };

  // aaddEventListener 메서드 방식
  $button2.addEventListener("click", (e) => {
    console.log(this); // window
    console.log(e.currentTarget); // $button2
  });
</script>


커스텀 이벤트

이벤트가 발생하면 암묵적으로 생성되는 이벤트 객체는 발생한 이벤트의 종류에 따라 이벤트 타입이 결정된다. 하지만 Event, UIEvent, MouseEvent 같은 이벤트 생성자 함수를 호출하여 명시적으로 생성한 이벤트 객체는 임의의 이벤트 타입을 지정할 수 있고, 이를 커스텀 이벤트라고 한다.

첫번째 인자는 이벤트 타입의 문자열을 전달해주지만 CustomeEvent를 통해 새로운 이벤트 타입을 지정할 수도 있다.

const keyboardEvent = new keyboardEvent("keyup");
console.log(keyboardEvent.type); // keyup

// 커스텀 이벤트 객체
const keyboardEvent = new CustomEvent("foo");
console.log(keyboardEvent.type); // foo

커스텀 이벤트 객체는 버블링되지 않으며, preventDefault 메서드로 취소할 수도 없다. 즉, bubbles와 cancelable 프로퍼티의 값이 false로 기본 설정된다.

하지만 두 번째 인자를 통해 기본 설정값을 변경할 수도 있다.

const customEvent = new MouseEvent("click");
console.log(customEvent.type); // click
console.log(customEvent.bubbles); // false

const customEvent = new MouseEvent("click", {
  bubbles: true,
  cancelable: true,
});


커스텀 이벤트 디스패치

생성된 커스텀 이벤트는 dispatchEvent 메서드로 디스패치(이벤트를 발생시키는 행위)할 수 있다. dispatchEvent 메서드에 이벤트 객체를 인수로 전달하면서 호출하면 인수로 전달한 이벤트 타입의 이벤트가 발생한다.

<script>
  const $button = document.querySelector(".btn");

  $button.addEventListener("click", (e: MouseEvent) => {
    alert("clicked");
  });

  const customEvent = new MouseEvent("click");

  // 커스텀 이벤트 디스패치(동기 처리). click 이벤트 발생
  $button.dispatchEvent(customEvent);
</script>

일반적으로 이벤트 핸들러는 비동기 처리 방식으로 동작하지만 dispatchEvent 메서드는 이벤트 핸들러를 동기 처리 방식으로 호출한다. 즉, dispatchEvent 메서드를 호출하면 커스텀 이벤트에 바인딩된 이벤트 핸들러를 직접 호출하는 것과 같다. 따라서, dispatchEvent 메서드로 이벤트를 디스패치하기 이전에 커스텀 이벤트를 처리할 이벤트 핸들러를 등록해야 한다.

기존 이벤트 타입이 아닌 임의의 이벤트 타입을 지정하여 커스텀 이벤트 객체를 생성한 경우 반드시 addEventListener 메서드 방식으로 이벤트 핸들러를 등록해야 한다.

그 이유는 on + 이벤트 타입으로 이루어진 이벤트 핸들러 어트리뷰트/프로퍼티가 요소 노드에 존재하지 않기 때문이다. 즉 'foo'라는 커스텀 이벤트 객체를 생성하더라도, 'onfoo'라는 핸들러 어트리뷰트/프로퍼티가 요소 노드에 존재하지 않기 때문에 어트리뷰트/프로퍼티 방식으로는 이벤트 핸들러를 등록할 수 없다.