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

코사카 마리코

컴포지터로 입력이 전달됨

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

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

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

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

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

컴포지터가 입력 이벤트를 수신함

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

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

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

JavaScript 실행이 기본 스레드의 작업이므로 페이지가 구성될 때 컴포지터 스레드는 이벤트 핸들러가 '빠른 스크롤 가능 지역'으로 연결된 페이지의 영역을 표시합니다. 이 정보를 확보하면 컴포지터 스레드는 이 영역에서 이벤트가 발생할 때 입력 이벤트를 기본 스레드에 전송하도록 할 수 있습니다. 입력 이벤트가 이 영역 외부에서 발생한 경우 컴포지터 스레드는 기본 스레드를 기다리지 않고 새 프레임 합성을 계속 진행합니다.

빠르게 스크롤할 수 없는 제한된 영역
그림 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;
}

이벤트 타겟 찾기

히트 테스트
그림 6: xy점에 무엇이 그려졌는지 묻는 페인트 레코드를 보는 기본 스레드

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

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

이전 게시물에서는 일반적인 디스플레이가 초당 60회 화면 새로고침을 하는 방법과 원활한 애니메이션을 위해 재생 주기를 유지하는 방법을 설명했습니다. 입력의 경우 일반적인 터치스크린 기기는 터치 이벤트를 초당 60~120회 전달하고 일반적인 마우스는 초당 100회 이벤트를 전달합니다. 입력 이벤트의 충실도가 화면을 새로고침할 수 있는 것보다 높습니다.

touchmove와 같은 연속 이벤트가 초당 120회 기본 스레드에 전송된다면 화면 새로고침의 속도에 비해 과도한 조회 테스트와 자바스크립트 실행이 트리거될 수 있습니다.

필터링되지 않은 이벤트
그림 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가 실행되지 않습니다. 이렇게 하면 코드가 파서를 차단하지 않으며 브라우저에서 파서 일시중지에 관해 걱정할 필요가 없습니다.

마무리

정말 고마워

웹사이트를 구축하기 시작했을 때는 코드를 작성하는 방법과 생산성을 높이는 데 도움이 되는 것에만 신경을 썼습니다. 이러한 항목들이 중요하지만, 브라우저가 우리가 작성하는 코드를 어떻게 받아들이는지 생각해 보아야 합니다. 최신 브라우저는 사용자에게 더 나은 웹 환경을 제공하기 위해 계속 투자해 왔습니다. 코드를 정리하여 브라우저에 예의를 갖추면 사용자 환경이 개선됩니다. 브라우저에 예의를 갖추기 위한 퀘스트에 저도 동참해 주시기 바랍니다.

알렉스 러셀, 폴 아일랜드, 메긴 키어니, 에릭 비델만, 마티아스 바인스, 애디 오스마니, 애디 오스마니, 러디 오스마니, 차례스 차례니

이 시리즈가 도움이 되었나요? 향후 게시물에 대한 질문이나 제안이 있으면 아래의 의견 섹션이나 트위터의 @kosamari를 통해 연락해 주세요.