Skip to content

Latest commit

 

History

History
614 lines (489 loc) · 21.8 KB

40.7 이벤트 위임~.md

File metadata and controls

614 lines (489 loc) · 21.8 KB

40.7 이벤트 위임

<!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>
  • 위 예제는 모든 내비게이션 아이템에 이벤트 핸들러 activate를 등록함.
  • 만약 100개라면 이벤트 핸들러는 100개, 10,000,000개라면 이벤트 핸들러는 10,000,000개가 생길 것이다.(매우 비효율적)
  • 성능 저하 뿐만 아니라, 유지보수에도 매우 힘들다.

이러한 점을 해결하기 위한 방법으로 이벤트 위임이라는 방식이 있다.

이벤트위임은 상위 DOM 요소에 이벤트를 등록하고 이벤트 전파를 통해 이벤트를 감지하여 처리하는 방식이다.

<!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>
  • 이벤트 위임을 통해 하위 DOM 요소에서 발생한 이벤트를 처리할 때 주의할 점은 상위 요소에 이벤트 핸들러를 등록하기 때문에, 실제로 발생시킨 DOM 요소가 기대한 DOM 요소가 아닐 수 있으므로 확인이 필요하다.

Element.prototype.matches 메서드로 전달된 선택자에 의해 특정 노드를 탐색 가능한지 확인할 수 있다.

function activate({ target }) {
  // 이벤트를 발생시킨 요소(target)이 ul#fruits의 자식 요소가 아니라면 무시한다.
  if (!target.matches('#fruits > li')) return;
  ...

일반적으로 이벤트 객체의 target 프로퍼티와 currentTarget 프로퍼티는 동일한 DOM 요소를 가리키지만, 이벤트 위임을 통해 상위 DOM 요소에 이벤트를 바인딩한 경우 target !== currentTaget일 수도 있다.

$fruits.onclick = activate;

이 때, 이벤트 객체의 currentTarget 프로퍼티는 언제나 $fruits를 가리키지만 target 프로퍼티이벤트를 발생시킨 DOM 요소를 가리킨다.

40.8 DOM 요소의 기본 동작의 조작

40.8.1 DOM 요소의 기본 동작 중단

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

<!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>
  • 상위 DOM 요소인 container 요소에 click 이벤트를 위임했다. 따라서 하위 요소에서 발생한 이벤트를 container가 캐치하여 처리한다.
  • 하지만, btn2 요소는 stopPropagation을 통해 이벤트 전파를 중단시킨다. 따라서 container는 btn2의 이벤트를 감지할 수 없다.

40.9 이벤트 핸들러 내부의 this

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

<!DOCTYPE html>
<html>
<body>
  <button onclick="handleClick()">Click me</button>
  <script>
    function handleClick() {
      console.log(this); // window
    }
  </script>
</body>
</html>
  • handleClick 함수는 이벤트 핸들러에 의해 일반 함수로 호출된다. 따라서 일반 함수 내부의 this는 window를 가리킨다.

  • 단, 이벤트 핸들러를 호출할때 인수로 전달한 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는 상위 스코프의 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>
  • 클래스에서 이벤트 핸들러를 바인딩하는 경우 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>
    • 위 예제는 increase 메서드 내부의 this는 클래스가 생성할 인스턴스를 가리키지 않는다. 이벤트 핸들러 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리키기 때문에, 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.$button.onclick = this.increase;
    
            // increase 메서드 내부의 this가 인스턴스를 가리키도록 한다.
            this.$button.onclick = this.increase.bind(this);
          }
    
          increase() {
            this.$button.textContent = ++this.count;
          }
        }
    
        new App();
      </script>
    </body>
    </html>
  • 또는 클래스 필드에 할당한 화살표 함수를 이벤트 핸들러로 등록하여 이벤트 핸들러 내부의 this가 인스턴스를 가리키도록 할 수도 있다. 다만 이때 이벤트 핸들러 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 이벤트 핸들러에 인수 전달

  • 함수에 인수를 전달하려면 함수를 호출할 때 전달해야 한다. 이벤트 핸들러 프로퍼티 방식과 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>
  • 또는 이벤트 핸들러를 반환하는 함수를 호출하면서 인수를 전달할 수 있다.

    <!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 커스텀 이벤트 생성

  • 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
  • 생성된 커스텀 이벤트 객체는 버블링되지 않으며 preventDefault 메서드로 취소할 수도 없다. 즉 커스텀 이벤트 객체는 bubbles와 cancelable 프로퍼티의 값이 false로 기본 설정된다.

    // MouseEvent 생성자 함수로 click 이벤트 타입의 커스텀 이벤트 객체를 생성
    const customEvent = new MouseEvent('click');
    console.log(customEvent.type); // click
    console.log(customEvent.bubbles); // false
    console.log(customEvent.cancelable); // false
  • 커스텀 이벤트 객체의 bubbles 또는 cancelable 프로퍼티를 true로 설정하려면 이벤트 생성자 함수의 두 번째 인수로 bubbles 또는 cancelable 프로퍼티를 갖는 객체를 전달한다.

    // MouseEvent 생성자 함수로 click 이벤트 타입의 커스텀 이벤트 객체를 생성
    const customEvent = new MouseEvent('click', {
      bubbles: true,
      cancelable: true
    });
    
    console.log(customEvent.bubbles); // true
    console.log(customEvent.cancelable); // true
  • 커스텀 이벤트 객체에는 bubbles 또는 cancelable 프로퍼티뿐만 아니라 이벤트 타입에 따라 가지는 이벤트 고유의 프로퍼티 값을 지정할 수 있다. 예를 들어, MouseEvent 생성자 함수로 생성한 마우스 이벤트 객체는 마우스 포인터의 좌표 정보를 나타내는 마우스 이벤트 객체 고유의 프로퍼티 screenX/screenY, clientX/clientY, pageX/pageY, offsetX/offsetY와 버튼 정보를 나타내는 프로퍼티 altKey, ctrlKey, shiftKey, button을 갖는다. 이러한 이벤트 객체 고유의 프로퍼티 값을 지정하려면 다음과 같이 이벤트 생성자 함수의 두 번째 인수로 프로퍼티를 갖는다.

    // 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
  • 이벤트 생성자 함수로 생성한 커스텀 이벤트는 isTrusted 프로퍼티 값이 언제나 false다. 커스텀 이벤트가 아닌 사용자의 행위에 의해 발생한 이벤트에 의해 생성된 이벤트 객체의 isTrusted 프로퍼티 값은 언제나 true다.

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

40.11.2 커스텀 이벤트 디스패치

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

  • 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>
    • 일반적으로 이벤트 핸들러는 비동기처리 방식으로 동작하지만 dispatchEvent 메서드는 이벤트 핸들러를 동기처리 방식으로 호출한다. 다시 말해, dispatchEvent 메서드를 호출하면 커스텀 이벤트에 바인딩된 이벤트 핸들러를 직접 호출하는 것과 같다.
  • 기존 이벤트 타입이 아닌 임의의 이벤트 타입을 지정하여 이벤트 객체를 생성하는 경우 일반적으로 CustomEvent 이벤트 생성자 함수를 사용한다.

    // CustomEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체를 생성
    const customEvent = new CustomEvent('foo');
    console.log(customEvent.type); // foo
  • 이때 CustomEvent 이벤트 생성자 함수에는 두 번째 인수로 이벤트와 함께 전달하고 싶은 정보를 담은 detail 프로퍼티를 포함하는 객체를 전달할 수 있다. 이 정보는 이벤트 객체의 detail 프로퍼티(e.detail)에 담겨 전달된다.

    <!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>
    • 기존 이벤트 타입이 아닌 임의의 이벤트 타입을 지정하여 커스텀 이벤트 객체를 생성한 경우 반드시 addEventListener 메서드 방식으로 이벤트 핸들러를 등록해야 한다.
    • 그 이유는, 'on + 이벤트 타입'으로 이루어진 이벤트 핸들러 어트리뷰트/프로퍼티가 요소 노드에 존재하지 않기 때문이다.