최신 웹브라우저 자세히 살펴보기 (4부)

Mariko Kosaka

컴포저이터에 입력이 들어옴

이 블로그는 Chrome 내부를 살펴보는 4부작 시리즈의 마지막으로, Chrome이 웹사이트를 표시하기 위해 코드를 처리하는 방식을 살펴봅니다. 이전 게시물에서는 렌더링 프로세스를 살펴보고 컴포저에 관해 알아봤습니다. 이 게시물에서는 사용자 입력이 들어올 때 컴포지터가 원활한 상호작용을 지원하는 방법을 살펴봅니다.

브라우저 관점에서의 입력 이벤트

'입력 이벤트'라고 하면 텍스트 상자에 입력하거나 마우스를 클릭하는 것만 생각할 수 있지만 브라우저의 관점에서 입력은 사용자의 모든 동작을 의미합니다. 마우스 휠 스크롤은 입력 이벤트이고 터치 또는 마우스오버 역시 입력 이벤트입니다.

화면에서 터치와 같은 사용자 동작이 발생하면 브라우저 프로세스가 처음에 동작을 수신합니다. 그러나 탭 내부의 콘텐츠는 렌더러 프로세스에서 처리하므로 브라우저 프로세스는 해당 동작이 발생한 위치만 인식합니다. 따라서 브라우저 프로세스는 이벤트 유형 (예: touchstart)과 좌표를 렌더러 프로세스로 전송합니다. 렌더러 프로세스는 이벤트 타겟을 찾고 연결된 이벤트 리스너를 실행하여 이벤트를 적절하게 처리합니다.

입력 이벤트
그림 1: 브라우저 프로세스를 통해 렌더기 프로세스로 라우팅되는 입력 이벤트

컴포저블이 입력 이벤트 수신

그림 2: 페이지 레이어 위로 마우스를 가져가면 표시되는 뷰포트

이전 게시물에서는 래스터라이즈된 레이어를 컴포지션하여 스크롤을 원활하게 처리하는 방법을 살펴봤습니다. 입력 이벤트 리스너가 페이지에 연결되어 있지 않으면 컴포저 스레드는 기본 스레드와 완전히 독립된 새 컴포지트 프레임을 만들 수 있습니다. 하지만 일부 이벤트 리스너가 페이지에 연결된 경우는 어떻게 해야 하나요? 컴포저 스레드는 이벤트를 처리해야 하는지 어떻게 알 수 있나요?

빠르게 스크롤할 수 없는 영역 이해

JavaScript 실행이 기본 스레드의 작업이므로 페이지가 합성될 때 컴포지터 스레드는 이벤트 핸들러가 'Non-Fast Scrollable Region'으로 연결된 페이지의 영역을 표시합니다. 이 정보를 통해 컴포저 스레드는 이벤트가 해당 영역에서 발생하면 입력 이벤트를 기본 스레드로 전송할 수 있습니다. 입력 이벤트가 이 영역 외부에서 발생하면 컴포저 스레드는 메인 스레드를 기다리지 않고 새 프레임 컴포지션을 계속 진행합니다.

빠른 스크롤할 수 없는 제한된 영역
그림 3: 빠르게 스크롤할 수 없는 영역에 대한 설명된 입력의 다이어그램

이벤트 핸들러 작성 시 주의해야 할 사항

웹 개발의 일반적인 이벤트 처리 패턴은 이벤트 위임입니다. 이벤트가 확장되므로 최상위 요소에 이벤트 핸들러를 하나 연결하고 이벤트 타겟을 기반으로 작업을 위임할 수 있습니다. 아래와 같은 코드를 보거나 작성해 보셨을 수 있습니다.

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault();
    }
});

모든 요소에 이벤트 핸들러를 하나만 작성하면 되므로 이 이벤트 위임 패턴의 인체공학적 특성이 매력적입니다. 그러나 브라우저의 관점에서 이 코드를 보면 이제 전체 페이지가 빠르게 스크롤할 수 없는 영역으로 표시됩니다. 즉, 애플리케이션이 페이지의 특정 부분으로부터의 입력에 관심이 없더라도 컴포지터 스레드는 기본 스레드와 통신하고 입력 이벤트가 수신될 때마다 기본 스레드를 기다려야 합니다. 따라서 컴포지터의 부드러운 스크롤 기능이 작동하지 않습니다.

전체 페이지에서 빠르게 스크롤할 수 없는 영역
그림 4: 전체 페이지를 포괄하는 빠른 스크롤할 수 없는 영역에 관해 설명된 입력 다이어그램

이를 방지하려면 이벤트 리스너에서 passive: true 옵션을 전달하면 됩니다. 이렇게 하면 브라우저에 기본 스레드에서 계속 이벤트를 수신 대기하고 싶지만 컴포저는 새 프레임을 계속 컴포지팅할 수 있다는 힌트를 줍니다.

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault()
    }
 }, {passive: true});

이벤트를 취소할 수 있는지 확인

페이지 스크롤
그림 5: 페이지의 일부가 수평 스크롤로 고정된 웹페이지

스크롤 방향을 가로 스크롤만으로 제한하려는 상자가 페이지에 있다고 가정해 보겠습니다.

포인터 이벤트에서 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
x.y 지점에 무엇이 그려졌는지 묻는 페인트 레코드를 보고 있는 기본 스레드 그림

컴포저 스레드가 입력 이벤트를 기본 스레드로 전송하면 가장 먼저 실행되는 것은 이벤트 타겟을 찾기 위한 히트 테스트입니다. 히트 테스트는 렌더링 프로세스에서 생성된 페인트 기록 데이터를 사용하여 이벤트가 발생한 점 좌표 아래에 무엇이 있는지 찾습니다.

기본 스레드로의 이벤트 전달 최소화

이전 게시물에서는 일반적인 디스플레이가 초당 60회 화면을 새로 고치는 방식과 매끄러운 애니메이션을 위해 이 주기를 어떻게 유지해야 하는지에 대해 다루었습니다. 입력의 경우 일반적인 터치 스크린 기기는 초당 60~120회의 터치 이벤트를 전송하고 일반적인 마우스는 초당 100회의 이벤트를 전송합니다. 입력 이벤트의 충실도가 화면이 새로고침할 수 있는 것보다 높습니다.

touchmove와 같은 연속 이벤트가 초당 120번 기본 스레드로 전송되면 화면이 새로고침될 수 있는 속도에 비해 과도한 수의 히트 테스트와 JavaScript 실행이 트리거될 수 있습니다.

필터링되지 않은 이벤트
그림 7: 프레임 타임라인을 플러딩하여 페이지 버벅거림을 유발하는 이벤트

기본 스레드에 대한 과도한 호출을 최소화하기 위해 Chrome은 연속 이벤트 (예: wheel, mousewheel, mousemove, pointermove, touchmove)를 병합하고 다음 requestAnimationFrame 직전까지 전달을 지연합니다.

병합된 이벤트
그림 8: 이전과 동일한 타임라인이지만 이벤트가 병합 및 지연됨

keydown, keyup, mouseup, mousedown, touchstart, touchend와 같은 개별 이벤트는 즉시 전달됩니다.

getCoalescedEvents를 사용하여 프레임 내 이벤트 가져오기

대부분의 웹 애플리케이션의 경우 병합된 이벤트만으로도 우수한 사용자 환경을 제공하기에 충분합니다. 그러나 그리기 애플리케이션을 빌드하고 touchmove 좌표를 기반으로 경로를 배치하는 경우 부드러운 선을 그리기 위해 중간 좌표가 손실될 수 있습니다. 이 경우 포인터 이벤트에서 getCoalescedEvents 메서드를 사용하여 병합된 이벤트에 관한 정보를 가져올 수 있습니다.

getCoalescedEvents
그림 9: 왼쪽의 부드러운 터치 동작 경로, 오른쪽의 제한된 병합 경로
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가 실행되지 않습니다. 이렇게 하면 어떤 코드도 파서를 차단하지 않으며 브라우저는 파서 일시중지에 관해 걱정할 필요가 없습니다.

마무리

감사합니다

웹사이트를 만들기 시작했을 때는 코드를 작성하는 방법과 생산성을 높이는 데 도움이 되는 방법에만 관심을 두었습니다. 이러한 사항도 중요하지만 브라우저가 작성한 코드를 사용하는 방법도 고려해야 합니다. 최신 브라우저는 사용자에게 더 나은 웹 환경을 제공하기 위해 끊임없이 투자해 왔습니다. 코드를 정리하여 브라우저에 친절하게 대하면 사용자 환경이 개선됩니다. 브라우저를 원활하게 지원하는 데 동참해 주시기 바랍니다.

Alex Russell, Paul Irish, Meggin Kearney, Eric Bidelman, Mathias Bynens, Addy Osmani, Kinuko Yasuda, Nasko Oskov, 찰리 레이스 등 이 시리즈의 초안을 검토해 주신 모든 분들께 감사드립니다.

이 시리즈가 마음에 드셨나요? 향후 게시물에 관한 질문이나 제안이 있으면 아래 댓글 섹션을 통해 알려주시거나 트위터의 @kosamari를 통해 알려주시기 바랍니다.