Passive Event Listener

이짜젠·2022년 4월 13일
5

최근 업무에서 특정 React 컴포넌트의 기본 wheel event의 동작을 막아달라는 니즈가 있었다.
위 내용을 처리하면서 대충 알고있던 Passive Event Listener 대해서 조금 더 자세히 들여다보게 되었다.


Passive Event Listener 란?

브라우저의 기본동작에 영향을 미치지 않는 Event Listener

Chrome 51 부터 등장한 스크롤 동작의 최적화를 위해 등장한 기능이며, 스크롤을 유발하는 touchwheel 이벤트에서 주로 사용된다. (브라우저별 지원현황 확인하기)

단순히 코드를 짜는 입장에서는 "Event.PreventDefault를 사용할 수 없는 이벤트 리스너" 정도로만 이해를해도 괜찮을 것 같다.


사용법

Event.addEventListener(type, listener, EventListenerOptions)

addEventListener의 세번째 파라미터인 EventListenerOptions 객체에 {passive: true} 값을 넘겨주면 passive EventListener로 등록이 된다.

아래코드는 checkbox input 엘리먼트의 click 이벤트에 Passive Event Listener를 등록한 예시다.

const chkbox = document.createElement("input");
chkbox.setAttribute("type", "checkbox");
chkbox.addEventListener(
  "click",
  (e) => {
    console.log("change!");
    e.preventDefault();
  },
  { passive: true }
);

document.body.appendChild(chkbox);

click 이벤트는 cancelable 하고, 콜백에서 e.preventDefault 호출하고있음에도 불구하고, 체크박스를 클릭해보면 정상적으로 체크처리가 되고있다.
또한 "Unable to preventDefault inside passive event listener invocation". 라는 에러로그도 함께 보인다.


언제 사용할까?

  • 스크롤의 성능을 개선하고 싶을 때
  • 브라우저 고유의 기능을 지키고 싶을 때

e.preventDefault를 막기위해서도 사용할 수 있지만, 주로 버벅거리는 스크롤을 최적화 하고 싶을때 사용된다.
도입부에 이야기했듯이, 이 기능이 등장한 배경 자체가 스크롤의 성능개선을 위해서다.

기본적으로 브라우저의 이벤트는, 리스너의 콜백이 처리된 이후 브라우저의 고유동작이 일어난다.
따라서 스크롤을 버벅거리게 하는 대부분의 원인은, 스크롤을 유발하는 이벤트 핸들러의 콜백함수에 있다.

간단한 예로,
만약 아래 코드처럼, 콜백함수안에 부하가 많은 작업(longTask 함수)이 있다고 가정해보자.
longTask의 처리가 완료된 후에 "wheel" 이라는 콘솔이 찍히고 스크롤이 일어나게된다.

function longTask() {
  const start = Date.now();
  while (Date.now() < start + 300) {}
}

document.addEventListener(
  "wheel",
  (e) => {
    longTask();
    console.log("wheel", e.timeStamp);
  },
);

{ passive: false } 인 경우, 예상한대로 콘솔이 찍힌 후 스크롤이 동작한다.

{ passive: true }인 경우, 콘솔이 찍히는것과 별개로 페이지 스크롤이 부드럽게 동작한다.

이처럼 passive event handler는 브라우저의 기본동작에 영향을 줄 수 없기때문에, scroll이 핸들러에 의해 블락되지 않고 마치 비동기처럼 동작하게된다.

이렇게 UX에 영향을 주는 이벤트들의 경우, Passive Event Listener를 사용하여 사용자의 불편함을 최소화 할 수 있다.

Default Passive Event Listener

passive의 default 값은 false 다.
그러나 Chrome 54+ 부터 아래와 같은 특별한 경우들은 passive 속성의 기본값이 true가 된다.

  • document또는 body에 이벤트 리스너를 추가할때
  • touchstart, touchmove와 같이 스크롤이 블록되는 이벤트인 경우

원리

크롬을 기준으로 브라우저에서 이벤트가 발생하면 대략 다음의 과정을 따른다.

  1. Browser Process가 이벤트를 감지 후, 이벤트의 타입과 좌표를 Renderer Process에게 넘긴다.

  2. Renderer Processcompositor thread는 해당 이벤트가 발생한 영역이 non-fast scrollable region 인지 확인한다. (non-fast scrollable region란, 이벤트 핸들러가 등록된 영역을 말한다.)

  3. non-fast scrollable region라면, main thread 에게 이를 알린다. (핸들러의 콜백을 수행)

  4. compositor threadmain thread의 작업이 끝날때까지 기다리고, 완료되면 다시 composite을 수행한다.

이미지 출처: https://developer.chrome.com/blog/inside-browser-part4

그러나 { passive: true } 로 핸들러를 등록을 하면, non-fast scrollable region 영역으로 인지를 하지 않게되고, 따라서 main thread의 작업이 끝날때까지 기다리지 않고, composite을 수행한다.


결론

Passive Event Listener를 직역하면 "수동적인 이벤트 수신기" 정도가 될 것 같은데, 네이밍의 명확한 의미와 유래는 찾을 수 없었다.

쉬운 기억을 위해 굳이 짜맞춰보자면,
이벤트가 발생했을때, 브라우저의 Renderer Process가 매번 감시하는 핸들러가 아닌, 수동적으로 감시하는 핸들러라고 이해하면 어떨까 싶다. like 코로나 수동감시대상
(여기서 감시란, 핸들러의 콜백의 수행이 완료되었는지를 체크하는걸 의미한다.)

throttle, debounce 와 함께 스크롤 최적화를 위한 좋은 옵션이 될 수 있을 것 같다.


참고

profile
오늘 먹은 음식도 기억이 안납니다. 그래서 모든걸 기록합니다.

0개의 댓글