💻 Frontend
이벤트 리스너의 passive option이란?
category
💻 Frontend
{ passive: true }
웹 개발자라면 다들
addEventListender()
를 통해 이벤트를 등록해봤을 것입니다. 이때 이벤트 리스너의 옵션을 지정할 수 있다는 사실을 알고계셨나요? 그 중 성능과 연관된 passive option에 대해 알아보겠습니다.
addEventListener(type, listener, options)
브라우저 렌더링 과정의 이해
passive option이 어떠한 원리로 성능을 향상시키는지 이해하기 위해선, 먼저 브라우저가 화면을 어떻게 그리는지 살펴볼 필요가 있습니다.
렌더러 프로세스란 HTML, CSS, JavaScript를 실제 웹 페이지, 눈으로 볼 수 있는 픽셀로 변환하는 과정을 일컫습니다. 이 작업을 보다 효율적으로 수행하기 위해, 기본적으로 모든 작업을 처리하는
Main thread
에 더하여 Worker threads
, Compositorthread
, Raster thread
등이 별도로 존재하며, 각자의 역할을 수행합니다.이 과정을 대략적으로 나열해보면 아래와 같습니다.
- Retrieving HTML: HTML 데이터를 수신합니다.
- Parse HTML: HTML을 해석/파싱합니다.
- Loading Subresources: CSS, JavaScript, Image 등 하위 리소스를 로드합니다.
- Build DOM Tree: 해석된 HTML을 DOM 객체 트리로 구축합니다.
- Build CSSOM Tree: CSS를 해석하여 CSSOM 객체 트리를 구축합니다.
- Compute Style: 생성된 DOM 및 CSSOM 트리를 조합하여, 페이지를 렌더링하는 데에 필요한 노드로 구성된 Render Tree를 구축합니다.
- Layout(Reflow): 뷰포트 내에서 각 노드의 정확한 위치와 크기를 계산하며, 렌더 트리에 반영합니다.
- Paint(Rasterize): 그리기 단계. 렌더 트리의 노드들을 실제 화면의 픽셀로 변환하여 레이어로 구성합니다. 위치와 관계없는 CSS 속성만이 적용됩니다.
- Composite: Paint 단계에서 생성한 레이어들을 합성하고, 화면을 업데이트합니다.
웹 페이지 퍼포먼스 최적화를 다룰 때 언급되는 단어로
reflow
와 repaint
를 꼽을 수 있습니다.reflow
란 레이아웃에 무언가 변화가 발생하는 경우, 브라우저가 문서 내 요소들의 위치와 크기를 다시 계산하고, 일부나 전체 범위에서 다시 렌더링하게 되는 동작을 말합니다. 위 파이프라인을 처음부터 반복하고, 화면을 새로 그려야 하기 때문에 부담이 큽니다.색상이나 이미지 같이 위치나 크기가 변하지 않는 정도라면
Paint
와 Composite
프로세스만 다시 수행하기도 하는데, 이것은 repaint
라고 합니다.따라서 이러한 동작이 어느 경우에 발생하는지를 미리 파악하고, 최대한 회피(특히
reflow
)하는 것이 렌더링 최적화의 핵심입니다. DOM을 주의 깊게 조작하지 않으면 생각 이상으로 쉽게 발생하기도 하며, 당연히 필요한 상황이어서 불가피하게 발생하는 경우 등도 있기 때문에 좀처럼 접근하기 어려운 부분이기도 합니다.addEventListener()
로 등록된 이벤트는 Compositor thread
에서 처리되는데, 원래라면 수신한 이벤트를 Main thread
에 넘기고 자신은 Render tree
를 다시 넘겨받을 때까지 기다리게 됩니다.그런데
passive
옵션이 활성화되었다면, Compositor thread
는 Main thread
의 프로세스를 기다리지 않고 자신의 작업인 Paint
와 Composite
를 즉시 수행하게 됩니다.이벤트를 수신하는 즉시 핸들러를 실행하고, 화면에 바로 반영/합성시킬 수 있게 됨으로써 렌더링 퍼포먼스가 향상되는 원리인 것입니다.
Event.preventDefault()
즉, 다시 말하면 passive 옵션이란
Compositor thread
에게 Main thread
의 작업을 대기하지 않고 자신의 작업을 즉시 수행하도록 지시하는 것이라고 할 수 있습니다. passive라는 표현 역시 이벤트의 취소 여부를 (능동적으로) 관찰하지 않는 수동적인 이벤트 리스너라는 의미에서 비롯된 바일 것입니다.다만 사용할 수 없게 되는 메소드가 하나 있는데, 바로 취소 가능한 이벤트(
Event.cancelable
)의 기본 동작을 취소하는 preventDefault()
입니다.이 메소드는
Main thread
에서 처리되는데, passive 옵션은 Main thread
를 의도적으로 배제하고 있으므로 호출이 불가하게 되며, 호출하는 경우에는 에러가 발생합니다. 정확히는 그냥 무시되는 것인데, Chrome 같은 경우는 아래와 같이 콘솔에 에러를 표시합니다.passive
옵션이 활성화되지 않았다면 preventDefault()
의 호출 여부를 지속적으로 관찰한다는 것이고, 이벤트 리스너의 입장에서는 이벤트가 취소될지 안 될지 알 수 없습니다. 때문에 항상 핸들러의 작업이 완료될 때까지 기다리고 보는 것이고, 이것이 차단(blocking)을 발생시키게 되는 것입니다.- passive: false →
preventDefault()
를 감지
- passive: true → 이벤트를 즉시 실행,
preventDefault()
호출 불가
이것이 passive option 기본값이 true가 아닌 이유일 것이다. passive를 true로 설정하면 개발자가 명시적으로 preventDefault를 호출할 수 없게 되므로 특정 기능에 제한될 수도 있다. 그렇기 때문에 무작정 true로 명시하는 것보단 정말로 필요한 옵션인지 생각해보자.
scroll event
scroll 이벤트는 취소가 불가능한 이벤트이기 때문에 passive 옵션을 신경쓰지 않아도 됩니다. 취소가 불가능하다는 것은 곧 이벤트 리스너가 렌더링을 차단할 수 없다는 의미이기 때문입니다.
touch, wheel event
브라우저에서는 터치 혹은 휠 이벤트가 발생할 때 마다 스크롤을 할지 말지 결정을 해야 합니다. 스크롤이 발생하면 많은 이벤트가 발생하게 되는데, 이벤트 마다 스크롤을 할지 말지 판단하게 된다면 브라우저는 느려질 수 있습니다.
이때 passive 속성을 사용해서 브라우저에게 preventDefault를 사용해서 스크롤을 막지 않겠다고 알려줍니다. 그러면 브라우저는 항상 스크롤을 하는 것으로 인식하고, 스크롤 할지 말지 결정하는데 낭비되는 자원을 절약해서 성능을 향상 시킬 수 있게 됩니다.
마무리
정리
passive option은 이벤트를 취소하지 않을 것임을 미리 브라우저에게 선언하기 때문에 이벤트 성능 향상에 도움이 된다.
브라우저에게 preventDefault를 사용해서 스크롤을 막지 않겠다고 알려준다. 그러면 브라우저는 항상 스크롤을 하는 것으로 인식하고, 스크롤 할지 말지 결정하는데 낭비되는 자원을 절약해서 성능을 향상 시킬 수 있게 된다.
다만 기본값이 true가 아닌 이유는 preventDefault를 명시적으로 선언할 수 없기 때문이다.