40장 이벤트
40.1 이벤트 드리븐 프로그래밍(40-01)
- 
    
브라우저는 특정 사건이 발생하면 이를 감지하여 이벤트를 발생
 - 
    
이벤트 핸들러 : 이벤트가 발생했을 때 호출될 함수 이벤트 핸들러 등록 : 브라우저에게 이벤트 핸들러의 호출을 위임하는 것
 - 
    
사용자가 아니라 브라우저가 이벤트를 감지하는 역할을 수행할 수 있기 때문에 이벤트 핸들러 등록이 필요한 것이다
 - 
    
이벤트 드리븐 프로그래밍 : 이벤트 중심으로 제어하는 프로그래밍 방식
 
<!DOCTYPE html>
<html>
<body>
  <button>Click me!</button>
  <script>
    const $button = document.querySelector('button');
    // 사용자가 버튼을 클릭하면 함수를 호출하도록 요청
    $button.onclick = () => { alert('button click'); };
  </script>
</body>
</html>
40.2 이벤트 타입
- 이벤트 타입 : 이벤트의 종류를 나타내는 문자열
 
40.2.1 마우스 이벤트
- 
    
click, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseover, mouseleave, mouseout
 - 
    
mouseenter, mouseover은 마우스 커서를 HTML 요소 안으로 이동했을때(버블링X, 버블링O)
 - 
    
mouseleave, mouseout은 위와 반대
 
40.2.2 키보드 이벤트
- keydown, keypress, keyup
 
40.2.3 포커스 이벤트
- 
    
focus, blur, focusin, focusout
 - 
    
focus, blur둘다 버블링X이고 focusin, focusout은 버블링O focus, focusin둘다 HTML요소가 포커스 받았을때 blur, focusout은 포커스 잃었을때
 
40.2.4 폼 이벤트
- submit, reset
 
40.2.5 값 변경 이벤트
- input, change, readystatechange
 
40.2.6 DOM 뮤테이션 이벤트
- DOMContentLoaded : 파싱완료해서 DOM 생성 완료했을때
 
40.2.7 뷰 이벤트
- resize, scroll
 
40.2.8 리소스 이벤트
- 
    
load, unload, abort, error
 - 
    
load : DOMContentLoaded 이벤트 발생 이후, 모든 리로스 로딩 완료때 unload : 리소스가 언로드될 때(주로 새로운 웹페이지 요청한 경우) abort : 리소스 로딩이 중단되었을 때 error : 리소스 로딩이 실패했을 때
 
40.3 이벤트 핸들러 등록
- 이벤트 핸들러 등록방법 3가지
 
40.3.1 이벤트 핸들러 어트리뷰트 방식(40-02)
- 
    
HTML 요소의 어트리뷰트 중에는 이벤트에 대응하는 이벤트 핸들러 어트리뷰트가 있다
 - 
    
on접두사+이벤트타입으로 이름은 이루어짐
 
<!DOCTYPE html>
<html>
<body>
  <button onclick="sayHi('Lee')">Click me!</button>
  <script>
    function sayHi(name) {
      console.log(`Hi! ${name}.`);
    }
  </script>
</body>
</html>
40-03
- 이벤트 핸들러 어트리뷰트 값은 암묵적으로 생성될 이벤트 핸들러의 함수 몸체를 의미
 
function onclick(event) {
  sayHi('Lee');
}
40-04
- 함수 참조를 할당한다면 인수 전달이 곤란하기 때문
 
<!-- 이벤트 핸들러에 인수를 전달하기 곤란하다. -->
<button onclick="sayHi">Click me!</button>
40-05
- 여러개 가능
 
<button onclick="console.log('Hi! '); console.log('Lee');">Click me!</button>
40-06
- HTML과 자바스크립트는 관심사가 다르므로 혼재하는 것보다 분리를 추천해서 이 방식은 추천하지 않음. 그러나 이방식으로 이벤트 처리하는 프레임워크/라이브러리도 많아서 알아두자
 
<!-- Angular -->
<button (click)="handleClick($event)">Save</button>
{ /* React */ }
<button onClick={handleClick}>Save</button>
<!-- Svelte -->
<button on:click={handleClick}>Save</button>
<!-- Vue.js -->
<button v-on:click="handleClick($event)">Save</button>
40.3.2 이벤트 핸들러 프로퍼티 방식(40-07)
- 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>
40-08
- 단점은 하나의 이벤트 핸들러만 바인딩
 
<!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>
40.3.3 addEventListener 메서드 방식(40-09)
- 
    
EventTarget.prototype.addEventListener 메서드 사용
 - 
    
인수1 : 이벤트 타입(on접두사X) 인수2 : 이벤트 핸들러 인수3 : true : capturing, false : bubbling(기본값)
 
<!DOCTYPE html>
<html>
<body>
  <button>Click me!</button>
  <script>
    const $button = document.querySelector('button');
    // 이벤트 핸들러 프로퍼티 방식 - 바인딩, 1개 이벤트핸들러 등록
    // $button.onclick = function () {
    //   console.log('button click');
    // };
    // addEventListener 메서드 방식 - 인수로전달, 1개 이상(등록순)
    $button.addEventListener('click', function () {
      console.log('button click');
    });
  </script>
</body>
</html>
40-10
- 두 방식을 모두 사용하면 바인딩된 이벤트 핸들러엔 addEventListener가 아무런 영향을 주지않아서 한번의 클릭에 2개의 이벤트 핸들러 호출함
 
<!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>
40-11
- 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>
40-12
- 단, 참조가 동일한 이벤트 핸들러를 중복 등록시 하나만 등록
 
<!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>
40.4 이벤트 핸들러 제거(40-13)
- 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>
40-14
- 무명 함수는 제거불가. 즉, 참조를 변수나 자료구조에 저장이 필요
 
// 이벤트 핸들러 등록
$button.addEventListener('click', () => console.log('button click'));
// 등록한 이벤트 핸들러를 참조할 수 없으므로 제거할 수 없다.
40-15
- 기명 함수는 간단히 바로 제거가 가능
 
// 기명 함수를 이벤트 핸들러로 등록
$button.addEventListener('click', function foo() {
  console.log('button click');
  // 이벤트 핸들러를 제거한다. 따라서 이벤트 핸들러는 단 한 번만 호출된다.
  $button.removeEventListener('click', foo);
});
40-16
- 함수 자신을 가리키는 arguments.callee를 사용해도 되지만, 코드 최적화에 방해하므로 추천하진 않음
 
// 무명 함수를 이벤트 핸들러로 등록
$button.addEventListener('click', function () {
  console.log('button click');
  // 이벤트 핸들러를 제거한다. 따라서 이벤트 핸들러는 단 한 번만 호출된다.
  // arguments.callee는 호출된 함수, 즉 함수 자신을 가리킨다.
  $button.removeEventListener('click', arguments.callee);
});
40-17
- 이벤트 핸들러 프로퍼티 방식으로 등록한 경우 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>
40.5 이벤트 객체(40-18)
- 이벤트 발생시 이벤트 객체가 동적으로 생성되고, 이벤트 핸들러의 첫 번째 인수로 전달
 
<!DOCTYPE html>
<html>
<body>
  <p>클릭하세요. 클릭한 곳의 좌표가 표시됩니다.</p>
  <em class="message"></em>
  <script>
    const $msg = document.querySelector('.message');
    // 클릭 이벤트에 의해 생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달된다.
    function showCoords(e) { // e나 아무거나 상관없음
      $msg.textContent = `clientX: ${e.clientX}, clientY: ${e.clientY}`;
    }
    document.onclick = showCoords;
  </script>
</body>
</html>
40-19
- 이벤트 핸들러 어트리뷰트 방식인 경우 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>
40-20
- event로 꼭 해야하는 이유는 암묵적으로 생성되는 이벤트 핸들러의 함수 몸체를 의미하기 때문
 
function onclick(event) {
  showCoords(event);
}
40.5.1 이벤트 객체의 상속 구조(40-21)
- 이벤트 객체의 상속 구조는 책의 그림 확인(많아서)
 
<!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>
40-22
- 이벤트 객체의 프로퍼티는 발생한 이벤트의 타입에 따라 달라짐
 
<!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>
40.5.2 이벤트 객체의 공통 프로퍼티(40-23)
- 
    
이벤트 객체의 공통 프로퍼티는 type, target, currentTraget 등등..
 - 
    
현재 체크상태를 출력해보자(체크 상태 변경될시)
 
<!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-24
- 일반적으로 아래 예시처럼 동일하지만 서로 다른 DOM요소를 가리킬수도 있다(40.7절 “이벤트 위임”참고)
 
$checkbox.onchange = e => {
  // e.target은 change 이벤트를 발생시킨 DOM 요소 $checkbox를 가리키고
  // e.currentTarget은 이벤트 핸들러가 바인딩된 DOM 요소 $checkbox를 가리킨다.
  console.log(e.target === e.currentTarget); // true
  $msg.textContent = e.target.checked ? 'on' : 'off';
};
40.5.3 마우스 정보 취득(40-25)
- 
    
마우스 포인터 좌표정보 프로퍼티 : screenX/Y, clientX/Y, pageX/Y offsetX/Y
 - 
    
버튼 정보 프로퍼티 : altKey, ctrlKey, shiftKey, button
 - 
    
아래 예시의 결과는 책의 그림 확인
 
<!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 키보드 정보 취득(40-26)
- 현재까지 입력된 값을 출력하는 예제
 
<!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>
40.6 이벤트 전파(40-27)
- 
    
이벤트 전파 : DOM트리상의 DOM요소노드에서 발생한 이벤트는 DOM트리를 통해전파
 - 
    
예로 li를 클릭시? window객체부터 차례로 내려와서(1) li를 만남(2) 그리고 다시 window객체로 차례로 되돌아감(3)
 - 
    
1.캡처링 단계 : 이벤트가 상위->하위요소로 전파 2.타깃 단계 : 이벤트가 이벤트 타깃에 도달 3.버블링 단계 : 이벤트가 하위->상위요소로 전파
 
<!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>
40-28
- 
    
ul 요소의 하위 요소인 li 요소를 클릭한 경우
 - 
    
이벤트 타깃은 li 요소, 커런트 타깃은 ul 요소다
 
<!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>
40-29
- 
    
이벤트 핸들러 어트리뷰트/프로퍼티 방식은 타깃, 버블링 단계만 가능
 - 
    
addEventListener방식은 추가로 캡처링 단계도 가능 캡처링 단계를 캐치하도록 코드 작성시 캐치함(아래 예제) - 인수로 true를 보내야함
 
<!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>
40-30
- 
    
캡처링, 버블링 단계의 이벤트를 캐치하는 이벤트 핸들러가 혼용된 경우
 - 
    
button 요소에서 클릭 이벤트 발생경우 p->button->body 순
 - 
    
p 요소에서 클릭 이벤트 발생경우 p->body 순
 
<!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>
40.7 이벤트 위임(40-31)
- 
    
이벤트 위임 : 상위 요소는 하위 요소의 이벤트를 캐치할 수 있다
 - 
    
내비게이션 아이템(li 요소)클릭시 active클래스를 추가하고 그 외의 나머지 아이템의 active클래스는 제거하는 예제
 
<!DOCTYPE html>
<html>
<head>
  <style>
    #fruits {
      display: flex;
      list-style-type: none;
      padding: 0;
    }
    #fruits li {
      width: 100px;
      cursor: pointer;
    }
    #fruits .active {
      color: red;
      text-decoration: underline;
    }
  </style>
</head>
<body>
  <nav>
    <ul id="fruits">
      <li id="apple" class="active">Apple</li>
      <li id="banana">Banana</li>
      <li id="orange">Orange</li>
    </ul>
  </nav>
  <div>선택된 내비게이션 아이템: <em class="msg">apple</em></div>
  <script>
    const $fruits = document.getElementById('fruits');
    const $msg = document.querySelector('.msg');
    // 사용자 클릭에 의해 선택된 내비게이션 아이템(li 요소)에 active 클래스를 추가하고
    // 그 외의 모든 내비게이션 아이템의 active 클래스를 제거한다.
    function activate({ target }) {
      [...$fruits.children].forEach($fruit => {
        $fruit.classList.toggle('active', $fruit === target);
        $msg.textContent = target.id;
      });
    }
    // 모든 내비게이션 아이템(li 요소)에 이벤트 핸들러를 등록한다.
    document.getElementById('apple').onclick = activate;
    document.getElementById('banana').onclick = activate;
    document.getElementById('orange').onclick = activate;
  </script>
</body>
</html>
40-32
- 위의 예제를 수정해서 이벤트 위임을 사용해 보겠다
 
<!DOCTYPE html>
<html>
<head>
  <style>
    #fruits {
      display: flex;
      list-style-type: none;
      padding: 0;
    }
    #fruits li {
      width: 100px;
      cursor: pointer;
    }
    #fruits .active {
      color: red;
      text-decoration: underline;
    }
  </style>
</head>
<body>
  <nav>
    <ul id="fruits">
      <li id="apple" class="active">Apple</li>
      <li id="banana">Banana</li>
      <li id="orange">Orange</li>
    </ul>
  </nav>
  <div>선택된 내비게이션 아이템: <em class="msg">apple</em></div>
  <script>
    const $fruits = document.getElementById('fruits');
    const $msg = document.querySelector('.msg');
    // 사용자 클릭에 의해 선택된 내비게이션 아이템(li 요소)에 active 클래스를 추가하고
    // 그 외의 모든 내비게이션 아이템의 active 클래스를 제거한다.
    function activate({ target }) {
      // 이벤트를 발생시킨 요소(target)가 ul#fruits의 자식 요소가 아니라면 무시한다.
      if (!target.matches('#fruits > li')) return;
      [...$fruits.children].forEach($fruit => {
        $fruit.classList.toggle('active', $fruit === target);
        $msg.textContent = target.id;
      });
    }
    // 이벤트 위임: 상위 요소(ul#fruits)는 하위 요소의 이벤트를 캐치할 수 있다.
    $fruits.onclick = activate;
  </script>
</body>
</html>
40-33
- Element.prototpye.matches 메서드는 특정 노드 탐색 가능한지 확인
 
function activate({ target }) {
  // 이벤트를 발생시킨 요소(target)이 ul#fruits의 자식 요소가 아니라면 무시한다.
  if (!target.matches('#fruits > li')) return;
  ...
40-34
- 상위 DOM 요소에 이벤트 바인딩한 경우 currentTarget은 변함없이 $fruits 요소 가리키지만, target은 실제로 이벤트 발생시킨 DOM 요소를 가리킴
 
$fruits.onclick = activate;
40.8 DOM 요소의 기본 동작의 조작
40.8.1 DOM 요소의 기본 동작 중단(40-35)
- 
    
DOM 요소는 저마다 기본 동작이 있다(예로 href는 링크 이동)
 - 
    
preventDefault 메서드는 기본 동작을 중단시킨다
 
<!DOCTYPE html>
<html>
<body>
  <a href="https://www.google.com">go</a>
  <input type="checkbox">
  <script>
    document.querySelector('a').onclick = e => {
      // a 요소의 기본 동작을 중단한다.
      e.preventDefault();
    };
    document.querySelector('input[type=checkbox]').onclick = e => {
      // checkbox 요소의 기본 동작을 중단한다.
      e.preventDefault();
    };
  </script>
</body>
</html>
40.8.2 이벤트 전파 방지(40-36)
- stopPropagation 메서드는 이벤트 전파를 중지시킨다
 
<!DOCTYPE html>
<html>
<body>
  <div class="container">
    <button class="btn1">Button 1</button>
    <button class="btn2">Button 2</button>
    <button class="btn3">Button 3</button>
  </div>
  <script>
    // 이벤트 위임. 클릭된 하위 버튼 요소의 color를 변경한다.
    document.querySelector('.container').onclick = ({ target }) => {
      if (!target.matches('.container > button')) return;
      target.style.color = 'red';
    };
    // .btn2 요소는 이벤트를 전파하지 않으므로 상위 요소에서 이벤트를 캐치할 수 없다.
    document.querySelector('.btn2').onclick = e => {
      e.stopPropagation(); // 이벤트 전파 중단
      e.target.style.color = 'blue';
    };
  </script>
</body>
</html>
40.9 이벤트 핸들러 내부의 this
40.9.1 이벤트 핸들러 어트리뷰트 방식(40-37)
- 일반 함수로 호출된걸로 봄
 
<!DOCTYPE html>
<html>
<body>
  <button onclick="handleClick()">Click me</button>
  <script>
    function handleClick() {
      console.log(this); // window
    }
  </script>
</body>
</html>
40-38
- 
    
앞에서 배운 암묵적으로 이벤트 핸들러 함수를 생성해서 어트리뷰트 값인 함수 할당부분은 함수 몸체가 될거라 했기 때문에 this를 인수로 보내주면 이벤트를 바인딩한 DOM 요소를 가리킴
 - 
    
즉, handleClick()이것은 암묵적 생성한 이벤트 핸들러 함수의 몸체 부분에 선언이 되어있고 일반 함수 호출로 밖에 볼 수 없었던것
 
<!DOCTYPE html>
<html>
<body>
  <button onclick="handleClick(this)">Click me</button>
  <script>
    function handleClick(button) {
      console.log(button); // 이벤트를 바인딩한 button 요소
      console.log(this);   // window
    }
  </script>
</body>
</html>
이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식(40-39)
- 두 방식 전부 이벤트 핸들러 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리킴. 즉, currentTarget 프로퍼티와 같음
 
<!DOCTYPE html>
<html>
<body>
  <button class="btn1">0</button>
  <button class="btn2">0</button>
  <script>
    const $button1 = document.querySelector('.btn1');
    const $button2 = document.querySelector('.btn2');
    // 이벤트 핸들러 프로퍼티 방식
    $button1.onclick = function (e) {
      // this는 이벤트를 바인딩한 DOM 요소를 가리킨다.
      console.log(this); // $button1
      console.log(e.currentTarget); // $button1
      console.log(this === e.currentTarget); // true
      // $button1의 textContent를 1 증가시킨다.
      ++this.textContent;
    };
    // addEventListener 메서드 방식
    $button2.addEventListener('click', function (e) {
      // this는 이벤트를 바인딩한 DOM 요소를 가리킨다.
      console.log(this); // $button2
      console.log(e.currentTarget); // $button2
      console.log(this === e.currentTarget); // true
      // $button2의 textContent를 1 증가시킨다.
      ++this.textContent;
    });
  </script>
</body>
</html>
40-40
- 화살표 함수의 경우
 
<!DOCTYPE html>
<html>
<body>
  <button class="btn1">0</button>
  <button class="btn2">0</button>
  <script>
    const $button1 = document.querySelector('.btn1');
    const $button2 = document.querySelector('.btn2');
    // 이벤트 핸들러 프로퍼티 방식
    $button1.onclick = e => {
      // 화살표 함수 내부의 this는 상위 스코프의 this를 가리킨다.
      console.log(this); // window
      console.log(e.currentTarget); // $button1
      console.log(this === e.currentTarget); // false
      // this는 window를 가리키므로 window.textContent에 NaN(undefined + 1)을 할당한다.
      ++this.textContent;
    };
    // addEventListener 메서드 방식
    $button2.addEventListener('click', e => {
      // 화살표 함수 내부의 this는 상위 스코프의 this를 가리킨다.
      console.log(this); // window
      console.log(e.currentTarget); // $button2
      console.log(this === e.currentTarget); // false
      // this는 window를 가리키므로 window.textContent에 NaN(undefined + 1)을 할당한다.
      ++this.textContent;
    });
  </script>
</body>
</html>
40-41
- 클래스의 경우 this에 주의
 
<!DOCTYPE html>
<html>
<body>
  <button class="btn">0</button>
  <script>
    class App {
      constructor() {
        this.$button = document.querySelector('.btn');
        this.count = 0;
        // increase 메서드를 이벤트 핸들러로 등록
        this.$button.onclick = this.increase;
      }
      increase() {
        // 이벤트 핸들러 increase 내부의 this는 DOM 요소(this.$button)를 가리킨다.
        // 따라서 this.$button은 this.$button.$button과 같다.
        this.$button.textContent = ++this.count;
        // -> TypeError: Cannot set property 'textContent' of undefined
      }
    }
    new App();
  </script>
</body>
</html>
40-42
- 
    
이벤트 핸들러 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리킴
 - 
    
bind 메서드 사용해서 인스턴스 가리키게 수정 가능
 
<!DOCTYPE html>
<html>
<body>
  <button class="btn">0</button>
  <script>
    class App {
      constructor() {
        this.$button = document.querySelector('.btn');
        this.count = 0;
        // increase 메서드를 이벤트 핸들러로 등록
        // this.$button.onclick = this.increase;
        // increase 메서드 내부의 this가 인스턴스를 가리키도록 한다.
        this.$button.onclick = this.increase.bind(this);
      }
      increase() {
        this.$button.textContent = ++this.count;
      }
    }
    new App();
  </script>
</body>
</html>
40-43
- 화살표 함수를 써도 해결되지만, 이때 이벤트 핸들러 increase는 프로토타입 메서드가 아닌 인스턴스 메서드가 된다
 
<!DOCTYPE html>
<html>
<body>
  <button class="btn">0</button>
  <script>
    class App {
      constructor() {
        this.$button = document.querySelector('.btn');
        this.count = 0;
        // 화살표 함수인 increase를 이벤트 핸들러로 등록
        this.$button.onclick = this.increase;
      }
      // 클래스 필드 정의
      // increase는 인스턴스 메서드이며 내부의 this는 인스턴스를 가리킨다.
      increase = () => this.$button.textContent = ++this.count;
    }
    new App();
  </script>
</body>
</html>
40.10 이벤트 핸들러에 인수 전달(40-44)
- 
    
이벤트 핸들러 어트리뷰트 방식은 함수 호출문을 사용해서 인수를 전달 그러나, 이벤트 핸들러 프로퍼티와 addEventListener방식은 함수 호출문이 아닌 함수 자체를 등록함으로 인수 전달 불가
 - 
    
하지만, 이벤트 핸들러 내부에서 함수를 호출하면서 인수 전달하면 됨
 
<!DOCTYPE html>
<html>
<body>
  <label>User name <input type='text'></label>
  <em class="message"></em>
  <script>
    const MIN_USER_NAME_LENGTH = 5; // 이름 최소 길이
    const $input = document.querySelector('input[type=text]');
    const $msg = document.querySelector('.message');
    const checkUserNameLength = min => {
      $msg.textContent
        = $input.value.length < min ? `이름은 ${min}자 이상 입력해 주세요` : '';
    };
    // 이벤트 핸들러 내부에서 함수를 호출하면서 인수를 전달한다.
    $input.onblur = () => {
      checkUserNameLength(MIN_USER_NAME_LENGTH);
    };
  </script>
</body>
</html>
40-45
- 또는 이벤트 핸들러를 반환하는 함수를 호출하면서 인수를 전달해도 됨
 
<!DOCTYPE html>
<html>
<body>
  <label>User name <input type='text'></label>
  <em class="message"></em>
  <script>
    const MIN_USER_NAME_LENGTH = 5; // 이름 최소 길이
    const $input = document.querySelector('input[type=text]');
    const $msg = document.querySelector('.message');
    // 이벤트 핸들러를 반환하는 함수
    const checkUserNameLength = min => e => {
      $msg.textContent
        = $input.value.length < min ? `이름은 ${min}자 이상 입력해 주세요` : '';
    };
    // 이벤트 핸들러를 반환하는 함수를 호출하면서 인수를 전달한다.
    $input.onblur = checkUserNameLength(MIN_USER_NAME_LENGTH);
  </script>
</body>
</html>
40.11 커스텀 이벤트
40.11.1 커스텀 이벤트 생성(40-46)
- 
    
커스텀 이벤트 : 개발자의 의도로 생성된 이벤트
 - 
    
기존 이벤트 타입을 사용해도 되고, 직접 문자열로 타입을 지정해도 됨
 
// KeyboardEvent 생성자 함수로 keyup 이벤트 타입의 커스텀 이벤트 객체를 생성
const keyboardEvent = new KeyboardEvent('keyup');
console.log(keyboardEvent.type); // keyup
// CustomEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체를 생성
const customEvent = new CustomEvent('foo');
console.log(customEvent.type); // foo
40-47
- 생성된 커스텀 이벤트 객체는 버블링 되지 않고, prevenDefault 메서드로 취소도 불가
 
// MouseEvent 생성자 함수로 click 이벤트 타입의 커스텀 이벤트 객체를 생성
const customEvent = new MouseEvent('click');
console.log(customEvent.type); // click
console.log(customEvent.bubbles); // false
console.log(customEvent.cancelable); // false
40-48
- true로 설정해야 버블링, 취소 가능하다
 
// MouseEvent 생성자 함수로 click 이벤트 타입의 커스텀 이벤트 객체를 생성
const customEvent = new MouseEvent('click', {
  bubbles: true,
  cancelable: true
});
console.log(customEvent.bubbles); // true
console.log(customEvent.cancelable); // true
40-49
- clientX같이 고유의 프로퍼티도 직접 커스텀 가능
 
// MouseEvent 생성자 함수로 click 이벤트 타입의 커스텀 이벤트 객체를 생성
const mouseEvent = new MouseEvent('click', {
  bubbles: true,
  cancelable: true,
  clientX: 50,
  clientY: 100
});
console.log(mouseEvent.clientX); // 50
console.log(mouseEvent.clientY); // 100
// KeyboardEvent 생성자 함수로 keyup 이벤트 타입의 커스텀 이벤트 객체를 생성
const keyboardEvent = new KeyboardEvent('keyup', { key: 'Enter' });
console.log(keyboardEvent.key); // Enter
40-50
- 커스텀 이벤트는 isTrusted 값이 항상 false 커스텀이 아니면 isTrusted 값이 항상 true
 
// InputEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체를 생성
const customEvent = new InputEvent('foo');
console.log(customEvent.isTrusted); // false
40.11.2 커스텀 이벤트 디스패치(40-51)
- 
    
생성된 커스텀 이벤트는 dispatchEvent 메서드로 디스패치(이벤트를 발생시키는 행위) 가능
 - 
    
일반적으로 이벤트 핸들러는 비동기 처리 방식인데, dispatchEvent는 동기 처리 방식이다. 즉, 직접 호출한것과 같다
 
<!DOCTYPE html>
<html>
<body>
  <button class="btn">Click me</button>
  <script>
    const $button = document.querySelector('.btn');
    // 버튼 요소에 click 커스텀 이벤트 핸들러를 등록
    // 커스텀 이벤트를 디스패치하기 이전에 이벤트 핸들러를 등록해야 한다.
    $button.addEventListener('click', e => {
      console.log(e); // MouseEvent {isTrusted: false, screenX: 0, ...}
      alert(`${e} Clicked!`);
    });
    // 커스텀 이벤트 생성
    const customEvent = new MouseEvent('click');
    // 커스텀 이벤트 디스패치(동기 처리). click 이벤트가 발생한다.
    $button.dispatchEvent(customEvent);
  </script>
</body>
</html>
40-52
// CustomEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체를 생성
const customEvent = new CustomEvent('foo');
console.log(customEvent.type); // foo
40-53
- 
    
CustomeEvent 생성자 함수 두번째 인수로 전달하고 싶은 정보를 담은 detail 프로퍼티를 포함하는 객체를 전달 할 수 있다
 - 
    
그리고 기존 타입이 아닌 커스텀한 타입으로 생성한 경우 반드시 addEventListener 방식이용할것. onfoo나 이런게 존재할리가 없기때문
 
<!DOCTYPE html>
<html>
<body>
  <button class="btn">Click me</button>
  <script>
    const $button = document.querySelector('.btn');
    // 버튼 요소에 foo 커스텀 이벤트 핸들러를 등록
    // 커스텀 이벤트를 디스패치하기 이전에 이벤트 핸들러를 등록해야 한다.
    $button.addEventListener('foo', e => {
      // e.detail에는 CustomEvent 함수의 두 번째 인수로 전달한 정보가 담겨 있다.
      alert(e.detail.message);
    });
    // CustomEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체를 생성
    const customEvent = new CustomEvent('foo', {
      detail: { message: 'Hello' } // 이벤트와 함께 전달하고 싶은 정보
    });
    // 커스텀 이벤트 디스패치
    $button.dispatchEvent(customEvent);
  </script>
</body>
</html>
댓글남기기