+ JS Deep Dive: 40. 이벤트 & debounce

random-olive·2023년 2월 20일
0

프로젝트 01 : 

목록 보기
24/25

1. 이벤트 드리븐 프로그래밍

  • event-driven programming : 프로그램의 흐름을 이벤트 중심으로 제어하는 프로그래밍 방식

  • 사용자와의 상호작용에 따라 (버틀 클릭, 키보드 입력, 마우스 이동 등) 함수를 호출하여 어떤 처리를 하고 싶을 때, 보통 사용자가 행동을 "언제" 할지 몰라서 함수를 "언제" 호출해야되는지 알 수 없다.

  • 하지만 다행히도 브라우저는 이런 특정 사건을 감지하여 이벤트 event를 발생시킬 수 있기에 해당하는 타입의 이벤트가 발생했을 때 호출될 함수(이벤트 핸들러)브라우저에게 알려 호출을 위임(이벤트 핸들러 등록)한다.




2. 이벤트 핸들러 등록 방법

1) 이벤트 핸들러 attribute

* on이벤트타입 = 함수 호출문 (사실 함수 몸체 자체를 의미함)

<body>
  <button onclick="handleClick">Click</button>
  <script>
  function handleClick(){
    console.log(1)
  }
  </script>
</body>

-> 이벤트 핸들러에 인수를 전달하기 위해 함수 참조문이 아닌 함수 호출문을 전달하게 된다.

  • 결국 DOM 노드 객체의 이벤트 핸들러 프로퍼티로 변환되므로 결과적으로 이벤트 핸들러 프로퍼티 방식과 동일하다고 볼 수 있다.

  • 관심사 분리 측면에서 더이상 사용하지 않는 것이 좋지만 CBD(Component Based Development) 방식의 Angular/React/Svelte/Vue.js 같은 프레임워크/라이브러리에서는 이 방식으로 처리한다. CBD에서는 HTML, CSS, JS를 관심사가 다른 개별적인 요소로 보지 않고, 뷰를 구성하기 위한 구성요소로 보기 때문에 관심사가 다르다고 생각하지 않는다.

2) 이벤트 핸들러 property

  • Window, Document, HTMLElement 타입의 DOM 객체는 이벤트에 대응하는 이벤트 핸들러 프로퍼티를 가지고 있다. 프로퍼티의 키는 'on+이벤트 타입'으로 이루어져 있으며, 프로퍼티에 함수를 바인딩하면 이벤트 핸들러가 등록된다.
  • attribute 방식과 달리 함수 참조를 할당한다.
  • 관심사 분리 측면에서는 좋지만 하나의 프로퍼티에 하나의 이벤트 핸들러만 바인딩 가능하다.
* 이벤트타깃. on이벤트타입 = 이벤트 핸들러 (함수)

<body>
  <button>click</button>
  <script>
    const $button = document.querySelector('button');
    $button.onclick = function (){console.log(1)}; <- 다음 핸들러에 의해 재할당되었기 때문에 이 부분은 실행되지 않는다.
    $button.onclick = function (){console.log(2)};
    $button.onclick = null; <- 이벤트 핸들러 제거
  </script>
</body>

3) 이벤트 핸들러 addEventListener

  • 앞의 두 방식(DOM level 0)과 달리 DOM level 2에서 도입된 이 방식은 마지막 매개변수에는 이벤트를 캐치할 이벤트 전파 단계(capturing/bubbling)를 지정한다.
  • 마지막 매개변수가 true일 경우 캡쳐링 단계에서 이벤트를 캡쳐하고, 그 외에는 버블링 단계에서 이벤트를 캐치한다.
  • 하나 이상의 핸들러를 등록 가능하며, 호출시 등록된 순서대로 호출된다.
  • 등록한 이벤트 핸들러는 removeEventListener로 삭제 가능하며, 모든 매개변수가 같아야 제대로 삭제된다.
  • 무명의 이벤트 핸들러는 삭제할 수 없으니, 등록시에 기명 핸들러로 등록하도록 한다.
  • 기명 함수를 이벤트 핸들러로 등록할 수 없다면, 함수 자신을 가리키는 arguments.callee를 사용할 수도 있으나, 이는 코드 최적화를 방해하므로 가급적 사용하지 말고 핸들러들의 참조를 꼭 저장해두도록 하자.
* 이벤트타깃.addEventListener(이벤트타입, 이벤트핸들러[, capture 사용여부])

<body>
  <button>click</button>
  <script>
    const $button = document.querySelector('button');
    $button.addListener('click',function(){console.log(1)}, true)
    $button.addListener('click',function(){console.log(2)}, true)
    $button.removeListener('click',function(){console.log(1)},false) //마지막 매개변수 다름->실패
    $button.removeListener('click',()=>console.log(1),true) //무명 핸들러->실패
    $button.removeListener('click',function(){console.log(1)}, true) //제거 성공
  </script>
</body>


3. 이벤트 타입 : 이벤트의 종류를 나타내는 문자열 및 예시

  • 마우스, 키보드, 포커스, 폼, 값 변경, DOM 뮤테이션, 뷰, 리소스 이벤트
  • 예시 : click, dbclick, mousedown, mouseup, mousemove, mouseenter, mouseleave, keydown, keyup, focus, blur, submit, reset, input, change, readystatechange, DOMContentLoaded, resize, scroll, load, unload, abort, error 외



4. 이벤트 객체

  • 이벤트가 발생하면 이벤트에 관련한 정보를 담고 있는 이벤트 객체가 동적으로 생성되고, 핸들러의 첫밴째 인수로 전달된다. 이벤트 객체는 다음과 같은 상속 구조를 가지며, Event를 포함한 그 이하는 모두 생성자 함수이다.
  • 공통 프로퍼티 : type, target, currentTarget, eventPhase, bubbles, cancelable, defaultPrevented, isTrusted, timeStamp
  • 다양한 타입의 객체 프로퍼티 : screenX, clientX, pageX, offesetX, altKey, metakey, key ...



5. 이벤트 전파 propagation

  • DOM 트리 상에 존재하는 DOM 요소 노드에서 발생한 이벤트는 DOM 트리를 통해 전파되며, 다음과 같이 3 단계로 전파된다.
  1. 캡쳐링 단계 capturing phase : 이벤트가 상위 → 하위 요소 방향으로 전파
  2. 타겟 단계 : 이벤트가 이벤트 타깃에 도달
  3. 버블링 단계 : 이벤트가 하위 → 상위 요소 방향으로 전파
<body>
  <p>이벤트 <button>버튼</button></p>
  <script>
    document.body.addEventListener('click',()=>{console.log('버블링')}) - (3)
    document.querySelector('p').addEventListener('click',()=>{console.log('캡쳐링')})  - (1)
    document.querySelector('button').addEventListener('click',()=>{console.log('타겟')}) - (2)

</body>
  • 위의 코드의 경우, button 요소에서 클릭 이벤트가 발생하면 순차적으로 캡처링 , 타겟, 버블링 단계를 캐치하는 이벤트 핸들러가 호출된다.
//console.log 결과
캡처링
타겟
버블링



6. 이벤트 위임 delegation

  • 여러 개의 하위 DOM 요소에 각각 이벤트 핸들러를 등록하는 대신 하나의 상위 DOM 요소에 이벤트를 등록하는 방법 (상위 요소가 하위 요소의 이벤트를 캐치 가능)
  • 예를 들어, 모든 li 요소가 100개 일 때 모두 클릭 이벤트에 반응하도록 핸들러를 등록하려면 원래는 100개 등록해야 하는데, 이는 성능 저하와 유지보수에 적합하지 않다.
  • 하지만 위임을 한다면 상위 1개의 요소에만 등록해도 같은 효과를 낸다.
  • 주의할 점 : 실제 이벤트 타깃은 다를 수도 있으니 검사할 필요가 있다.
    -> Element.prototype.matches 메서드로 특정 노드를 탐색 가능한지 확인한다.
<style>
  #fruits {
     display: flex;
     list-style-type: none;
     padding: 0;
  }

  #fruits .active {
    color:red;
    text-decoration:underline;
  }
</style>

<body>
  <nav>
    <ul id="fruits">
      <li id ="apple" class="active">Apple</li>
      <li id ="banana">Banana</li>
      <li id ="orange">Orange</li>
    </ul>
  </nav>
...

  <script>
...
//위임 전
    document.getElementById('apple').onclick=activate;
    document.getElementById('banana').onclick=activate;
    document.getElementById('orange').onclick=activate;

//위임 후 (1)
    $fruits.onclick = activate;

</script>
</body>
  • (1) : 이 경우 이벤트 객체의 currentTarget 프로퍼티는 $fruit 요소를 가리키지만 이벤트 객체의 target 프로퍼티는 실제로 이벤트를 발생시킨 DOM 요소를 가리킬 수 있다.



7. DOM 요소의 기본 동작 조작

1) DOM 요소의 기본 동작 중단 : preventDefault

2) 이벤트 전파 방지 : stopPropagation

  • 예시: 상위 DOM에 위임시키고 하위 요소 이벤트 발생시 해당 요소에게 바인딩된 이벤트 핸들러만 실행시키고 싶을 때 (다른 요소에서 캐치되어서 실행되지 못하게)
e.preventDefault();
e.stopPropagation();



8. 이벤트 핸들러 내부의 this, 이벤트 핸들러에 인수 전달

9. 커스텀 이벤트

10. debounce & throttle

  • DOM 이벤트를 제어하는 방법

  • 사용 이유 : 사용자가 스크롤과 같은 수많은 이벤트를 발생시켰을 때 이에 대한 무수한 callback이 발생하고, 이는 큰 리소스 소모로 이어짐. 이러한 과도한 실행횟수를 줄여 성능 문제를 해결하기 위함

  • 사용 사례

    • 사용자가 창 크기 조정을 멈출 때까지 기다렸다가 resize
    • 사용자가 키보드 입력을 중지할 때까지 stop
    • 페이지의 스크롤 위치를 측정하고 일정 간격으로만 응답
  • Debounce : 연이어 호출되는 함수들을 그룹화해 하나의 이벤트만 발생시킴

    • 활용 사례 : resize, keyboard
  • Throttle : 이벤트를 일정 주기마다 발생하도록 함

    • 사례 : scroll (infinite scroll)
    • 무한 스크롤 : 사용자가 footer까지 스크롤하면 추가 컨텐츠를 요청하는 원리. 디바운싱을 적용하면 footer까지 다 가서야 컨텐츠를 요청하기 때문에 그 전에 요청하기 위해 스로틀을 사용한다.
profile
Doubts kills more dreams than failure ever will

0개의 댓글