본문 바로가기
Web

크롬 브라우저는 어떻게 작동 할까? - 04

by Vintz 2021. 8. 8.
반응형
Google Developers 사이트에서 읽은 내용을 정리한 글입니다.

컴포지터에 입력이 들어오고 있어요!

저번 글에선 렌더링 프로세스와 컴포지터에 대해 알아봤다. 이번 글에선 컴포지터가 어떻게 사용자 입력(input) 시 매끄럽게(smooth) 상호작용을 하게 만드는지 알아보자.

브라우저 관점에서의 입력 이벤트(input events)

일반적으로 우리가 "입력 이벤트(input events)"라고 하면 텍스트 박스에 타이핑을 하거나 마우스 클릭 이벤트를 떠올릴 것이다. 하지만 브라우저 관점에서 의미하는 입력은 사용자가 하는 모든 행동(gesture)을 뜻한다. 즉, 마우스 휠 스크롤도 입력 이벤트이며 터치 또는 마우스 오버(mouse over) 또한 입력 이벤트인 것이다.

 

화면에 터치와 같은 사용자 행동이 발생할 때 브라우저 프로세스가 처음으로 행동을 수신(receives)하게 된다. 하지만 브라우저 프로세스는 이 행동이 발생한 위치만 알고 있으며 탭 내부의 콘텐츠는 렌더러 프로세스에 의해 처리가 된다. 따라서 브라우저 프로세스에 touchstart와 같은 이벤트 타입과 해당 이벤트의 좌표(coordinates)를렌더러 프로세스에 보낸다. 그 후 렌더러 프로세스는 이벤트 타겟을 찾고 해당하는 이벤트 리스너를 실행하여 알맞게 처리한다.

브라우저 프로세스를 통해 렌더러 프로세스로 라우팅된 입력 이벤트 - Google Developers

입력 이벤트를 수신하는 컴포지터

저번 글에서 우리는 컴포지터가 어떻게 컴포지팅으로 래스터된 레이어들이 매끄럽게 스크롤을 처리할 수 있는지 알아보았다. 만약 페이지에 입력에 대한 이벤트 리스너가 연결되어 있지 않을 경우 컴포지터 스레드는 메인 스레드와 완전히 독립된 새로운 컴포지트 프레임을 생성할 수 있다. 하지만 페이지에 이미 어떤 이벤트 리스너가 연결되어 있다면 어떨까? 어떻게 컴포지터 스레드가 필요한 이벤트를 찾아내고 처리할 수 있었을까?

고속 스크롤 불가 영역(Non-Fast Scrollable Region) 이해하기

자바스크립트의 실행은 메인 스레드의 작업이므로, 페이지가 컴포지트가 되었을 때 컴포지터 스레드는 이벤트 핸들러가 연결된 영역을 '고속 스크롤 불가 영역(Non-Fast Scrollable Region)'으로 표시(marks)한다. 이 정보를 기준으로 해당 영역에 이벤트가 발생하게 되면 컴포지터 스레드가 메인 스레드로 입력 이벤트를 보낼지를 알 수 있다.

 

만약 해당 영역 밖에서 이벤트가 들어오게 되면 컴포지터 스레드는 메인 스레드 개입 없이 새로운 컴포지팅 프레임을 생성한다.

고속 스크롤 불가 영역에 대한 입력 이벤트 설명 - Google Developers

이벤트 핸들러 사용 시 주의하기

웹 개발에서 일반적인 이벤트 핸들링 패턴은 이벤트 위임(event delegation)이다. 이벤트 버블링으로 인해 하나의 이벤트 핸들러를 최상위의 요소에 연결하고 이벤트 타겟을 기준으로 처리를 위임 할 수 있다. 다음 코드를 보자.

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

하나의 이벤트 핸들러로 모든 요소에 적용하는 이런 이벤트 위임 패턴이 편해보일 것이다. 하지만 브라우저의 관점으로 보면 웹 페이지 전체가 고속 스크롤 불가 영역으로 표시된다. 이것은 앱이 신경 쓰지 않는 페이지의 부분에 입력이 들어와도 컴포지터 스레드는 들어오는 입력 이벤트마다 메인 스레드와 통신하고 처리가 끝날 때까지 기다린다는 뜻이다. 따라서 컴포지터의 매끄러운 스크롤링 기능이 저하 된다.

전체 페이지가 고속 스크롤 불가 영역일 때 - Google Developers

이 문제를 완화하기 위해, passive: true 옵션을 이벤트 리스너에 사용할 수 있다. 이 옵션은 브라우저에게 여전히 메인 스레드에 이벤트를 받지만(listen), 컴포지터는 메인 스레드를 기다리지 않고 새로운 프레임을 만들어도 된다는 힌트를 준다.

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

이벤트 취소가 가능한지 확인하기

페이지 일부가 가로 스크롤로 고정된(fixed) 웹 페이지 - Google Developers

페이지에 스크롤 방향을 가로 스크롤만 되도록 제한하려는 상자가 있다고 가정해보자.

 

포인터 이벤트에서 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});

또는 이벤트 핸들러를 완전히 없애고 CSS 규칙인 touch-action과 같은 속성을 쓸 수도 있다.

#area {
  touch-action: pan-x;
}

이벤트 타겟 찾기

x, y 점에 뭐가 그려져 있는지 확인하기 위해 페인트 기록을 보는 메인 스레드 - Google Developers

컴포지터 스레드가 입력 이벤트를 메인 스레드에 보낼 때, 가장 처음으로 하는 일은 이벤트 타겟을 찾기 위한 히트 테스트(hit test) 실행(run)이다. 이벤트가 발생한 점 좌표 아래 무엇이 있는지 확인하기 위해 히트 테스트는 렌더링 프로세스에서 생성된 페인트 기록 데이터를 사용한다.

메인 스레드에 이벤트 전송하는 것을 최소화하기

저번 글에서 일반적인 화면(display)은 초당 60번의 새로고침(refreshes)을 한다는 것과 애니메이션을 그 속도에 맞춰야 매끄럽게 보여진다는 것을 알게 되었다. 입력의 경우, 일반적인 터치 스크린 장치는 초당 60-120번의 터치 이벤트를 전달하며 마우스는 초당 100번의 이벤트 전달이 일반적이다. 입력 이벤트의 전달 속도는 이러한 화면 새로고침 속도보다 더 빠르다.

 

touchmove(스크린에 손가락이 닿은 채로 움직일 때 주기적으로 발생)와 같이 연속적인 이벤트가 초당 120회씩 메인 스레드로 보내지면 화면이 새로고침 되는 속도보다 훨씬 많이 히트 테스트를 하거나 자바스크립트를 실행할 수도 있다.

버벅거림(페이지 정크)의 원인이 되는 넘쳐나는 이벤트의 프레임 타임라인 - Google Developers

과도한 메인 스레드의 호출을 최소화하기 위해, 크롬은 연속적인 이벤트들(wheel, mousewheel, mousemove, pointermove, touchmove와 같은 이벤트)을 합치고 다음 requestAnimationFrame 메서드 실행 직전까지 전송(dispatching)을 지연시킨다.

이전과 같은 타임라인이지만 합쳐지고 지연된 이벤트 - Google Developers

keydown, keyup, mouseup, mousedown, touchstart 그리고 touchend와 같은 비연속적인(discrete) 이벤트는 즉시 전송된다.

프레임 내 이벤트들을 얻기 위한 getCoalescedEvents 사용하기

대부분의 웹 앱에서는 합쳐진 이벤트들은 충분한 사용자 경험을 제공한다. 그러나 만약 드로잉 앱과 같은 touchmove 좌표에 기반한 경로를 설정할 때 매끄러운 선을 그리기 위한 경로 사이사이에 좌표들이 손실될 수 있다. 이 경우엔 포인터 이벤트의 getCoalescedEvents 메서드 사용으로 합쳐진 이벤트들의 정보를 얻을 수 있다.

매끄러운 터치 제스처 경로인 왼쪽, 합쳐져 제한된 경로인 오른쪽 - Google Developers

window.addEventListener('pointermove', event => {
    // 합쳐진 이벤트의 정보 얻기
    const events = event.getCoalescedEvents();
    for (let event of events) {
        const x = event.pageX;
        const y = event.pageY;
        // x와 y 좌표를 사용하여 선을 그린다. (draw a line using x and y coordinates.)
    }
});

마무리

드디어 4개의 파트로 이뤄진 글을 마무리하게 되었다. 사실 정리라고도 뭐하지만 두 번, 세 번 읽어보고 나름 정리하며 직접 글로 쓰니 공부가 되었다. 중간중간 '나의 현재 프로그래밍 지식과 위치에서 이렇게까지 깊게 공부하는게 맞는걸까?'라는 생각도 하고 포기하고 싶기도 했지만 분명 이 지식들이 도움이 될거라 확신한다. 이 글에서 쓰여진 코드들은 사실 본 적도 없고 자주 사용하는 코드는 아닌 것 같다.(내가 모르는 걸수도..) 이벤트 위임 같은 경우도 얼리 리턴을 통해 성능을 개선하는 것 같은데 passive: true 옵션이 있는 줄도 몰랐다. 모처럼 브라우저에 대해 깊게 파본 것 같은데 재미도 있었고 나름 뿌듯하다.

 

마지막 파트는 구글 개발자 사이트에선 번역이 아직 되지 않았다. 파파고의 도움을 받고 나름 해석해서 썼지만 막히는 부분을 네이버 D2 사이트에서 해결했다. 이걸 마지막에 알게 되다니..정말 친절하게 설명이 되어 있고 이걸 먼저 봤다면 내가 이 시리즈를 썼을까? 싶기도 하다.(아니면 더 정리를 잘했을 수도?) 글을 쓰고 보다보니 날 한번 잡아서 하나의 글로 총정리를 해보고 싶다.

반응형