모던 자바스크립트 Deep Dive 40장-이벤트

HustleKang·2022년 5월 16일

이벤트 드리븐 프로그래밍

브라우저는 스스로 감지해서 특정 사건이 발생하면 해당 이벤트를 발생시킨다
특정 이벤트에 대해 어떤 일을 하고 싶다면
우리는 브라우저에게 특정 이벤트가 발생 했을 때 호출될 함수를 알려주어 브라우저에게 호출을 위임한다
왜냐하면 우리는 특정 이벤트가 언제 발생할지 알 수 없으므로, 이벤트를 감지할 수 있는 브라우저에게 전적으로 위임하는 것

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

이벤트와 이벤트 핸들러를 통해 사용자와 애플리케이션은 상호작용한다
프로그램의 흐름을 이벤트 중심으로 제어하는 프로그램이 방식을 이벤트 드리븐 프로그래밍 이라함

이벤트 타입

이벤트의 종류를 나타내는 문자열

마우스 이벤트

이벤트 타입 이벤트 발생 시점
click
dbclick
mousedown
mouseup
mousemove
mouseenter 마우스 커서를 HTML 요소 안으로 이동했을 때

버블링 X

mouseover 마우스 커서를 HTML 요소 안으로 이동했을 때

버블링 O

mouseleave 마우스 커서를 HTML 요소 밖으로 이동했을 때

버블링 X

mouseout 마우스 커서를 HTML 요소 안으로 이동했을 때

버블링 O

키보드 이벤트

이벤트 타입 이벤트 발생 시점
keydown command,option,control,shift 키는 1번만 발생

나머지 키들은 누르고 있으면 계속 발생

keyup 누른 키를 놓았을 때 1번 발생
keypress command,option,control,shift,delete,esc 키는 이벤트 발생 안함,

폐지되었으므로 사용 권장X

포커스 이벤트

이벤트 타입 이벤트 발생 시점
focus HTML 요소가 포커스 받았을 때

버블링 X

blur HTML 요소가 포커스를 잃었을 때

버블링 X

focusin 포커스 받았을 때

버블링 O

focusout 포커스 잃었을 때

버블링 O

폼 이벤트

이벤트 타입 이벤트 발생 시점
submit form 요소 내의 sumbit 버튼을 클릭했을 때

값 변경 이벤트

이벤트 타입 이벤트 발생 시점
input input, select, textarea 요소의 값이 입력됐을 때

입력을 하고 있을 때 발생

change input, select, textarea 요소의 값이 변경됐을 때

입력하던 HTML요소에서 포커스를 잃었을 때 발생

readystatechange HTML 문서의 파싱 상태인 document.readyState가 변경될 때

'loading', 'interactive', 'complete'

DOM 뮤테이션 이벤트

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

뷰 이벤트

이벤트 타입 이벤트 발생 시점
resize 브라우저 윈도우 크기를 리사이즈 할 때 연속적으로 발생

window 객체에서만 발생

scroll 스크롤할 때 연속적으로 발생

리소스 이벤트

이벤트 타입 이벤트 발생 시점
load DOMContentLoaded 이벤트 발생 이후, 모든 리소스 로딩이 완료됐을 때

주로 winodw 객체에서 발생

unload 리소스가 언로드될 때

주로 새로운 웹페이지를 요청한 경우

abort 리소스 로딩이 중단되었을 때
error 리소스 로딩이 실패했을 때

이벤트 핸들러 등록

이벤트 핸들러를 등록하는 방법은 총 3가지

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

HTML 요소의 어트리뷰트 중에는 이벤트 핸들러 어트리뷰트가 있음
이벤트 핸들러 어트리뷰트 이름 : on + 이벤트 타입    ex) onchange
이벤트 핸들러 어트리뷰트에 함수 호출문을 할당하면 이벤트 핸들러가 등록됨

<!DOCTYPE html>
<html>
  <body>
    <button onclick="handleClick()">버튼</button>

    <script>
    handleClick = () => console.log('Click');
    </script>
  </body>
</html>

함수 호출문을 할당하면 함수 호출문의 평가 결과가 이벤트 핸들러로 등록됨
이 때 어트리뷰트의 값인 handleClick()은 암묵적으로 생성될 이벤트 핸들러의 함수 몸체를 의미
onclick="handleClick( )" 어트리뷰트는 파싱되어 암묵적으로 함수를 생성한 뒤 , 해당 요소의 onclick 이벤트 핸들러 프로퍼티(이벤트 핸들러 어트리뷰트의 이름과 동일한 )에 암묵적으로 생성한 함수를 할당함

function onclick(){
      console.log('Click');
    }
 // 이런 함수가 암묵적으로 생성이 되고 요소의 onclick 프로퍼티에 바인딩

이렇게 동작하는 이유는 이벤트 핸들러에 인수를 전달하기 위해서

// 인수를 전달할 수 없다
<button onclick="handleClick">버튼</button>

바닐라 자바스크립트에서는 이벤트 핸들러 어트리뷰트 방식을 사용하지 않는 것이 좋다
관심사가 다른 HTML과 자바스크립트를 혼용하기 때문에

하지만 React/Vue/Angular/Svelte/의 경우 이벤트 핸들러 어트리뷰트 방식으로 이벤트를 처리함
Component Based Development 방식에서는 HTML,CSS,자바스크립트 모두 뷰를 구성하기 위한 구성요소로 보기 때문에 관심사가 같다고 봄

{ /* React */ }
<button onClick={handleClick}>버튼</button>

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

window 객체, Document, HTMLElement 타입의 DOM 노드 객체에는 이벤트 핸들러 프로퍼티가 있음
이벤트 핸들러 프로퍼티 키 : on + 이벤트타입 ex) onclick
이벤트 핸들러 프로퍼티에 함수를 바인딩하면 이벤트 핸들러가 등록됨

<!DOCTYPE html>
<html>
  <body>
    <button>버튼</button>
    <script>
    const $btn = document.querySelector('button');
    $btn.onclick = () => console.log('click!');
    </script>
  </body>
</html>

이벤트 타깃(이벤트를 발생시킬 객체)은 button 요소
이벤트 타입은 'click'

이벤트 핸들러는 대부분 이벤트를 발생시킬 이벤트 타깃에 바인딩 하지만
이벤트 위임의 경우 해당 이벤트 흐름에 있는 모든 DOM 요소에서 이벤트를 캐치할 수 있기 때문에 이벤트 타깃이 아닌 상위 DOM 요소에 이벤트 핸들러를 바인딩함

이벤트 핸들러 프로퍼티에는 단 하나의 이벤트 핸들러만 바인딩 가능

<!DOCTYPE html>
<html>
 <body>
   <button>버튼</button>
   <script>
   const $btn = document.querySelector('button');
   $btn.onclick = () => console.log('click!'); // 실행 X
   $btn.onclick = () => console.log('재할당 click!');
   </script>
 </body>
</html>

addEventListener 메서드 방식

  • DOM Level 2 에서 도입
  • EventTarget.prototype.addEventListener 메서드
  • EventTarget.addEventListener('이벤트 타입', 이벤트 핸들러[,true,false])

1번째 인수인 이벤트 타입은 on 없이 ex) click
3번째 인수는 이벤트를 캐치할 전파 단계를 설정

  • false,생략 : 버블링 단계에서 이벤트 캐치
  • true : 캡처링 단계에서 이벤트 캐치
<!DOCTYPE html>
<html>
  <body>
    <button>버튼</button>
    <script>
    const $btn = document.querySelector('button');
    $btn.addEventListener('click',()=>console.log('click'));
    </script>
  </body>
</html>

addEventListener 메서드로 등록한 이벤트 핸들러는 이벤트 핸들러 프로퍼티에 바인딩된 이벤트 핸들러와 별개다
즉 둘 다 등록 되어있다면 둘 다 실행된다
또한 중복해서 addEventListener로 여러개를 등록하면 등록된 순서대로 모두 호출됨
but 참조가 동일한 함수를 중복 등록하면 한개만 등록

<!DOCTYPE html>
<html>
 <body>
   <button>버튼</button>
   <script>
   const $btn = document.querySelector('button');
   $btn.onclick = () => console.log('.onclick 프로퍼티에 등록');
   $btn.addEventListener('click',()=>console.log('addEventListner로 등록'));
   $btn.addEventListener('click',()=>console.log('addEventListner로 등록 2'));
   $btn.addEventListener('click',()=>console.log('addEventListner로 등록 3'));

   $btn.addEventListener('click',handleClick);  
   $btn.addEventListener('click',handleClick); // 중복 등록 X
   
   function handleClick() {
   	console.log('handleClick');
   }
   </script>
 </body>
</html>

이벤트 핸들러 제거

  • EventTarget.prototype.removeEventListener
  • addEventListener로 등록할 때 전달한 인수를 동일하게 전달해야만 제거됨
  • 등록 시 무명 함수를 이벤트 핸들러 인수로 전달했다면 삭제 불가
  • 등록과 동시에 삭제해서 딱 1번만 호출되는 이벤트 핸들러를 만들 수도 있음
  • 이벤트 핸들러 프로퍼티로 등록한 이벤트 핸들러는 null을 프로퍼티에 할당하여 제거
<!DOCTYPE html>
<html>
  <body>
    <button>버튼</button>
    <script>
    const $btn = document.querySelector('button');
    // 딱 1번만 호출되는 이벤트 핸들러 
    $btn.addEventListener('click',function handleClick(){
      console.log('handleClick');
      $btn.removeEventListener('click',handleClick);
    })
    
    $btn.onclick = () => console.log('click'); // 등록
    $btn.onclick = null; // 제거
    </script>
  </body>
</html>

이벤트 객체

이벤트 발생 시 해당 이벤트에 관한 정보를 담고 있는 이벤트 객체가 생성되고
이벤트 핸들러의 첫 번째 인수로 전달됨
따라서 이벤트 객체를 전달받으려면 이벤트 핸들러를 정의할 때 이벤트 객체를 받을 매개변수를 선언해야함

<!DOCTYPE html>
<html>
  <body>
    <p>클릭한 화면의 좌표값을 알려드림슨</p>
    <p id="coordi"></p>
    <script>
    const $p = document.getElementById('coordi');
    const printCoordi = (e) => {
      $p.textContent = ` x = ${e.clientX}, y = ${e.clientY}`;
    }
    document.addEventListener('click',printCoordi);
    </script>
  </body>
</html>

이벤트 핸들러 어트리뷰트로 이벤트 핸들러를 등록했다면
어트리뷰트 값으로 할당하는 문의 매개변수 이름을 event로 해야만 이벤트 객체를 받을 수 있음

<!DOCTYPE html>
<html onclick="printCoordi(event)"> // event가 아니면 이벤트 객체 못 받음
  <body>
    <p>클릭한 화면의 좌표값을 알려드림슨</p>
    <p id="coordi"></p>
    <script>
    const $p = document.getElementById('coordi');
    const printCoordi = (e)=>{
      $p.textContent = ` x = ${e.clientX}, y = ${e.clientY}`;
    }
    </script>
  </body>
</html>

이벤트 객체의 상속 구조

이벤트 타입에 따라 다양한 이벤트 객체가 생성됨

위 사진 모두 생성자 함수이므로 생성자 함수를 호출하여 이벤트 객체 생성 가능
이벤트가 발생하면 생성되는 이벤트 객체도 생성자 함수에 의해 생성됨
CustomEvent 객체는 인위적으로 생성해야 생김

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

공통 프로퍼티설명타입
type이벤트 타입string
target이벤트를 발생시킨 DOM 요소DOM 요소 노드
currentTarget이벤트 핸들러가 바인딩된 DOM 요소DOM 요소 노드
eventPhase이벤트 전파 단계

0:이벤트 없음, 1: 캡처링, 2:타깃 , 3:버블링

number
bubbles이벤트를 버블링으로 전파하는지 여부boolean
cancelablepreventDefault()로 기본 동작 취소 가능 여부boolean
defaultPreventedpreventDefault 호출 여부boolean
isTrusted사용자에 의해 발생한 이벤트면 trueboolean
timeStamp이벤트 발생 시각 (1970.1.1.00:00:0 부터 경과한 ms)number

마우스 정보 취득

MouseEvent 타입의 이벤트 객체가 갖는 고유의 프로퍼티는 아래와 같다

  • 마우스 포인터의 좌표 정보
    - screenX, screenY
    - clientX, clientY
    - pageX, pageY
    - offsetX, offsetY
  • 해당 키를 누른채로 클릭하였는지 여부
    • shiftKey
    • altKey
    • ctrlKey
    • button : 마우스에서 어떤 버튼을 눌렀는지
      • 0 : 마우스 왼쪽
      • 1 : 마우스 휠
      • 2: 마우스 오른쪽

키보드 정보 취득

KeyboardEvent 타입의 이벤트 객체가 갖고 있는 고유의 프로퍼티는 아래와 같다

  • altKey, shiftKey, metaKey (맥은 command키,윈도우는 윈도우키)
  • key
<!-- Enter키를 누르면 input에 입력된 값을 <p>안에서 보여주는 예제 -->
<!DOCTYPE html>
<html>
  <body>
    <input type="text">
    <p class="message"></p>

    <script>
      const $input = document.querySelector('input[type=text]');
      const $msg = document.querySelector('.message');

      $input.addEventListener('keydown',e => {
        if (e.key!=='Enter' || e.isComposing) return; // 한글 입력 시 이벤트 2번 발생 방지
        $msg.textContent = e.target.value;
        e.target.value = '';
      });
    </script>
  </body>
</html>

이벤트 전파

  1. 이벤트 객체가 생성되면 window부터 시작해서 이벤트 타깃방향으로 전파된다
    = 캡처링 단계
  2. 이벤트 객체가 이벤트를 발생시킨 이벤트 타깃에 도달함
    = 타깃 단계
  3. 다시 이벤트 타깃부터 window 방향으로 전파
    = 버블링 단계

따라서 해당 이벤트 패스에 있는 모든 DOM 요소는 이벤트를 캐치할 수 있다

이벤트 위임

다수의 하위 DOM 요소에 각각 이벤트 핸들러를 등록하지 말고 그 요소들을 포함하는 상위 DOM 요소에 이벤트 핸들러를 딱 한번 등록하는 것
주의사항
내가 원하지 않았던 요소에서 이벤트가 발생할 수도 있음
ex) 여러개의 li태그에서의 click이벤트를 처리하기위해 ul태그에 이벤트를 위임 했을 때 li태그의 영역이 아닌 ul태그의 영역에서 click하게되면 원하지 않는 상황이 발생한다
따라서 이벤트 핸들러에서 이벤트가 발생한 요소를 한번 더 체크해야한다

<!DOCTYPE html>
<html>
  <head>
    <style>
      .red{
        color: red;
      }
      .blue{
        color: blue;
      }
      li{
        cursor: pointer;
      }
    </style>
  </head>
  <body>
   <ul id="table">
     <li>1번</li>
     <li>2번</li>
     <li>3번</li>
     <li>4번</li>
     <li>5번</li>
   </ul>
   <p></p>
    <script>
      const table = document.getElementById('table');
      table.addEventListener('click',e=>{
        if (! e.target.matches('#table > li')) return // 이벤트 타깃 검증
        document.querySelector('p').textContent = `${e.target.firstChild.nodeValue} 선택`;
        [...table.children].forEach(li =>{
          if (li===e.target) li.className = 'red';
          else li.className = 'blue';
        });
      });
     
    </script>
  </body>
</html>


DOM 요소의 기본 동작 조작

DOM 요소의 기본 동작 중단

이벤트 객체의 preventDefault 메서드는 DOM 요소의 기본 동작을 중단시킴

<!-- <a> 클릭해도 네이버 홈페이지로 이동하지 않음 -->
<!DOCTYPE html>
<html>
  <body>
    <a href="https://www.naver.com">Naver</a> 
    <script>
     const atag = document.querySelector('a');
     atag.addEventListener('click', e => e.preventDefault());
    </script>
  </body>
</html>

이벤트 전파 방지

이벤트 객체의 stopPropagation 메서드는 이벤트 전파를 중지시킴
하위 DOM 요소의 이벤트를 개별적으로 처리하기 위해 이벤트 전파를 중단시킬 때 사용

<!-- 1번은 클릭해도 빨간색으로 변하지 않는다 -->
<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li>1번</li>
      <li>2번</li>
      <li>3번</li>
    </ul>
    <script>
      const table = document.querySelector('ul');
      table.addEventListener('click',e=>{
        if (! e.target.matches('ul > li')) return
        e.target.style.color = 'red';
      });
      table.firstElementChild.addEventListener('click', e => e.stopPropagation());
    </script>
  </body>
</html>

이벤트 핸들러 내부의 this

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

일반 함수로 호출되기 때문에 this는 전역 객체
어트리뷰트 값에 함수 호출문 할당할 때 인수로 this를 전달하면 이벤트를 바인딩한 DOM 요소를 가르킴

<!DOCTYPE html>
<html>
  <body>
    <button onclick="handleClick(this)">버튼</button>
    <script>
      function handleClick(that){
        console.log(this); // window
        console.log(that); // <button>
      }
    </script>
  </body>
</html>

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

이벤트 핸들러 내부의 this = 이벤트를 바인딩한 DOM 요소
이벤트 핸들러 내부의 this = 이벤트 객체의 currentTarget 프로퍼티

<!DOCTYPE html>
<html>
  <body>
    <button class="btn1">버튼1</button>
    <button class="btn2">버튼2</button>
    <button class="btn3">버튼3</button>
    <script>
    const $btn1 = document.querySelector('button[class="btn1"]');
    const $btn2 = document.querySelector('button[class="btn2"]');
    const $btn3 = document.querySelector('button[class="btn3"]');

    $btn1.onclick = function(e){
      console.log(this);
      console.log(e.currentTarget === this); // true
    }
    $btn2.addEventListener('click',function(e){
      console.log(this);
      console.log(e.currentTarget === this); // true
    });
    $btn3.addEventListener('click', e => {
      console.log(this); // 화살표 함수는 this바인딩 X, 상위 스코프의 this 참조
      console.log(e.currentTarget === this); // false
    });
    </script>
  </body>
</html>

이벤트 핸들러에 인수 전달

이벤트 핸들러 어트리뷰트 방식은 함수 호출문을 할당하기 때문에 인수 전달 가능

<!DOCTYPE html>
<html>
  <body>
    <button type="text" onclick="handleClick(2,4)">버튼</button>
    <script>
      const handleClick = (a,b) => alert(`${a}+${b} = ${a+b}`);
    </script>
  </body>
</html>

이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식의 경우 함수 자체를 할당하기 때문에 인수 전달이 불가능 할 것 같지만
아래와 같이 인수 전달이 가능함

  • 이벤트 핸들러 내부에서 함수를 호출
<!DOCTYPE html>
<html>
  <body>
    <button type="text">버튼</button>
    <script>
      const $btn = document.querySelector('button');
      const handleClick = (a,b) => alert(`${a}+${b} = ${a+b}`);
      $btn.addEventListener('click',()=>{
        handleClick(2,4);
      });
    </script>
  </body>
</html>
  • 이벤트 핸들러를 반환하는 함수를 호출
<!DOCTYPE html>
<html>
  <body>
    <button type="text">버튼</button>
    <script>
      const $btn = document.querySelector('button');
      const 이벤트핸들러뱉는함수 = (a,b) => () => alert(`${a}+${b} = ${a+b}`)
      $btn.addEventListener('click', 이벤트핸들러뱉는함수(2,4));
    </script>
  </body>
</html>

커스텀 이벤트

커스텀 이벤트 생성

  • 생성자 함수를 호출하여 명시적으로 생성한 이벤트 객체는 이벤트 타입 지정 가능
  • 이 때 생성자 함수의 첫 번째 인수는 이벤트 타입을 나타내는 문자열
  • 기존의 이벤트 타입이 아니라면 CustomEvent 생성자 함수를 사용
  • 커스텀 이벤트 객체는 버블링 X, preventDefault 로 취소 불가
  • 두번째 인수를 통해 기존 이벤트 타입의 고유 프로퍼티 지정 가능
  • isTrusted 프로퍼티는 언제나 false
const keyboardEvent = new KeyboardEvent('keyup');

const customEvent = new CustomEvent('myType');

console.log(customEvent.bubbles); // false
console.log(customEvent.cancelable); // false

// 이벤트 타입의 따른 고유 프로퍼티를 지정하려면 3번째 인수로 객체를 넘긴다
const customMouseEvent = new MouseEvent('click',{
	bubbles : true,
    cancelable : true,
    clientX : 50,
    clientY : 100
});

커스텀 이벤트 디스패치

  • 디스패치 = 이벤트를 발생시키는 행위
  • dispatchEvent(이벤트 객체) 메서드로 커스텀 이벤트를 발생시킬 수 있음
  • 일반적인 이벤트 핸들러는 비동기 처리 방식이지만 dispatchEvent는 동기
  • dispatchEvent 메서드를 호출하면 커스텀 이벤트에 바인딩된 이벤트 핸들러를 직접 호출하는 것이기 때문에 커스텀 이벤트를 처리할 이벤트 핸들러를 등록한 이후 디스패치 해야함
  • 커스텀 이벤트 타입은 이벤트 핸들러 등록시 addEventListner 메서드만 가능
    요소 노드에는 당연히 내가 만든 이벤트 타입에 해당하는 프로퍼티가 없으니까
<!DOCTYPE html>
<html>
  <body>
    <button type="text">버튼</button>
    <script>
      const $btn = document.querySelector('button');
      $btn.addEventListener('myType', e => {
        alert(e.detail.message);
      });
      
      const customEvent = new CustomEvent('myType',{
        detail : {message : 'Hi'}
      });

      $btn.dispatchEvent(customEvent);

    </script>
  </body>
</html>

이웅모, 『모던 자바스크립트 Deep Dive』, 위키북스(2021)

profile
grindin'

0개의 댓글