본문 바로가기

JavaScript

[Deep dive] 40장 이벤트(2)

40.6 이벤트 전파 

 

이벤트 전파(event propagation)

- DOM 트리 상에 존재하는 DOM 요소 노드에서 발생하는 이벤트는 DOM 트리를 통해 전파 

- 생성된 이벤트 객체는 이벤트를 발생시킨 DOM 요소의 이벤트 타깃(event target)을 중심으로 DOM 트리를 통해 전파 

- 이벤트는 이벤트를 발생시킨 이벤트 타깃은 물론 상위 DOM 요소에서도 캐치 가능 

- DOM 트리를 통해 전파되는 이벤트는 이벤트 패스에 위치한 모든 DOM 요소에서 캐치 가능

 

캡처링 단계 (capturing phase)

- 이벤트가 상위 요소에서 하위 요소 방향으로 전파 

 

타깃 단계 (target phase)

- 이벤트가 이벤트 타깃에 도달

 

버블링 단계(bubbling phase)

- 이벤트가 하위 요소에서 상위 요소 방향으로 전파 

 

ex)

- ul 요소에 이벤트 핸들러를 바인딩하고, ul 요소의 하위 요소인 li 요소를 클릭하여 이벤트 발생 

- 이벤트 타깃(event target)은 li 요소, 커런트 타깃(event.currentTarget)은 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 HTMLIElement]
        console.log(`커런트 타깃 : ${e.currentTarget}`); // [object HTMLUListElment]
      });
    </script>
  </body>
</html>

- li 요소를 클릭하면 클릭 이벤트가 발생하여 클릭 이벤트 객체가 생성되고 클릭된 li 요소가 이벤트 타깃이 됨

- 클릭 이벤트 객체는 window에서 시작해서 이벤트 타깃 방향으로 전파 (캡처링 단계)

- 이벤트 객체는 이벤트를 발생시킨 이벤트 타깃에 도달 ( 타깃 단계)

- 이벤트 객체는 이벤트 타깃에서 시작해서 window 방향으로 전파 (버블링 단계)

 

- 이벤트 핸들러 어트리뷰트/프로퍼티 방식으로 등록한 이벤트 핸들러는 타깃 단계와 버블링 단계의 이벤트만 캐치 가능

- addEventListener 메서드 방식으로 등록한 이벤트 핸들러는 타깃 단계와 버블링 단계 뿐만 아니라 캡처링 단계 이벤트도 선별적으로 캐치 

<!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]
     });

     //타깃 단계의 이벤트를 캐치 
     $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>

 

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

<!DOCTYPE html>
<html>
<head>
  <style>
    html, body { height : 100%; }
  </style>
</head>
  <body>
    <p>버블링과 캡처링 이벤트 <button>버튼</button></p>
    <script>
      //버블링 단계의 이벤트를 캐치 
      document.body.addEventListenr('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>

- body 요소는 버블링 단게의 이벤트만을 캐치하고, p 요소는 캡처링 단계의 이벤트만 캐치 

 

- 이벤트는 캡처링 - 타깃 - 버블링 단계로 전파되므로 

 1) button 요소에서 클릭 이벤트 발생

  - 캡처링 단계를 캐치하는 p 요소의 이벤트핸들러가 호출

  - 버블링 단계의 이벤트를 캐치하는 body 요소의 이벤트가  단계적으로 호출

Handler for paragraph
Handler for button
Handler for body

2) p 요소에서 클릭 이벤트 발생 

 - 캡처링 단계를 캐치하는 p 요소의 이벤트 핸들러가 호출

 - 버블링 단계를 캐치하는 body 요소의 이벤트가 순차적으로 호출

Handler for paragraph
Handler for body

 

40.7 이벤트 위임

 

이벤트 위임 (event delegation)

- 여러 개의 하위 DOM 요소에 각각 이벤트 핸들러를 등록하는 대신 하나의 DOM 요소에 이벤트 핸들러를 등록

- 이벤트 위임을 통해 상위 DOM 요소에 이벤트 핸들러를 등록하면 여러 개의 하위 DOM 요소에 이벤트 핸들러를 등록할 필요가 없음

 

ex)

- 사용자가 네비게이션 아이템(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 = "orage">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($fruits => {
          $fruits.classList.toggle('active', $fruits === target);
          $msg.textContent = target.id;
        });
      }
        //이벤트 위임: 상위 요소(ul#fruits)는 하위 요소의 이벤트를 캐치할 수 있다
        $fruits.onclick = activate;
    </script>
  </body>
</html>

- 이벤트 위임을 통해 상위 DOM 요소에 이벤트를 바인딩한 경우 이벤트 객체의 target  프로퍼티와 currentTarget 프로퍼티가 다른 DOM 요소를 가리킬 수 있음

 

40.8 DOM 요소의 기본 동작 조작

 

40.8.1 DOM 요소의 기본 동작 중단 

 

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

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

- 이벤트 객체의 stopPropagation 메서드는 이벤트 전파를 중지시킴

 

ex)

- 상위 DOM 요소인 container 요소에 이벤트 위임

- 하위 DOM 요소에서 발생한 클릭 이벤트를 상위 DOM 요소인 container 요소가 캐치하여 이벤트 처리 

- 하위 요소중에서 btn2 요소는 자체적으로 이벤트 처리 

- btn2 요소는 자신이 발생시킨 이벤트가 전파되는 것을 중단하여 자신에게 바인딩된 이벤트 핸들러만 실행되도록 함 

<!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 이벤트 핸들러 어트리뷰트 방식 

- handleClick 함수 내부의 this는 전역 객체 window를 가리킴

- handleClick 함수는 이벤트 핸들러에 의해 일반 함수로 호출

- 일반 함수로서 호출되는 함수 내부의 this는 전역객체를 가리킴

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

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

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

 

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

- 이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식 모두 이벤트 핸들러 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리킴

- 즉, 이벤트 핸들러 내부의 this는 이벤트 객체의 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>

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

- 화살표 함수는 함수 자체의 this 바인딩을 갖지 않음

<!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는 이벤트를 바인딩한 DOM 요소를 가리킴
      console.log(this); //window
      console.log(e.currentTarget); //$button1
      console.log(this === e.currentTarget); //false
    }

    //this는 window를 가리키므로 window.textContent에 NaN을 할당 
    ++this.textContent;

    //addEventListener 메서드 방식 
    $button2.addEventListener('click', e => {
      //this는 이벤트를 바인딩한 DOM 요소를 가리킴
      console.log(this); //window
      console.log(e.currentTarget); //$button2
      console.log(this === e.currentTarget); //false
    });
   
    //this는 window를 가리키므로 window.textContent에 NaN을 할당
    ++this.textContent;
   </script>
  </body>
</html>

- 클래스에서 이벤트 핸들러를 바인딩하는경우 this에 주의 

 -> increase 메서드 내부의 this는 this.$button을 가리키므로 increase 메서드를 이벤트 핸들러로 바인딩할 때 bind 메서드를 사용해 this를 전달해 increase 메서드 내부의 this가 클래스를 생성할 인스턴스를 가리키도록 해야 함

<!DOCTYPE html>
<html>
  <body>
   <button class="btn">0</button>
   <script>
    class App {
      constructor() {
      this.$button = document.querySelector('.btn');
      this.count = 0;
     
      //increase 메서드 내부의 this가 인스턴스를 가리키도록
      this.$button.onclick = this.increae.bind(this);
    }

    increase() {
      this.$button.textContent = ++this.count;
    }
  }
   </script>
  </body>
</html>

 

40.10 이벤트 핸들러에 인수 전달 

 

- 이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식의 경우 이벤트 핸들러를 브라우저가 호출하기 떄문에 함수 호출문이 아닌 함수자체를 등록 -> 인수 전달 불가 

 

1) 이벤트 핸들러 내부에서 함수를 호출하면서 인수 전달 가능 

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

2) 이벤트 핸들러를 반환하는 함수를 호출하면서 인수 전달 가능

<!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.11 커스텀 이벤트 

 

40.11.1 커스텀 이벤트 생성

- Event,UIEvent,MouseEvent 같은 이벤트 생성자 함수를 호출하여 명시적으로 생성한 이벤트 객체는 임의의 이벤트 타입을 지정할 수 있음

- 개발자의 의도로 생성된 이벤트를 커스텀 이벤트라 함

 

- 이벤트 생성자 함수는 첫 번째 인수로 이벤트 타입을 나타내는 문자열 전달받음

 1) 기존 이벤트 타입 사용

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

 2) 기존 이벤트 타입이 아닌 임의의 문자열 사용 

 - CustomEvent 이벤트 생성자 함수 사용

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

 

- 생성된 커스텀 이벤트 객체는 버블링 되지 않으며, preventDefault 메서드로 취소할 수 없음

- 이벤트 생성자 함수로 생성한 커스텀 이벤트는 isTrusted 프로퍼티의 값이 언제나 false임

 

40.11.2 커스텀 이벤트 디스패치 

- 커스텀 이벤트 객체를 생성한 후 커스텀 이벤트를 사용하려면 이벤트 객체를 전달하는 과정 필요

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

- dispatchEvent 메서드에 이벤트 객체를 인수로 전달하면서 호출하면 인수로 전달한 이벤트 타입의 이벤트가 발생 

- 일반적으로 이벤트 핸들러는 비동기(asynchronous) 처리 방식으로 동작하지만 dispatchEvent 메서드는 이벤트 핸들러를 동기(Synchronous)처리 방식으로 호출 

- 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,...}
      alert(`${e} Clicked!`);
    });

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

    //커스텀 이벤트 디스패치(동기 처리). click 이벤트가 발생
    $button.dispatchEvent(customEvent);
   </script>
  </body>
</html>

 

- CustomEvent 이벤트 생성자 함수에는 두 번째 인수로 이벤트와 함께 전달하고 싶은 정보를 담은 detail 프로퍼티를 포함하는 객체 전달 가능

- 이 정보는 이벤트 객체의 detail 프로퍼티(e.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에는 CustomeEvent 함수의 두 번째 인수로 전달한 정보가 담겨있음
      alert(e.detail.message);
    });

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

    //커스텀 이벤트 디스패치 
    $button.dispatchEvent(customEvent);

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

'JavaScript' 카테고리의 다른 글

[Deep dive] 42장 비동기 프로그래밍  (0) 2023.08.23
[Deep dive] 41장 타이머  (0) 2023.08.22
[Deep dive] 40장 이벤트(1)  (2) 2023.08.22
[Deep dive] 39장 DOM (3)  (0) 2023.08.21
[Deep dive] 39장 DOM (2)  (0) 2023.08.21