컴포저이터에 입력이 들어옴
이 블로그는 Chrome 내부를 살펴보는 4부작 시리즈의 마지막으로, Chrome에서 웹사이트를 표시하기 위해 코드를 처리하는 방식을 살펴봅니다. 이전 게시물에서는 렌더링 프로세스를 살펴보고 컴포저에 관해 알아봤습니다. 이 게시물에서는 사용자 입력이 들어올 때 컴포지터가 원활한 상호작용을 지원하는 방법을 살펴봅니다.
브라우저 관점에서의 입력 이벤트
'입력 이벤트'라고 하면 텍스트 상자에 입력하거나 마우스를 클릭하는 것만 생각할 수 있지만 브라우저의 관점에서 입력은 사용자의 모든 동작을 의미합니다. 마우스 휠 스크롤은 입력 이벤트이고 터치 또는 마우스 오버도 입력 이벤트입니다.
화면에서 터치와 같은 사용자 동작이 발생하면 브라우저 프로세스가 처음에 동작을 수신합니다. 그러나 탭 내부의 콘텐츠는 렌더러 프로세스에서 처리하므로 브라우저 프로세스는 해당 동작이 발생한 위치만 인식합니다. 따라서 브라우저 프로세스는 이벤트 유형 (예: touchstart
)과 좌표를 렌더러 프로세스로 전송합니다. 렌더러 프로세스는 이벤트 타겟을 찾고 연결된 이벤트 리스너를 실행하여 이벤트를 적절하게 처리합니다.
![입력 이벤트](https://developer.chrome.com/static/blog/inside-browser-part4/image/input-event-265a73164e715.png?authuser=8&hl=ko)
컴포저블이 입력 이벤트 수신
이전 게시물에서는 래스터라이즈된 레이어를 컴포지션하여 스크롤을 원활하게 처리하는 방법을 살펴봤습니다. 입력 이벤트 리스너가 페이지에 연결되어 있지 않으면 컴포저 스레드는 기본 스레드와 완전히 독립된 새 컴포지트 프레임을 만들 수 있습니다. 하지만 일부 이벤트 리스너가 페이지에 연결된 경우는 어떻게 해야 하나요? 컴포저 스레드는 이벤트를 처리해야 하는지 어떻게 알 수 있나요?
빠르게 스크롤할 수 없는 영역 이해
JavaScript 실행은 기본 스레드의 작업이므로 페이지가 컴포지션될 때 컴포저 스레드는 이벤트 핸들러가 연결된 페이지의 영역을 '빠르게 스크롤할 수 없는 영역'으로 표시합니다. 이 정보를 통해 컴포저 스레드는 이벤트가 해당 영역에서 발생하면 입력 이벤트를 기본 스레드로 전송할 수 있습니다. 입력 이벤트가 이 영역 외부에서 발생하면 컴포저 스레드는 메인 스레드를 기다리지 않고 새 프레임 컴포지션을 계속 진행합니다.
![빠르게 스크롤할 수 없는 제한된 영역](https://developer.chrome.com/static/blog/inside-browser-part4/image/limited-non-fast-scrollab-376be5ee2cd6b.png?authuser=8&hl=ko)
이벤트 핸들러 작성 시 주의해야 할 사항
웹 개발에서 일반적인 이벤트 처리 패턴은 이벤트 위임입니다. 이벤트가 번들로 표시되므로 최상위 요소에 이벤트 핸들러를 하나 연결하고 이벤트 타겟을 기반으로 작업을 위임할 수 있습니다. 아래와 같은 코드를 보거나 작성해 보셨을 수 있습니다.
document.body.addEventListener('touchstart', event => {
if (event.target === area) {
event.preventDefault();
}
});
모든 요소에 이벤트 핸들러를 하나만 작성하면 되므로 이 이벤트 위임 패턴의 인체공학적 특성이 매력적입니다. 그러나 브라우저의 관점에서 이 코드를 보면 이제 전체 페이지가 빠르게 스크롤할 수 없는 영역으로 표시됩니다. 즉, 애플리케이션이 페이지의 특정 부분에서 발생하는 입력을 신경 쓰지 않더라도 컴포저 스레드는 입력 이벤트가 들어올 때마다 기본 스레드와 통신하고 이를 기다려야 합니다. 따라서 컴포저의 원활한 스크롤 기능이 손상됩니다.
![전체 페이지에서 빠르게 스크롤할 수 없는 영역](https://developer.chrome.com/static/blog/inside-browser-part4/image/full-page-non-fast-scroll-dd3010cc3ee0f.png?authuser=8&hl=ko)
이를 방지하려면 이벤트 리스너에서 passive: true
옵션을 전달하면 됩니다. 이렇게 하면 브라우저에 기본 스레드에서 계속 이벤트를 수신 대기하고 싶지만 컴포저는 새 프레임을 계속 컴포지팅할 수 있다는 힌트를 줍니다.
document.body.addEventListener('touchstart', event => {
if (event.target === area) {
event.preventDefault()
}
}, {passive: true});
이벤트를 취소할 수 있는지 확인
![페이지 스크롤](https://developer.chrome.com/static/blog/inside-browser-part4/image/page-scroll-2cf8d804f8587.png?authuser=8&hl=ko)
페이지에 스크롤 방향을 가로 스크롤로만 제한하려는 상자가 있다고 가정해 보겠습니다.
포인터 이벤트에서 passive: true
옵션을 사용하면 페이지 스크롤이 원활할 수 있지만 스크롤 방향을 제한하기 위해 preventDefault
하려는 시점에 세로 스크롤이 시작되었을 수 있습니다. event.cancelable
메서드를 사용하여 이를 확인할 수 있습니다.
document.body.addEventListener('pointermove', event => {
if (event.cancelable) {
event.preventDefault(); // block the native scroll
/*
* do what you want the application to do here
*/
}
}, {passive: true});
또는 touch-action
와 같은 CSS 규칙을 사용하여 이벤트 핸들러를 완전히 삭제할 수 있습니다.
#area {
touch-action: pan-x;
}
이벤트 타겟 찾기
![Hit Test](https://developer.chrome.com/static/blog/inside-browser-part4/image/hit-test-39f1d20855064.png?authuser=8&hl=ko)
컴포저 스레드가 입력 이벤트를 기본 스레드로 전송하면 가장 먼저 실행되는 것은 이벤트 타겟을 찾기 위한 히트 테스트입니다. 히트 테스트는 렌더링 프로세스에서 생성된 페인트 레코드 데이터를 사용하여 이벤트가 발생한 점 좌표 아래에 있는 항목을 찾습니다.
기본 스레드로의 이벤트 전달 최소화
이전 게시물에서는 일반적인 디스플레이가 초당 60번 화면을 새로고침하는 방식과 원활한 애니메이션을 위해 이 케이던스를 따라야 하는 방법을 설명했습니다. 입력의 경우 일반적인 터치 스크린 기기는 초당 60~120회의 터치 이벤트를 전송하고 일반적인 마우스는 초당 100회의 이벤트를 전송합니다. 입력 이벤트의 충실도가 화면이 새로고침할 수 있는 것보다 높습니다.
touchmove
와 같은 연속 이벤트가 초당 120번 기본 스레드로 전송되면 화면이 새로고침될 수 있는 속도에 비해 과도한 수의 히트 테스트와 JavaScript 실행이 트리거될 수 있습니다.
![필터링되지 않은 이벤트](https://developer.chrome.com/static/blog/inside-browser-part4/image/unfiltered-events-5c569da8f6466.png?authuser=8&hl=ko)
Chrome은 기본 스레드의 과도한 호출을 최소화하기 위해 연속 이벤트 (예: wheel
, mousewheel
, mousemove
, pointermove
, touchmove
)를 병합하고 다음 requestAnimationFrame
직전까지 전달을 지연합니다.
![병합된 이벤트](https://developer.chrome.com/static/blog/inside-browser-part4/image/coalesced-events-42fbb3813612d.png?authuser=8&hl=ko)
keydown
, keyup
, mouseup
, mousedown
, touchstart
, touchend
와 같은 개별 이벤트는 즉시 전달됩니다.
getCoalescedEvents
을 사용하여 프레임 내 이벤트 가져오기
대부분의 웹 애플리케이션의 경우 병합된 이벤트만으로도 우수한 사용자 환경을 제공하기에 충분합니다.
그러나 그리기 애플리케이션을 빌드하고 touchmove
좌표를 기반으로 경로를 배치하는 경우 부드러운 선을 그리기 위해 중간 좌표가 손실될 수 있습니다. 이 경우 포인터 이벤트에서 getCoalescedEvents
메서드를 사용하여 병합된 이벤트에 관한 정보를 가져올 수 있습니다.
![getCoalescedEvents](https://developer.chrome.com/static/blog/inside-browser-part4/image/getcoalescedevents-ab59c1cb92e1b.png?authuser=8&hl=ko)
window.addEventListener('pointermove', event => {
const events = event.getCoalescedEvents();
for (let event of events) {
const x = event.pageX;
const y = event.pageY;
// draw a line using x and y coordinates.
}
});
다음 단계
이 시리즈에서는 웹브라우저의 내부 작동 방식을 다뤘습니다. DevTools에서 이벤트 핸들러에 {passive: true}
를 추가하는 것이 권장되는 이유나 스크립트 태그에 async
속성을 작성하는 이유를 생각해 본 적이 없다면 이 시리즈를 통해 브라우저가 더 빠르고 원활한 웹 환경을 제공하기 위해 이러한 정보가 필요한 이유를 파악하시기 바랍니다.
Lighthouse 사용
브라우저에 적합한 코드를 작성하고 싶지만 어디서부터 시작해야 할지 모르겠다면 Lighthouse를 사용하세요. 이 도구는 웹사이트를 감사하고 올바르게 작동하는 부분과 개선이 필요한 부분에 관한 보고서를 제공합니다. 감사 목록을 검토하면 브라우저에서 중요하게 생각하는 사항을 파악할 수도 있습니다.
실적 측정 방법 알아보기
성능 조정은 사이트마다 다를 수 있으므로 사이트의 성능을 측정하고 사이트에 가장 적합한 조치를 결정하는 것이 중요합니다. Chrome DevTools팀에는 사이트 성능을 측정하는 방법에 관한 튜토리얼이 거의 없습니다.
사이트에 기능 정책 추가
한 단계 더 나아가려면 기능 정책을 사용하세요. 이 새로운 웹 플랫폼 기능은 프로젝트를 빌드할 때 가이드 역할을 할 수 있습니다. 기능 정책을 사용 설정하면 앱의 특정 동작이 보장되고 실수를 방지할 수 있습니다.
예를 들어 앱이 파싱을 차단하지 않도록 하려면 동기식 스크립트 정책에서 앱을 실행하면 됩니다. sync-script: 'none'
가 사용 설정되면 파서 차단 JavaScript가 실행되지 않습니다. 이렇게 하면 코드가 파서를 차단하지 않으며 브라우저는 파서 일시중지를 걱정할 필요가 없습니다.
마무리
![감사합니다](https://developer.chrome.com/static/blog/inside-browser-part4/image/thank-you-cae9a20f3c237.png?authuser=8&hl=ko)
웹사이트를 만들기 시작했을 때는 코드를 작성하는 방법과 생산성을 높이는 데 도움이 되는 방법에만 관심을 두었습니다. 이러한 사항도 중요하지만 브라우저가 작성한 코드를 사용하는 방법도 고려해야 합니다. 최신 브라우저는 사용자에게 더 나은 웹 환경을 제공하기 위한 방법에 지속적으로 투자해 왔습니다. 코드를 정리하여 브라우저에 친절하게 대하면 사용자 환경이 개선됩니다. 브라우저를 잘 지원하기 위한 여정에 함께해 주시기 바랍니다.
Alex Russell, Paul Irish, Meggin Kearney, Eric Bidelman, Mathias Bynens, Addy Osmani, Kinuko Yasuda, Nasko Oskov, 찰리 레이스 등 이 시리즈의 초안을 검토해 주신 모든 분들께 감사드립니다.
이 시리즈가 마음에 드셨나요? 향후 게시물에 관해 궁금한 점이나 제안사항이 있으면 아래 댓글 섹션이나 트위터의 @kosamari를 통해 알려주세요.