본문 바로가기

JavaScript

[모던자바스크립트 DeepDive] 40장 이벤트

본 글은 자바스크립트 스터디를 진행하면서 [모던자바스크립트 DeepDive] 책을 바탕으로 작성된 글입니다.

 

 

 

 

 

 

이벤트 드리븐 프로그래밍

  • 브라우저는 스스로 감지해서 특정 사건이 발생하면 해당 이벤트를 발생시킨다.
  • 특정 이벤트에 대해 어떤 일을 하고 싶다면 우리는 브라우저에게 특정 이벤트가 발생 했을 때 호출될 함수를 알려주어 브라우저에게 호출을 위임한다
  • 왜냐하면 우리는 특정 이벤트가 언제 발생할지 알 수 없으므로, 이벤트를 감지할 수 있는 브라우저에게 전적으로 위임하는 것
    • 이벤트 핸들러 : 이벤트가 발생했을 때 호출될 함수
    • 이벤트 핸들러 등록 : 브라우저에게 이벤트 핸들러의 호출을 위임하는 것
  • 이벤트와 이벤트 핸들러를 통해 사용자와 애플리케이션은 상호작용한다
  • 프로그램의 흐름을 이벤트 중심으로 제어하는 프로그램이 방식을 이벤트 드리븐 프로그래밍 이라고 한다.

 

 

 

이벤트 타입

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

 

 

 

이벤트 핸들러 등록

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

 

1️⃣ 이벤트 핸들러 어트리뷰트 방식

  • 이벤트 핸들러 어트리뷰트 이름 : 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>

 

 

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

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

 

3️⃣ 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
cancelable preventDefault()로 기본 동작 취소 가능 여부 boolean
defaultPrevented preventDefault 호출 여부 boolean
isTrusted 사용자에 의해 발생한 이벤트면 true boolean
timeStamp 이벤트 발생 시각 (1970.1.1.00:00:0 부터 경과한 ms) number

 

 

 

 

 

 

이벤트 전파

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

 

 

 

 

이벤트 위임

  • 다수의 하위 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>
728x90
반응형