[JS] DeepDive(40) 이벤트 (1) - event객체
FrontEnd/Deep Dive

[JS] DeepDive(40) 이벤트 (1) - event객체

728x90

이벤트 드리븐 프로그래밍

 

브라우저는 처리해야 할 특정 사건이 발생하면 이를 감지하여 이벤트를 발생시킨다.예를들어 클릭 , 키보드 입력, 마우스 이동 등이 일어나면 브라우저는 이를 감지하여 특정한 타입의 이벤트를 발생시킨다.

 

이벤트가 발생 했을 때 호출될 함수를 이벤트 핸들러라고 하고 이벤트가 발생 했을 때 브라우저에게 이벤트 핸들러의 호출을 위임하는 것을 이벤트 핸들러 등록이라고 한다.

 

 

 

브라우저는 사용자의 버튼 클릭을 감지해 클릭 이벤트를 발생시킬 수 있다.

 

<!DOCTYPE html>
<html>
<body>
  <button>Click me!</button>
  <script>
    const $button = document.querySelector('button');

    // 사용자가 버튼을 클릭하면 함수를 호출하도록 요청
    $button.onclick = () => { alert('button click'); };
  </script>
</body>
</html>

 

 

위와 같이 이벤트와 그에 대응하는 함수를 통해 사용자와 애플리케이션이 상호작용하며 이런 프로그래밍 방식을 이벤트 드리븐 프로그래밍이라고 한다.

 

 

이벤트 타입

 

이벤트 타입은 이벤트의 종류를 나타내는 문자열이다.

 

https://developer.mozilla.org/en-US/docs/Web/Events

 

Event reference | MDN

Events are fired to notify code of "interesting changes" that may affect code execution. These can arise from user interactions such as using a mouse or resizing a window, changes in the state of the underlying environment (e.g. low battery or media events

developer.mozilla.org

 

위 MDN 문서에서 확인할 수 있다.

 

 

이벤트 핸들러 등록

 

 

이벤트 핸들러는 이벤트가 발생한 경우 브라우저에 호출을 위임한 함수이다.

 

 

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

HTML 요소의 어트리뷰트 중에 이벤트에 대응하는 이벤트 핸들러 어트리뷰트가 있다.

 

<!DOCTYPE html>
<html>
<body>
  <button onclick="sayHi('Lee')">Click me!</button>
  <script>
    function sayHi(name) {
      console.log(`Hi! ${name}.`);
    }
  </script>
</body>
</html>

 

이벤트 핸들러를 등록하는 경우 콜백함수처럼 함수 참조를 등록해야 한다. 함수가 아닌 값을 반환하는 함수호출문을 핸들러로 등록하면 브라우저가 이벤트 핸들러를 호출할 수 없다.

 

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

 

즉, onclick 어트리뷰트는 파싱되어 함수를 암묵적으로 생성하고 onclick 이벤트 핸들러 프로퍼티에 할당된다.

 

function onclick(event) {
  sayHi('Lee');
}

 

이처럼 동자갛는 이유는 핸들러에 인수를 전달하기 위해서이다. 함수 참조를 할당한다면 이벤트 핸들러에 인수를 전달하기 어렵다.

 

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

 

 

이벤트 핸들러 어트리뷰트 방식은 오래된 코드에서 간혹 사용하기에 알아둘 필요는 있지만 HTML과 JS는 관심사가 다르므로 분리하는 것이 좋다.

 

CBD 방식의 Angular/React/Svelte/Vue.js 와같은 프레임워크/라이브러리에서 이벤트 핸들러 어트리뷰트 방식으로 이벤트를 처리한다.

 

CBD방식에서는 HTML,CSS,JS 가 관심사가 다른 개별 요소로 바지 않고 뷰를 구성하기 위한 요소로 보기 때문이다.

 

 

 

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

window 객체와 document, HTMLElement타입의 DOM 객체는 이벤트에 대응하는 이벤트 핸들러 프로퍼티를 가지고 있다.

 

 

 

<!DOCTYPE html>
<html>
<body>
  <button>Click me!</button>
  <script>
    const $button = document.querySelector('button');

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

 

이벤트 핸들러를 등록하기 위해서는 이벤트 타깃 , 이벤트 타입 , 이벤트 핸들러를 지정해야한다.

 

 

 

이벤트 핸들러 어트리뷰트 방식도 DOM 노드 객체의 이벤트 핸들러 프로퍼티로 변환되므로 두 방식은 같은 방식이다.

 

하지만 이벤트 핸들러 프로퍼티 방식은 HTML과 JS가 뒤섞이는 문제를 해결할 수 있다는 장점이 있지만 하나의 이벤트 핸들러에 하나의 이벤트 핸들러만 바인딩할 수 있다는 단점이 있다.

 

<!DOCTYPE html>
<html>
<body>
  <button>Click me!</button>
  <script>
    const $button = document.querySelector('button');

    // 이벤트 핸들러 프로퍼티 방식은 하나의 이벤트에 하나의 이벤트 핸들러만을 바인딩할 수 있다.
    // 첫 번째로 바인딩된 이벤트 핸들러는 두 번째 바인딩된 이벤트 핸들러에 의해 재할당되어
    // 실행되지 않는다.
    $button.onclick = function () {
      console.log('Button clicked 1');
    };

    // 두 번째로 바인딩된 이벤트 핸들러
    $button.onclick = function () {
      console.log('Button clicked 2');
    };
  </script>
</body>
</html>

 

 

 

 

addEventListener 메서드 방식

 

 

 

 

 

<!DOCTYPE html>
<html>
<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>

 

 

 

이벤트 핸들러 프로퍼티 방식은 이벤트 핸들러 프로퍼티에 이벤트 핸들러를 바인딩하지만 addEventListenr 메서드에서는 이벤트 핸들러를 인수로 전달한다. 

 

만약 HTML요소에서 발생한 동일한 이벤트에 대해 두방식을 모두 사용하면 어떻게 될까?

 

<!DOCTYPE html>
<html>
<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('[addEventListener 메서드 방식]button click');
    });
  </script>
</body>
</html>

 

 

addEventListenr 메서드 방식은 이벤트 핸들러 프로퍼티에 바인딩된 이벤트 핸들러에 아무런 영향을 주지 않기 때문에 두 이벤트가 모두 실행된다.

 

HTML요소에서 발생한 동일 이벤트에 대해 이벤트 핸들러 프로퍼티는 하나 이상의 이벤트 핸들러를 등록할 수 없지만 addEventListener 메서드는 하나 이상의 이벤트 핸들러를 등록할 수 있다.

 

<!DOCTYPE html>
<html>
<body>
  <button>Click me!</button>
  <script>
    const $button = document.querySelector('button');

    // addEventListener 메서드는 동일한 요소에서 발생한 동일한 이벤트에 대해
    // 하나 이상의 이벤트 핸들러를 등록할 수 있다.
    $button.addEventListener('click', function () {
      console.log('[1]button click');
    });

    $button.addEventListener('click', function () {
      console.log('[2]button click');
    });
  </script>
</body>
</html>

 

단 addEventListener 메서드를 통해 참조가 동일한 이벤트 핸들러를 중복 등록하는 경우에는 하나의 이벤트 핸들러만 등록된다.

 

<!DOCTYPE html>
<html>
<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>

 

 

이벤트 핸들러 제거

 

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>

 

 

해당 메서드에 인수로 전달한 이벤트 핸들러는 addEventListener 메서드에 인수로 전달한 등록 이벤트 핸들러와 동일한 함수여야 한다. 만약 무명함수를 이벤트로 등록했다면 제거할 수 없다.

 

// 이벤트 핸들러 등록
$button.addEventListener('click', () => console.log('button click'));
// 등록한 이벤트 핸들러를 참조할 수 없으므로 제거할 수 없다.

 

 

단 기명 이벤트 핸들러 내부에서 removeEvent 메서드를 호출하는 경우 가능하다. 이런 예제의 경우 단 한번의 이벤트만 발생하게 된다.

 

// 기명 함수를 이벤트 핸들러로 등록
$button.addEventListener('click', function foo() {
  console.log('button click');
  // 이벤트 핸들러를 제거한다. 따라서 이벤트 핸들러는 단 한 번만 호출된다.
  $button.removeEventListener('click', foo);
});

 

 

기명 함수를 이벤트 핸들러로 등록할 수 없다면 호출된 함수, 즉 함수 자신을 가리키는 arguments.callee를 사용할 수 있다.

 

// 무명 함수를 이벤트 핸들러로 등록
$button.addEventListener('click', function () {
  console.log('button click');
  // 이벤트 핸들러를 제거한다. 따라서 이벤트 핸들러는 단 한 번만 호출된다.
  // arguments.callee는 호출된 함수, 즉 함수 자신을 가리킨다.
  $button.removeEventListener('click', arguments.callee);
});

 

 

 

이벤트 핸들러 프로퍼티 방식으로 등록한 이벤트 핸들러는 removeEventListiener 메서드로 제거할 수 없으며 이벤트 핸들러 프로퍼티에 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}`;
    }

    document.onclick = showCoords;
  </script>
</body>
</html>

 

 

이벤트 핸들러 어트리뷰트 방식으로 이벤트 핸들러를 등록했다면 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>

 

이벤트 핸들러 어트리뷰트 방식의 경우 이벤트 매개변수이름을 꼭 event로 두어야 함을 알고있ㅈ.ㅏ

 

 

이벤트 객체의 상속 구조

이벤트가 발생하면 이벤트 타입에 따라 다양한 타입의 이벤트 객체가 생성된다.

 

위 그림의 Event,UIEvent, MousEvent등 모두가 생성자 함수이다.

 

 

<!DOCTYPE html>
<html>
<body>
  <script>
    // Event 생성자 함수를 호출하여 foo 이벤트 타입의 Event 객체를 생성한다.
    let e = new Event('foo');
    console.log(e);
    // Event {isTrusted: false, type: "foo", target: null, ...}
    console.log(e.type); // "foo"
    console.log(e instanceof Event); // true
    console.log(e instanceof Object); // true

    // FocusEvent 생성자 함수를 호출하여 focus 이벤트 타입의 FocusEvent 객체를 생성한다.
    e = new FocusEvent('focus');
    console.log(e);
    // FocusEvent {isTrusted: false, relatedTarget: null, view: null, ...}

    // MouseEvent 생성자 함수를 호출하여 click 이벤트 타입의 MouseEvent 객체를 생성한다.
    e = new MouseEvent('click');
    console.log(e);
    // MouseEvent {isTrusted: false, screenX: 0, screenY: 0, clientX: 0, ... }

    // KeyboardEvent 생성자 함수를 호출하여 keyup 이벤트 타입의 KeyboardEvent 객체를
    // 생성한다.
    e = new KeyboardEvent('keyup');
    console.log(e);
    // KeyboardEvent {isTrusted: false, key: "", code: "", ctrlKey: false, ...}

    // InputEvent 생성자 함수를 호출하여 change 이벤트 타입의 InputEvent 객체를 생성한다.
    e = new InputEvent('change');
    console.log(e);
    // InputEvent {isTrusted: false, data: null, inputType: "", ...}
  </script>
</body>
</html>

 

따라서 위처럼 생성자 함수로 객체를 만드는 것 또한 가능하다!

 

이벤트가 발생하면 위처럼 이벤트 객체가 생성자 함수에 의해 생성되게 된다.

 

 

 

 

이벤트 객체 중 일부는 사용자의 행위에 의해 생성된 것이며 일부는 JS 코드에 의해 인위적으로 생성된 것이다. 즉 이벤트 객체의 프로퍼티는 발생한 이벤트의 타입에 따라 달라지게 된다.

 

 

<!DOCTYPE html>
<html>
<body>
  <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 이벤트가 발생하면 FocusEvent 타입의 이벤트 객체가 생성된다.
    $input.onfocus = console.log;

    // input 이벤트가 발생하면 InputEvent 타입의 이벤트 객체가 생성된다.
    $input.oninput = console.log;

    // keyup 이벤트가 발생하면 KeyboardEvent 타입의 이벤트 객체가 생성된다.
    $input.onkeyup = console.log;

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

 

 

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

 

Event 인터페이스 즉 Event.prototype에 정의되어 있는 이벤트 관련 프로퍼티는 모든 파생 이벤트 객체에 상속되며 공통 프로퍼티는 아래와 같다!

 

- type : 이벤트 타입

- target : 이벤트를 발생시킨 DOM 요소

- currentTarget : 이벤트 핸들러가 바인딩된 DOM 요소

- eventPhase : 이벤트 전파 단계

- bubbles : 이벤트를 버블링으로 전파하는지 여부

- cancelable : preventDefault 메서드를 호출하여 이벤트의 기본 동작을 취소할 수 있는지 여부

- defaultPrevented : preventDefault 메서드를 호출하여 이벤트를 취소했는지 여부

- isTrusted : 사용자 행위에 의해 발생한 이벤트인지 여부

- timeStamp : 이벤트가 발생한 시각

 

 

체크박스 요소의 체크상태가 변경되면 현재 체크 상태를 출력하게 하는 예제를 보자.

 

<!DOCTYPE html>
<html>
<body>
  <input type="checkbox">
  <em class="message">off</em>
  <script>
    const $checkbox = document.querySelector('input[type=checkbox]');
    const $msg = document.querySelector('.message');

    // change 이벤트가 발생하면 Event 타입의 이벤트 객체가 생성된다.
    $checkbox.onchange = e => {
      console.log(Object.getPrototypeOf(e) === Event.prototype); // true

      // e.target은 change 이벤트를 발생시킨 DOM 요소 $checkbox를 가리키고
      // e.target.checked는 체크박스 요소의 현재 체크 상태를 나타낸다.
      $msg.textContent = e.target.checked ? 'on' : 'off';
    };
  </script>
</body>
</html>

 

 

마우스 정보 취득

 

click,dblclick,mousedown,mouseup 등 이벤트가 발생하면 생성되는 MouseEvent 타입의 이벤트 객체가 있다.

 

 

해당 이벤트를 활용해서 드래그 & 드롭 예제를 만들어보자.

 

<!DOCTYPE html>
<html>
<head>
  <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 };
    // 오프셋: 이동할 거리
    const offset = { x: 0, y: 0 };

    // mousemove 이벤트 핸들러
    const move = e => {
      // 오프셋 = 현재(드래그하고 있는 시점) 마우스 포인터 위치 - 드래그 시작 시점의 마우스 포인터 위치
      offset.x = e.clientX - initialMousePos.x;
      offset.y = e.clientY - initialMousePos.y;

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

    // mousedown 이벤트가 발생하면 드래그 시작 시점의 마우스 포인터 좌표를 저장한다.
    $box.addEventListener('mousedown', e => {
      // 이동 거리를 계산하기 위해 mousedown 이벤트가 발생(드래그를 시작)하면
      // 드래그 시작 시점의 마우스 포인터 좌표(e.clientX/e.clientY: 뷰포트 상에서 현재
      // 마우스의 포인터 좌표)를 저장해 둔다. 한번 이상 드래그로 이동한 경우 move에서
      // translate3d(${offset.x}px, ${offset.y}px, 0)으로 이동한 상태이므로
      // offset.x와 offset.y를 빼주어야 한다.
      initialMousePos.x = e.clientX - offset.x;
      initialMousePos.y = e.clientY - offset.y;

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

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

 

 

 

키보드 정보 취득

 

keydown,keyup,keypress 이벤트가 발생하면 생성되는 KeyboardEvent 타입의 이벤트 객체를 사용하는 예제를 보자.

 

input 요소의 입력 필드에 엔터 키가 입력되면 현재까지 입력 필드에 입력된 값을 출력하는 예제를 만들어 보자.

 

<!DOCTYPE html>
<html>
<body>
  <input type="text" />
  <em class="message"></em>
  <script>
    const $input = document.querySelector('input[type=text]');
    const $msg = document.querySelector('.message');

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

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

 

 

 

 

 

 

728x90