20
5월
2019

{ passive:true } 의 진정한 의미

addEventListener는 대상에 지정한 이벤트가 들어올 경우 호출할 함수를 등록하는 method입니다. 자바스크립트를 하는 사람이라면 누구나 아는 기초적인 method이죠.

하지만, 과거에는 addEventListener가 웹브라우저 별로 동작이 달랐고 버그도 많아서 jQuery 같은 라이브러리의 on(), off() method로 간단히 처리하는 경우가 많았습니다. 지금 많이 사용되는 react, vue.js 도 addEventListener를 직접 사용할 일은 많지 않습니다.

자바스크립트의 대표적인 method이지만 의외로 addEventListener를 직접 사용할 일은 점점 줄어들고 있는 것 같습니다.

AddEventListener

addEventListener는 세개의 인자를 받을 수 있습니다.

target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);
target.addEventListener(type, listener[, useCapture, wantsUntrusted  ]); // Gecko/Mozilla only

첫번째 인자는 type으로 target에 반응할 이벤트 유형 (예. click, focus, scroll, …)입니다. 두번째 인자는 listener 함수이며 지정한 이벤트가 발생할 경우 실행할 함수 (=지정한 이벤트를 전달받을 함수) 입니다.

세번째가 중요한데, 원래는 useCapture boolean 으로 이벤트 전달 방식을 capturing 또는 bubbling 방식으로 지정할 수 있는 인자입니다. [참조]

  • Capturing : 최상위 부모에서 target까지 이벤트를 아래로 순차적으로 전달하는 방식
  • Bubbling : target으로부터 최상위 부모로 이벤트를 올려보내 전달하는 방식

참고로 세번째 인자를 false로 지정하여 bubbling 방식을 사용하고, listener 함수에서 event.stopPropagation() 메소드를 사용하면 이벤트를 올려보내는 동작을 막을 수 있습니다. event.cancelBubble = true; 도 같은 역할을 합니다.

보통 상위 요소에 같은 이벤트로 등록된 listener 함수가 있을 경우, 의도치 않게 상위 요소의 listener가 실행되지 않도록 막는 용도로 많이 사용합니다.

최근 Chrome 51, Firefox 49, Safari 10(with iOS) 부터는 이 세번째 인자에 useCapture 속성을 포함한 객체를 사용할 수 있도록 변경되었는데 이것이 EventListenerOptions 입니다.

// it's same
document.addEventListener('touchstart', listener, true);
document.addEventListener('touchstart', listener, { capture:true });

EventListenerOptions

이 속성은 비교적 최근에 지원이 시작되었습니다. 지원범위는 caniuse.com을 확인하시면 됩니다.

image from https://caniuse.com/#feat=passive-event-listener

현재 EventListenerOptions에서 지원되는 속성은 다음과 같습니다.

document.addEventListener('touchstart', listener, {
  capture: false,
  once: false,
  passive: false,
});
  • capture : 이벤트 전달 방식으로 capturing(true) 또는 bubbling(false) 을 선택함.
  • once : true일 경우 이벤트를 한번만 받고 해제함.
  • passive : true일 경우 이벤트에 의해 스크롤이 블럭되는 것을 방지함.

이 세번째 객체는 대부분 “모바일 디바이스의 부드러운 스크롤” 문제를 해결하다 보면 접하게 됩니다. 스크롤에 관련된 성능 문제를 해결하다보면, addEventListener로 이벤트를 등록할 때에 { passive:true } 를 지정해서 스크롤 성능을 향상시켰다는 아티클을 많이 찾을 수 있습니다. 그런데, passive 속성이 의미하는 바는 무엇인지, 왜 스크롤 성능과 관계가 있는지 궁금해졌습니다.


Inside modern web browser…

크롬이나 파이어폭스 같은 최근의 브라우저는 브라우저 내부에 여러가지 프로세스가 있습니다. 네트워크, 브라우저, UI, GPU, 플러그인, 렌더러 등…

크롬을 예를 들면 렌더러 프로세스가 탭 내의 웹 콘텐츠를 처리하게 되는데요, 렌더러 프로세스 내부에서도 웹 콘텐츠 처리를 위한 하위 스레드들이 돌게 됩니다. main, worker, compositor, raster, …

image from Inside look at modern web browser (part 3)

렌더러 프로세스가 화면을 그리는 과정은 다음 파이프라인을 따릅니다.

image from 렌더링 성능

메인 스레드에서는 자바스크립트를 실행한 후 layout, paint를 거쳐 Layout tree를 생성한 후 컴포지터 스레드에 넘기게 됩니다. 컴포지터 스레드는 넘겨받은 Layout tree에 따라 composite를 수행하여 실제로 화면에 그리게 되지요.

즉 위의 렌더링 파이프라인 중 “Javascript ~ Paint” 까지는 메인 스레드에서 처리하고 “Composite”는 컴포지터 스레드에서 처리하게 됩니다.

성능 최적화에 관심이 있는 분들은 아마 reflow, repaint에 대한 것을 알고 계실 겁니다.

화면의 레이아웃에 영향을 주는 속성이 변경될 경우 위 파이프라인을 모두 수행하는 reflow가 발생하고, 색상이나 이미지 등 paint에 관계되는 속성만 변경될 경우 Paint부터 Composite까지 수행하는 repaint가 발생합니다.

만약 레이아웃이나 페인트에 영향을 주지 않는 경우 reflow, repaint 모두 발생하지 않고 Composite 작업만 수행하면 되는데, 이때엔 컴포지터 스레드가 메인 스레드의 처리를 기다리지 않고 바로 처리할 수 있기 때문에 성능이 매우 좋습니다.

javascript에서 CSS 속성을 변경하게 되면 layout, paint, composite 중 어느 한 작업부터 파이프라인을 따라 처리를 하게 됩니다.

참고로, CSS 속성은 웹브라우저의 조합에 따라 시작되는 파이프라인 단계가 모두 다른데요, 이에 대해 자세히 알고 싶다면 csstriggers.com 을 참조하세요.

{ passive:true } 의 진정한 의미

자바스크립트에 addEventListener()를 통해 등록된 이벤트는 컴포지터 스레드가 받습니다. 이벤트가 들어오면 컴포지터 스레드는 메인 스레드에 이벤트를 넘기고 렌더링 파이프라인에 따라 layout, paint를 거쳐 Layout tree를 기다리는 것이 원래의 동작입니다.

EventListenerOptions 의 속성 중 passive 속성이 바로 이 파이프라인과 관계가 있습니다.

{ passive:true } 의 진정한 의미는 이벤트를 받는 컴포지터 스레드에 해당 이벤트가 메인 스레드의 처리를 기다리지 않고 바로 Composite를 수행해도 된다는 힌트를 주는 것입니다.

이 속성이 설정되면 컴포지터 스레드는 원래의 동작대로 이벤트를 메인 스레드에 넘기기는 하지만, 처리를 기다리지 않고 바로 Composite를 수행합니다. 즉, 스크롤 이벤트를 받아 새 프레임을 바로 합성할 수 있다는 의미이고, 결과적으로 스크롤 성능이 향상되게 됩니다.


스크롤 성능 향상을 위해 { passive:true } 속성을 사용할 경우 e.preventDefault()를 사용할 수 없다는 아티클도 자주 찾을 수 있습니다.

document.addEventListener('touchmove', (e) => {
  e.preventDefault();
  // Do something...
},
  { passive: true }
);

위와 같은 코드는 touchmove 이벤트로 다른 동작을 수행하기 위한 목적을 가지고 있는데요, 실제로 크롬에서 실행해 보면 아래 화면과 같이 에러가 발생하고 실제로 이벤트가 막아지지 않습니다.

그 이유는 { passive:true } 힌트를 통해 컴포지터 스레드가 메인 스레드의 처리를 기다리지 않고 바로 합성해야 하는데, listener 함수에 있는 e.preventDefault()는 스크롤을 막고 메인 스레드에서 처리를 해야하는 메소드이기 때문입니다.

참고로 e.preventDefault()는 target의 기본 동작을 막는 메소드입니다. 예를 들어 target이 <a> 태그인 경우 href 속성에 의한 기본 동작은 막히게 되고 event에 대한 listener 함수만 실행되게 됩니다.

보통 <a> 태그를 <button>태그처럼 동작하게 하기 위한 목적으로 사용되곤 합니다.

따라서 e.preventDefault()를 통해 이벤트를 막을 필요가 있다면 passive 속성은 꼭 false가 되어야 합니다.


EventListenerOptions feature detection

EventListenerOptions는 비교적 최근에 지원이 시작되었으므로, feature detection을 통해 사용해야 합니다. 간단하게 Modenizr를 사용하는 방법도 있습니다만, 공식 문서에도 소개된 다음 함수로도 가능합니다.

// Test via a getter in the options object to see if the passive property is accessed
var supportsPassive = false;
try {
  var opts = Object.defineProperty({}, 'passive', {
    get: function() {
      supportsPassive = true;
    }
  });
  window.addEventListener("testPassive", null, opts);
  window.removeEventListener("testPassive", null, opts);
} catch (e) {}

// Use our detect's results. passive applied if supported, capture will be false either way.
elem.addEventListener('touchstart', fn, supportsPassive ? { passive: true } : false); 

Chrome 55+의 변경사항

EventListenerOptions가 처음 도입되었을 때에 passive 의 기본값은 모든 이벤트에 대해서 passive 속성은 false 였기 때문에 e.preventDefault() 에서 오류가 발생할 일은 없었습니다.

다만 Chrome 55+, Firefox 61+부터 특정 상황의 passive 속성의 기본값이 변경되었습니다. window, document, document.body를 대상으로 한 touchstart, touchmove 에 대해서는 passive 속성의 default는 true로 변경되었습니다. 이 변경은 Chrome(android), Firefox만 해당되기 때문에 iOS에서는 발생하지 않는데요…

그래서 지금은 EventListenerOptions의 지원상황이 다르고 웹브라우저에 따라 passive 속성의 기본값도 다른 상황이기 때문에 꼭 feature detection을 하여 { passive:false } 로 적용하거나, e.preventDefault()를 사용하지 않도록 해야합니다. 위에서 언급했듯이 passive 속성을 false로 조절할 경우 성능에 영향이 있을 수 있으므로 e.preventDefault()를 꼭 사용해야 하는지 고민이 필요할 것 같습니다.

정리

  1. EventListenerOptions는 Chrome 51+, Firefox 49+ 부터 사용할 수 있다.
  2. { passive:true } 를 설정하면 이벤트 처리와 별도로 컴포지터 스레드에서 바로 composite를 수행하기 때문에, 스크롤 성능이 향상된다.
  3. {passive:true } 를 설정할 경우 e.preventDefault()는 사용할 수 없다. passive 속성을 false로 변경하면 사용할 수 있지만 성능에 영향을 주기 때문에 e.preventDefault()를 사용하지 않아도 되는지 검토해야 한다.
  4. Chrome 55+ 부터는 window, document, document.body 의 touchstart, touchmove 이벤트에 한하여 { passive:true } 가 default이므로 e.preventDefault()를 사용하려면 passive 속성을 false로 변경할 필요가 있다.

위 문서를 작성하면서 google developer 에 등록되었던 “Inside look at modern web browser” 시리즈가 도움이 많이 되었습니다. 이 글에서는 최근의 웹브라우저들이 어떤 단계를 거쳐 렌더링을 하는지 알려주고 성능 향상을 위해 개발자가 시도할 수 있는 좋은 팁도 확인할 수 있습니다.

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다

%d 블로거가 이것을 좋아합니다: