본문 바로가기
Javascript/javascript Core

[Javascript강의]15강.이벤트

by 카리3 2021. 10. 12.

이벤트

 

이벤트 핸들러 등록

1. 이벤트 핸들러 이트리뷰트 방식
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

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

</html>
2.이벤트 핸들러 프로퍼티 방식
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <button>Click me!</button>
</body>
<script>
    const $button = document.querySelector('button');
    //이벤트 바인딩
    $button.onclick =  function() {
        console.log('button click');        
    }
    //중복 등록 안됨
    $button.onclick =  function() {
        console.log('button click2');        
    }
</script>

</html>

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <button>Click me!</button>
</body>
<script>
    const $button = document.querySelector('button');
    //addEventListener 메서드 방식
    $button.addEventListener('click',  function() {
        console.log('button click');        
    });
    //중복 등록 가능
    $button.addEventListener('click', function () {
        console.log('button click2');
    });
    //참조가 동일한 이벤트 핸들러를 중복 등록하면 하나의 핸들러만 등록된다.
    //$button.addEventListener('click',handleClick);
    //$button.addEventListener('click',handleClick);
</script>

</html>

 

이벤트 핸들러 제거

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

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

    const handleClick = () => console.log('button click');
    
    //이벤트 핸들러 등록
    $button.addEventListener('click', handleClick);

    //이벤트 핸들러 제거
    //전달한 인수가 일치하지 않으면 제거되지 않는다.
    $button.removeEventListener('click',handleClick,true); //실패
    $button.removeEventListener('click', handleClick); //성공
    
    //이벤트 핸들러 프로퍼티 방식으로 이벤트 핸들러 등록
    $button.onclick = handleClick;
    $button.removeEventListener('click', handleClick); //실패
    $button.onclick = null;//성공
</script>

</html>

 

이벤트 객체

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <p>클릭하세요. 클릭한 곳의 좌표가 표시됩니다</p>
    <em class="message"></em>
</body>
<script>
    const $msg = document.querySelector('.message');

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

</html>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        html, body{height: 100%;}
    </style>
</head>

<!-- 이벤트 핸들러 어트리뷰트 방식의 경우 event가 아닌 다른 이름으로는 이벤트 객체를 전달받지 못한다.-->
<body onclick="showCoords(event)"> 
    <p>클릭하세요. 클릭한 곳의 좌표가 표시됩니다</p>
    <em class="message"></em>
</body>
<script>
    const $msg = document.querySelector('.message');

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

</html>

이벤트 전파

이벤트 전파

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <ul id="fruits">
        <li id="apple">Apple</li>
        <li id="banana">Banana</li>
        <li id="orange">Orange</li>
    </ul>    
</body>
<script>
    const $fruits = document.getElementById('fruits');
    const $banana = document.getElementById('banana');

    $fruits.addEventListener('click', e => {
        console.log(`이벤트 단계: ${e.eventPhase}`); //1.캡처링 단계
        console.log(`이벤트 타깃: ${e.target}`);
        console.log(`커런트 타깃: ${e.currentTarget}`);
    }, true); //3번째 인수 true
    $banana.addEventListener('click', e => {
        console.log(`이벤트 단계: ${e.eventPhase}`); //2.타깃 단계
        console.log(`이벤트 타깃: ${e.target}`);
        console.log(`커런트 타깃: ${e.currentTarget}`);
    });
    $fruits.addEventListener('click', e => {
        console.log(`이벤트 단계: ${e.eventPhase}`); //3.버블링 단계
        console.log(`이벤트 타깃: ${e.target}`);
        console.log(`커런트 타깃: ${e.currentTarget}`);
    });
</script>

</html>

 

이벤트 위임

이벤트 위임은 여러 개의 하위 DOM 요소에 각각 이벤트 핸들러를 등록하는 대신 하나의 상위 DOM 요소에 이벤트 핸들러를 등록하는 방법을 말한다

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <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>
</body>
<script>
    const $fruits = document.getElementById('fruits');
    const $msg = document.querySelector('.msg');

    //사용자 클릭에 의해 선택된 내비게이션 아이템(li 요소)에 active 클래스를 추가하고
    //그 외의 모든 내비게이션 아이템의 active 클래스를 제거한다.
    function activate({target}){
        if( !target.matches('#fruits > li')) return;

        [...$fruits.children].forEach($fruit => {
            $fruit.classList.toggle('active',$fruit === target);
            $msg.textContent = target.id;
        });
    }

    /*
    //모든 내비게이션 아이템에 이벤트 핸들러를 등록한다.
    document.getElementById('apple').onclick = activate;
    document.getElementById('banana').onclick = activate;
    document.getElementById('orange').onclick = activate;
    */

    //이벤트 위임: 상위 요소(ul#fruits)는 하위 요소의 이벤트를 캐치할 수 있다.
    $fruits.onclick = activate;
</script>

</html>

일반적으로 이벤트 객체의 target 프로퍼티와 currentTarget 프로퍼티는 동일한 DOM 요소를 가리키지만 이벤트 위임을 통해 상위 DOM 요소에 이벤트를 바인딩한 경우 이벤트 객체의 target 프로퍼티와 cuurentTarget 프로퍼티가 다른 DOM 요소를 가리킬 수 있다. 이때 이벤트 객체의 currentTarget 프로퍼티는 언제나 변함없이 $fruits요소를 가리키지만 이벤트 객체의 target 프로퍼티는 실제로 이벤트를 발생시킨 DOM 요소를 가리킨다.

DOM 요소의 기본 동작 중단

DOM 요소는 저마다 기본 동작이 있다. 예를 들어 a요소를 클릭하면 href 어트리뷰트에 지정된 링크로 이동하고, checkbox 또는 radio 요소를 클릭하면 체크 또는 해제된다.

<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>
//이벤트 전파 방지
  <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>

 

이벤트 핸들러의 this

  <button onclick="handleClick(this, event);">Click me</button>
  <script>
      function handleClick(button, e){
          console.log(button); //이벤트를 바인딩한 button요소
          console.log(this); //window
          console.log(e);          
      }
  </script>

이벤트 핸들러를 호출할 때 인수로 전달한 this는 이벤트를 바인딩한 DOM 요소를 가리킨다.

이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식 모두 이벤트 핸들러 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리킨다. 즉, 이벤트 핸들러 내부의 this는 이벤트 객체의 currentTarget 프로퍼티와 같다.

화살표 함수로 정의한 이벤트 핸들러 내부의 this는 상위 스코프 this를 가리킨다.

<button class="button1">0</button>
  <button class="button2">0</button>
  <button class="button3">0</button>
  <script>
      const $button1 = document.querySelector('.button1');
      const $button2 = document.querySelector('.button2');
      const $button3 = document.querySelector('.button3');

      //이벤트 핸들러 프로퍼티 방식
      $button1.onclick = function (e) {
          //this는 이벤트를 바인딩한 DOM요소를 가리킨다.
          console.log(this); //$button1 
          console.log(e.currentTarget); //$button1 
          console.log( this === e.currentTarget); //true
          
          ++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

        ++this.textContent;
    });

    //화살표 함수의 this는 상위 스코프의 this를 가리킨다.
    $button3.addEventListener('click', e => {
        //this는 이벤트를 바인딩한 DOM요소를 가리킨다.
        console.log(this); //window
        console.log(e.currentTarget); //$button2 
        console.log(this === e.currentTarget); //false

        ++this.textContent; //window.textContent   NaN
    });
//클래스에서 이벤트 핸들러를 바인딩하는 경우
<button class="bbtn">0</button>
  <script>
      class App{
          constructor(){
              this.$button = document.querySelector('.bbtn');
              this.count = 0;

              //increase 메서드를 이벤트 핸들러로 등록
              this.$button.onclick = this.increase;
          }

          increase() {
              //이벤트 핸들러 increase 내부의 this는 DOM 요소(this.$button)를 가리킨다.
              //따라서 this.$button은 this.$button.$button과 같다
              console.log(this);  //this.$button
              console.log(this.count); //undefined
              console.log(this.textContent); //0
              
              
              this.$button.textContent = ++ this.count;
          }
      }

      new App();      
  </script>

//increase 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리킨다.
 <button class="bbtn2">0</button>
    <script>
        class App2 {
            constructor() {
                this.$button = document.querySelector('.bbtn2');
                this.count = 0;

                //increase 메서드를 이벤트 핸들러로 등록
                this.$button.onclick = this.increase.bind(this);
            }

            increase() {
                //이벤트 핸들러 increase 내부의 this는 DOM 요소(this.$button)를 가리킨다.
                //따라서 this.$button은 this.$button.$button과 같다
                this.$button.textContent = ++this.count;
            }
        }

        new App2();
    </script>
    
    //increase 메서드를 이벤트 핸들러로 바인딩할 때 bind 메서드를 사용해 this를
    //전달하여 increase 메서드 내부의 this가 클래스가 생성할 인스턴스를 가리키도록 해야 한다.
//클래스 필드에 할당한 화살표 함수를 이벤트 핸들러로 등록하여 이벤트 핸들러 내부의
//this가 인스턴스를 가리키도록 할 수 있다.

    <button class="bbtn3">0</button>
    <script>
        class App3 {
            constructor() {
                this.$button = document.querySelector('.bbtn3');
                this.count = 0;

                //increase 메서드를 이벤트 핸들러로 등록
                this.$button.onclick = this.increase;
            }

            //클래스 필드 정의
            //increase는 인스턴스 메서드이며 내부의 this는 인스턴스를 가리킨다
            increase = () => this.$button.textContent = ++this.count;
        }

        new App3();
    </script>

 

이벤트 핸들러에 인수 전달

이벤트 핸들러 어트리뷰트 방식은 함수 호출문을 사용할 수 있기 때문에 인수를 전달하수 있지만 이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식의 경우 이벤트 핸들러를 브라우저가 호출하기 때문에 함수 호출문이 아닌 함수 자체를 등록해야 한다. 따라서 인수를 전달할 수 없다. 그러나 이벤트 핸들러 내부에서 함수를 호출하면서 인수를 전달할 수 있다.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<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>
    const checkUserNameLength2 = min => e => {
        $msg.textContent = $input.value.length < min ? `이름은 ${min}자 이상 입력해 주세요 : ` : '';
    };

    //이벤트 핸들러를 반환하는 함수를 호출하면서 인수를 전달한다.
    $input.onblur = checkUserNameLength2(MIN_USER_NAME_LENGTH);

 

커스텀 이벤트

이벤트가 발생하면 암묵적으로 생성되는 이벤트 객체는 발생한 이벤트의 종류에 따라 이벤트 나입이 결정된다. 하지만 Event, UIEvent, MouseEvent 같은 이벤트 생성자 함수를 호출하여 명시적으로 생성한 이벤트 객체는 임의의 이벤트 타입을 지정할 수 있다. 이철럼 개발자의 의도로 생성된 이벤트를 커스텀 이벤트라 한다.

//keyboardEvent 생성자 함수로 keyup 이벤트 타입의 커스텀 이벤트 객체를 생성
    const keyboardEvent = new KeyboardEvent('keyup');
    console.log(keyboardEvent.type); //keyup

    //CustomEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체를 생성
    const customEvent = new CustomEvent('foo');
    console.log(customEvent.type); //foo
    console.log(customEvent.bubbles);
    console.log(customEvent.cancelable);
    
    const customEvent2 = new MouseEvent('click',{
        bubbles: true,
        cancelable: true,
        clientX: 50,
        clientY: 100
    })
    
    console.log(customEvent2.bubbles); //true
    console.log(customEvent2.cancelable); //true
    console.log(customEvent2.clientX); //50
    console.log(customEvent2.clientY); //100  
    
    const keyboardEvent2 = new KeyboardEvent('keyup', {key:'Enter'});
    console.log(keyboardEvent2.key); //Enter

    //이벤트 생성자 함수로 생성한 커스텀 이벤트는 isTrusted 프로퍼티의 값이 언제나 false다.
    const customEvent3 = new InputEvent('foo');
    console.log(customEvent.isTrusted); //false

생성된 커스텀 이벤트는 dispatchEvent 메서드로 디스패치(이벤트를 발생시키는 행위)할 수 있다.

    <button class="btn">Click me</button>
    <script>
      const $button = document.querySelector('.btn');
      
       // 버튼 요소에 foo 커스텀 이벤트 핸들러를 등록
       // 커스텀 이벤트를 디스패치하기 이전에 이벤트 핸들러를 등록해야 한다.
      $button.addEventListener('click', e => {
        console.log(e); //MouseEvent        
      });

      //커스텀 이벤트 생성
      const customEvent4 = new MouseEvent('click');

      //커스텀 이벤트 디스패치(동기 처리), click  이벤트가 발생한다.
      $button.dispatchEvent(customEvent4)

    </script>
    <button class="btn2">Click me2</button>
    <script>
        const $button2 = document.querySelector('.btn2');

        //버튼 요소에 foo 커스텀 이벤트 핸들러를 등록
        //커스텀 이벤트를 디스패치하기 이전에 이벤트 핸들러를 등록해야 한다.
        $button2.addEventListener('foo', e => {
           //e.detail에는 CustomeEvent 함수의 두 번째 인수로 전달한 정보가 담겨 잇다.
           console.log(e.detail.message);      
        });

        //CustomEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체를 생성
        const customEvent5 = new CustomEvent('foo', {
            detail: {message: 'Hello'} //이벤트와 함께 전달하고 싶은 정보
        });

        //커스텀 이벤트 디스패치
        $button2.addEventListener('click',e => {
            $button2.dispatchEvent(customEvent5); //커스텀 이벤트 실행
            console.log(e);
            
        });
    </script>