[JavaScript] 이벤트 다루기

서동경·2023년 2월 17일
0
post-thumbnail

🍀 이벤트 (Event)

이벤트란 HTML 문서에서 발생하는 어떠한 상황을 뜻한다.

🌱 자바스크립트의 이벤트 처리

자바스크립트에서 가장 보편적인 이벤트 처리 방법은 querySelector() 메서드를 통해 HTML의 요소를 찾아서 가져온 후, addEventListener() 메서드를 통해 이벤트 핸들러를 등록하는 것이다.

<button id="myButton">Click Me</button>

해당 요소를 자바스크립트로 다루는 방법은 다음과 같다.

const myButton = document.querySelector("#myButton");
myButton.addEventListener("click", function() {
	alert("hello world");
});

querySelector() 메서드를 통해 id가 'myButton'인 요소 하나를 자바스크립트로 가져온 후, addEventListener() 메서드를 통해 클릭 이벤트가 발생할 경우 이벤트 핸들러, 즉 alert() 메서드를 실행시킨다. 한편 여러개의 요소를 가져올 때는 querySelectorAll() 메서드를 사용한다. tag-name, id(#), class-name(.) 모두 querySelector(), querySelectorAll() 메서드로 가져올 수 있다.

그외에도 HTML과 자바스크립트의 이벤트 속성( [ex] onclick )을 이용하는 방법이 있다.

🌱 이벤트 전파(Propagation)

🧩 이벤트 버블링(Bubbling)

브라우저가 기본적으로 선택하는 방식인 이벤트 버블링을 먼저 살펴보자. 이벤트 버블링이란, 거품이 수면 위로 올라가는 현상처럼 하위 요소에서 이벤트가 발생할 경우 상위 요소까지 해당 이벤트가 전파되는 현상이다.

이벤트 버블링으로 인해 하위 요소에서 일어난 이벤트를 상위 요소에서 처리할 수 있다. 그러나 원하지 않은 동작을 일으키기도 해 문제가 되기도 한다.

"outer - inner - btn" 구조를 가진 요소를 만들어서 버블링에 대해 알아보자.

<div id="outer">
  <div id="inner">
    <button id="btn">Click me</button>
  </div>
</div>

그리고 자바스크립트는 각 영역에 클릭 이벤트가 발생할 때마다 콘솔에 출력되도록 작성하였다.

const outer = document.querySelector("#outer");
outer.addEventListener("click", function () {
  console.log("outer clicked");
});

const inner = document.querySelector("#inner");
inner.addEventListener("click", function () {
  console.log("inner clicked");
});

const btn = document.querySelector("#btn");
btn.addEventListener("click", function (event) {
  console.log("button clicked");
});

이제 가장 하위 요소인 'btn'을 클릭하고 출력을 살펴보자.

모든 영역에서 클릭 이벤트가 발생하여 각각의 이벤트 핸들러들이 실행되었다. 위 출력은 버블링의 장단점을 모두 보여준다.

만약 개발자가 하위 요소에 이벤트를 등록하지 않아도 상위 요소에서 해당 이벤트를 처리할 수 있도록 설계하길 원한다면 장점이 된다.

하지만 개발자가 'btn'의 이벤트만 반응하여 해당 이벤트 핸들러만 실행되도록 설계하길 원한다면 단점이다. 즉 "button clicked"만 실행되어야 하는데 상위 요소로 타고 타고 올라가서 결국 모든 상위 요소에 event가 발생하여 "inner clicked", "outer clicked"도 같이 출력된다.

그렇다면 버블링을 없애는 방법을 알아보자.

const outer = document.querySelector("#outer");
outer.addEventListener("click", function () {
  console.log("outer clicked");
});

const inner = document.querySelector("#inner");
inner.addEventListener("click", function () {
  console.log("inner clicked");
});

const btn = document.querySelector("#btn");
btn.addEventListener("click", function (event) {
  console.log("button clicked");
  event.stopPropagation(); // 이벤트 버블링 방지 코드
});

위와 같이, 원하는 출력을 얻고 난 후 이벤트의 메서드인 stopPropagation()를 통해 버블링이 일어나는 것을 중지시도록 코드를 수정했다.

그리고 다시 'btn'을 클릭하면,

이벤트가 더이상 상위 요소로 전파되지 않는 것을 확인할 수 있다.

🧩 이벤트 캡처링(Capturing)

이번에는 이벤트 캡처링에 대해 알아보겠다. 캡처링은 버블링과 반대로, 즉 상위 요소부터 하위에 있는 타켓 요소를 향하여 이벤트가 전파되는 현상이다. 이벤트 버블링과 마찬가지로 발생한 이벤트를 부모 요소에서 처리할 수 있다는 장점이 있다.

캡처링은 기본 특성이 아니므로 이를 발생시키기 위해서 addEventListener() 메서드의 세 번째 인자를 이용해야한다. 'true'(혹은 객체 형식인 '{cature: true}')를 넣어주면 버블링이 일어나지 않고 캡처링이 일어난다.

버블링 예제와 같은 HTML 요소를 통해 캡처링에 대해 알아보자

const outer = document.querySelector("#outer");
outer.addEventListener("click", function () {
  console.log("outer clicked");
}, true);

const inner = document.querySelector("#inner");
inner.addEventListener("click", function () {
  console.log("inner clicked");
}, true);

const btn = document.querySelector("#btn");
btn.addEventListener("click", function (event) {
  console.log("button clicked");
}, true);

똑같은 자바스크립트 코드에서 addEventListener() 메서드의 세 번째 인자에 'true'를 넣었다. 이제 각 요소는 버블링이 아닌 캡처링이 일어난다. 이제 btn 영역을 클릭하면 어떻게 출력될까?

버블링 예제와 비슷한 출력같지만, 출력 순서가 반대이다. 즉 상위 요소에서 하위 요소로 이벤트가 전파되는 것을 확인할 수 있다.

🔎 이벤트 전파의 단계

이벤트 전파는 다음과 같은 순서를 따른다.

  1. 캡처링 단계: 이벤트가 하위 요소로 전파되는 단계
  2. 타켓 단계: 이벤트가 타겟 요소에 전달되는 단계
  3. 버블링 단계: 이벤트가 상위 요소로 전파되는 단계

그러므로 특정 요소에 이벤트 리스너를 통해 클릭 이벤트를 등록한 후, 해당 요소에서 클릭 이벤트를 발생시킨다면 상위 요소부터 클릭 이벤트가 발생한 요소까지 이벤트가 전파되는 캡처링 단계를 거쳐, 해당 요소에 이벤트가 전달되는 타겟 단계를 거친 후, 다시 상위 요소로 이벤트가 전파되는 버블링 단계를 거친다.

근데 아무리 버블링이 기본값이라 해도 캡처링도 "전파의 단계"에 속하는데 왜 실제 코드를 실행시키면 버블링 단계만 나타날까라는 의문이 들었다.

그래서 ChatGPT에게 물어봤다.

브라우저에서는 이벤트 리스너에 capture: true를 입력하지 않으면 캡처링 단계가 "활성화"되지 않는다고 한다.

즉 이벤트 전파 단계에는 캡처링 단계 버블링 단계가 모두 포함되어있어도 이것은 설계에 대한 이야기일뿐, 실제로는 "어떤 방식을 선택하는 것"이라고 이해하자.

🌱 이벤트 위임(Delegation)

이벤트 위임이란 부모 요소에 이벤트 리스너를 등록하고, 자식 요소에서 발생한 이벤트가 부모 요소에서 처리되도록 하는 기술이다.

다음 HTML 문서를 통해 이벤트 위임을 알아보자.

<ul id="myList">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

3개의 'li' 요소가 있다. 그러나 각각 이벤트 리스너를 달아주는 것은 너무 번거롭다. 이벤트 버블링을 활용하면 귀찮음을 덜 수 있다. 상위 요소인 'ul'에만 이벤트 리스너를 등록해보자.

const myList = document.querySelector("#myList");

myList.addEventListener("click", (event) => {
  // 클릭된 요소가 li 태그인 경우에만 처리 !
  if (event.target.tagName === "LI") {
    console.log("You clicked on:", event.target.textContent);
  }
});

이렇게 상위에 하나의 이벤트 리스너를 등록함으로써 아래에 있는 여러개의 하위 요소의 이벤트를 처리할 수 있다. 각 리스트를 순서대로 클릭해보면 다음과 같이 출력된다.

이런식으로, 개별적으로 이벤트 리스너가 등록되어있지 않더라도 원하는 결과를 얻을 수 있다.

🌱 이벤트 제어

클로저를 이용한 Debounce, Throttle을 통해 무분별하게 발생하는 이벤트를 제어할 수 있다.

🧩 Debounce

Debounce는 이벤트가 연속적으로 발생했을 때, 일정 시간을 두고 마지막 이벤트만 처리하는 기술이다.

예를들어, 검색창에 키워드를 입력할 때마다 검색 결과를 업데이트하는 경우, 사용자가 타이핑을 완료하기 전에는 검색을 실행하지 않고 일정 시간이 지난 후에 검색을 실행하도록 구현할 수 있다.

  <body>
    <p>Debounce Example</p>
    <input type="text" id="search-input" />
  </body>

  <script>
    // debounce 함수 정의
    function debounce(callback, delay) {
      let timer;
      return function () {
        clearTimeout(timer);
        timer = setTimeout(callback, delay);
      };
    }

    // 검색창에 debounce 적용
    const input = document.querySelector("#search-input");
    input.addEventListener(
      "input",
      debounce(() => {
        console.log("search");
      }, 500)
    );
  </script>

검색창에 입력을 할 때마다 "search"라는 메시지를 출력하고 debounce() 함수를 사용하여 입력 이벤트가 자주 발생하는 것을 방지하는 코드이다.

debounce() 함수는 고차 함수로, 전달받은 callback 함수를 delay 시간 동안 지연시켜 실행한다. 내부적으로는 timer 변수를 정의하고 있고 클로저 함수를 반환한다.

클로저 함수는 timer 변수를 초기화하고 setTimeout() 함수를 통해 delay 시간 후에 callback 함수를 실행하도록 새로운 timer를 설정한다.

그리고 검색창에 Debounce를 적용하기 위해, 검색창에 input 이벤트가 발생할 때 실행할 이벤트 핸들러를 등록한다. 그렇다면 input 이벤트 발생 시 callback, delay 매개 변수의 값을 가진 debouce() 함수가 실행된다. debouce() 함수가 실행되더라도 이벤트가 발생할 때마다 timer를 설정하기 위한 지연이 있기 때문에, callback 함수까지 실행되려면 500ms동안 이벤트가 발생하지 않아야 한다.

🧩 Throttle

Throttle은 이벤트가 한번 발생하면 일정 시간동안 이벤트를 무시하는 기술이다.

예를 들어, 스크롤을 하여 일단 이벤트가 발생하면 그 후 일정 시간 동안은 이벤트를 무시하도록 설정하여 과도한 스크롤을 방지할 수 있다.

  <body>
    <div style="height: 500vh">
      <p>Throttle Example</p>
    </div>
  </body>

  <script>
    // throttle 함수 정의
    function throttle(callback, delay) {
      let timer;
      return function () {
        if (!timer) {
          timer = setTimeout(() => {
            callback();
            timer = null;
          }, delay);
        }
      };
    }

    // 스크롤 이벤트에 throttle 적용
    window.addEventListener(
      "scroll",
      throttle(() => {
        console.log("scroll");
      }, 1000)
    );
  </script>

throttle() 함수를 사용하여 스크롤 이벤트가 자주 발생하는 것을 방지하는 코드이다.

throttle() 함수는 고차 함수로, 전달받은 callback 함수를 delay 시간 동안 지연시켜 실행한다

브라우저 스크롤에 Throttle을 적용하기 위해, window 객체에 scroll 이벤트가 발생할 때마다 throttle() 함수를 실행하는 이벤트 핸들러를 등록한다. 그렇다면 scroll 이벤트 발생 시 callback, delay 매개 변수의 값을 가진 throttle() 함수가 실행된다. throttle() 함수가 실행되면 500ms 동안 대기한 후 callback 함수를 실행한다. 즉 연속적으로 이벤트가 발생한다하더라도 한번 이벤트가 발생하면 다음 이벤트가 발생할 때까지 500ms의 시간 간격을 두게된다.

profile
개발 공부💪🏼

0개의 댓글