본문 바로가기

CS/모던 자바스크립트 Deep Dive

40장 이벤트(2)

반응형

2023년 5월 6일 767p~783p

 

40.5 이벤트 객체

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

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

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

참고로, 이벤트 핸들러 어트리뷰트 방식으로 이벤트 핸들러를 등록했다면 event를 통해 이벤트 객체를 전달받을 수 있다. 즉, 'event'가 아닌 다른 이름으로는 이벤트 객체를 전달받지 못한다.

그 이유는, 이벤트 핸들러 어트리뷰트 값은 사실 암묵적으로 생성되는 이벤트 핸들러의 함수 몸체를 의미하기 때문이다.  onclick="showCoords(event)" 어트리뷰트는 파싱되어 다음과 같은 함수를 암묵적으로 생성하여 onclick 이벤트 핸들러 프로퍼티에 할당한다.

function onclick(event){
  showCoords(event);}

40.5.1 이벤트 객체의 상속 구조

이벤트가 발생하면 이벤트 타입에 따라 다양한 타입의 이벤트 객체가 생성된다. 이벤트 객체는 다음과 같은 상속 구조를 갖는다.

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

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

  • type : 이벤트 타입
  • target : 이벤트를 발생시킨 DOM 요소
  • currentTarget : 이벤트 핸들러가 바인딩된 DOM 요소
  • eventPhase : 이벤트 전파단계 (0 : 이벤트 없음, 1:캡처링 단계, 2: 타깃 단계, 3:버블링단계)
  • 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>

40.5.3 마우스 정보 취득

mouseEvent 타입의 이벤트 객체가 갖는 고유의 프로퍼티들은 다음과 같다.

  • 마우스 포인터의 좌표 정보를 나타내는 프로퍼티 : screenX/screenY, clientX/clientY, pageX/pageY, offsetX/offsetY
  • 버튼 정보를 나타내는 프로퍼티 :altKey, crtlKey, shiftKey, button

예를 들어, DOM 요소를 드래그하여 이동시키는 예제를 보자. 드래그는 마우스 버튼을 누른 상태에서 마우스를 이동하는 것으로 시작하고 마우스 버튼을 떼면 종료한다. 따라서 드래그는 mousedown 이벤트 발생한 상태에서 mousemove 이벤트가 발생한 시점에 시작하고 mouseup 이벤트가 발생한 시점에 종료한다.

드래그가 시작되면 드래그 시작 지점, 즉 mousedown 이벤트가 발생했을 때의 마우스 포인터 좌표와 드래그를 하고 있는 시점, 즉 mousemove 이벤트가 발생할 때마다의 마우스 포인터 좌표를 비교하여 드래그 대상의 이동 거리를 계산한다.

mouseup 이벤트가 발생하면 드래그가 종료한 것이다. 이때 드래그 대상 요소를 이동시키는 이벤트 핸들러를 제거하여 이동을 멈춘다.

이때 clientX/clientY는 뷰포트, 즉 웹페이지의 가시 영역을 기준으로 마우스 포인터 좌표를 나타낸다.

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

40.5.4 키보드 정보 취득

KeyboardEvent 타입의 이벤트 객체가 갖는 고유의 프로퍼티들에는 altKey, ctrlKey, shiftKey, metaKey, key, keyCode 등이 있다.

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

keyup 이벤트가 발생하면 생성되는 KeyboardEvent 타입의 이벤트 객체는 입력한 키 값을 문자열로 반환하는 key 프로퍼티를 제공한다. 참고로 input 요소의 입력 필드에 한글을 입력하고 엔터 키를 누르면 keyup 이벤트 핸들러가 두 번 호출되므로 이를 회피하려면 keydown 이벤트를 캐치한다.

 

40.6 이벤트 전파

DOM 요소 노드에서 발생한 이벤트는 DOM 트리를 통해 전파된다. 이를 이벤트 전파라고 한다.

<!DOCTYPE html>
<html>
<body>
  <ul id="fruits">
    <li id="apple">Apple</li>
    <li id="banana">Banana</li>
    <li id="orange">Orange</li>
  </ul>
</body>
</html>

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

이벤트 전파는 이벤트 객체가 전파되는 방향에 따라 3단계로 구분할 수 있다.

  • 캡처링 단계 : 이벤트가 상위 요소에서 하위 요소 방향으로 전파
  • 타깃 단계 :  이벤트가 이벤트 타깃에 도달
  • 버블링 단계 : 이벤트가 하위 요소에서 상위 요소 방향으로 전파
버블링 : 하위 -> 상위 / 캡쳐링 : 상위 -> 하위

예를 들어, ul 요소에 이벤트 핸들러를 바인딩하고 ul 하위 요소인 li 요소를 클릭해보자.

이때 이벤트 타켓(event.target)은 이벤트가 발생한 곳인 li 요소이고, 커런트 타깃(event.currentTarget)은 이벤트 핸들러를 바인딩 한 ul 요소다.

이 코드는 버블링 단계의 이벤트를 캐치한다. li 요소를 클릭하면 클릭 이벤트 객체가 생성되고 클릭된 li 요소가 이벤트 타깃이 된다. 이때 클릭 이벤트 객체는 window에서 이벤트 타깃 방향으로 전파되고 이것이 캡처링 단계다. 이후 이벤트 객체는 이벤트를 발생시킨 이벤트 타깃에 도달하고 이것이 타깃 단계다. 이후 이벤트 객체는 이벤트 타깃에서 시작해서 window 방향으로 전파되며 이것이 버블링 단계다.

<!DOCTYPE html>
<html>
<body>
  <ul id="fruits">
    <li id="apple">Apple</li>
    <li id="banana">Banana</li>
    <li id="orange">Orange</li>
  </ul>
  <script>
    const $fruits = document.getElementById('fruits');

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

이벤트 핸들러 어트리뷰트.프로퍼티 방식으로 등록한 이벤트 핸들러는 타킷 단계와 버블링 단계의 이벤트만 캐치할 수 있지만, addEventListener 방식은 타킷 단계와 버블링 단계 뿐 아니라 캡처링 단계의 이벤트도 선별적으로 캐치할 수 있다. 캡처링 단계의 이벤트를 캐치하려면 addEventListener 메서드 세번째 파라미터로 true를 전달해야 한다. 생략하거나 false를 전달하면 타깃 단계와 버블링 단계의 이벤트만 캐치할 수 있다.

만약 이벤트 핸들러가 캡처링 단계의 이벤트를 캐치하도록 설정되어 있다면 이벤트 핸들러는 window에서 시작하여 이벤트 타킷 방향으로 전파되는 이벤트 객체를 캐치하고, 이벤트를 발생시킨 이벤트 타깃과 이벤트 핸들러가 바인딩된 커런트 타깃이 같은 DOM 요소라면 이벤트 핸들러는 타깃 단계의 이벤트 객체를 캐치한다.

<!DOCTYPE html>
<html>
<body>
  <ul id="fruits">
    <li id="apple">Apple</li>
    <li id="banana">Banana</li>
    <li id="orange">Orange</li>
  </ul>
  <script>
    const $fruits = document.getElementById('fruits');
    const $banana = document.getElementById('banana');

    // #fruits 요소의 하위 요소인 li 요소를 클릭한 경우
    // 캡처링 단계의 이벤트를 캐치한다.
    $fruits.addEventListener('click', e => {
      console.log(`이벤트 단계: ${e.eventPhase}`); // 1: 캡처링 단계
      console.log(`이벤트 타깃: ${e.target}`); // [object HTMLLIElement]
      console.log(`커런트 타깃: ${e.currentTarget}`); // [object HTMLUListElement]
    }, true);

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

    // 버블링 단계의 이벤트를 캐치한다.
    $fruits.addEventListener('click', e => {
      console.log(`이벤트 단계: ${e.eventPhase}`); // 3: 버블링 단계
      console.log(`이벤트 타깃: ${e.target}`); // [object HTMLLIElement]
      console.log(`커런트 타깃: ${e.currentTarget}`); // [object HTMLUListElement]
    });
  </script>
</body>
</html>

이처럼 이벤트는 이벤트를 발생시킨 이벤트 타깃은 물론 상위 DOM 요소에서도 캐치할 수 있다.

다음 예제는 캡처링 단계의 이벤트와 버블링 단계의 이벤트를 캐치하는 이벤트 핸들러가 혼용되는 경우다.

<!DOCTYPE html>
<html>
<head>
  <style>
    html, body { height: 100%; }
  </style>
<body>
  <p>버블링과 캡처링 이벤트 <button>버튼</button></p>
  <script>
    // 버블링 단계의 이벤트를 캐치
    document.body.addEventListener('click', () => {
      console.log('Handler for body.');
    });

    // 캡처링 단계의 이벤트를 캐치
    document.querySelector('p').addEventListener('click', () => {
      console.log('Handler for paragraph.');
    }, true);

    // 타깃 단계의 이벤트를 캐치
    document.querySelector('button').addEventListener('click', () => {
      console.log('Handler for button.');
    });
  </script>
</body>
</html>

이벤트는 캡처링-타깃-버블링 단계로 전파되므로 만약 button 요소에서 클릭이벤트가 발생하면 다음과 같이 출력되고,

Handler for paragraph. // 캡처링 단계를 캐치하는 p 요소의 이벤트 핸들러 호출
Handler for button. // 이벤트 타깃 단계를 캐치하는 button 요소의 이벤트 핸들러 호출
Handler for body. // 버블링 단계를 캐치하는 body 요소의 이벤트 핸들러 호출

p 요소에서 클릭 이벤트가 발생하면 다음과 같이 출력된다.

Handler for paragraph.
Handler for body.

 

반응형

'CS > 모던 자바스크립트 Deep Dive' 카테고리의 다른 글

42장 비동기 프로그래밍  (1) 2023.05.09
40장 이벤트(3)  (0) 2023.05.08
40장 이벤트(1)  (0) 2023.05.05
39장 DOM(4)  (0) 2023.05.04
39장 DOM(3)  (0) 2023.05.03