웹 개발에서 렌더링이란 HTML, CSS, 자바스크립트 등 개발자가 작성한 문서가 브라우저에 출력되는 과정을 의미한다. 브라우저는 종류마다 서로 다른 렌더링 엔진을 가지고 있다.
브라우저의 주요 역할은 두가지이다.
위의 그림은 크롬의 멀티 프로세스 구조를 나타낸 것이다. 각 프로세스가 어떤 역할을 하는지 알아보자.
Process | What it controls |
---|---|
Browser | Controls 'chrome' part of the application including address bar, bookmarks, and back and forward buttons. |
Renderer | Controls anything inside of the tab where a website is displayed. |
Plugin | Controls any plugins used by the website. |
GPU | Handle GPU tasks in isolation from other processes. |
💥 Chrome이 멀티 프로세스 구조를 사용하는 이유
크롬은 브라우저 탭마다 하나의 프로세스를 부여한다. 만약 여러 탭이 하나의 렌더러 프로세스로 돌아간다면 어떻게 될까?
우리는 하나의 탭이 unresponsive
, 즉 무응답 상태로 변하면, 해당 탭을 닫고 살아있는 다른 탭으로 이동하여 작업을 계속한다. 하지만 모든 탭들이 하나의 프로세스 위에서 돌아간다면, 모든 탭들이 무응답 상태가 된다. 따라서 크롬은 탭별로 프로세스를 부여하여 위 현상을 방지한다.
💥 Site Isolation
Site Isolation은 악성 웹 페이지에서 다른 웹 사이트의 데이터를 쉽게 탈취할 수 없도록 만들어진 Protection으로, 비교적 최근에 도입된 기능이다.
iframe으로 하위 페이지를 부르는 경우를 생각해보자. 이전에는 부모 페이지와 자식 iframe 페이지가 동일 프로세스 상에 존재했다. 서로 같은 프로세스이기 때문에 메모리 공간을 공유하게 되고, 이는 다른 페이지에서 메모리에 접근하여 데이터를 읽을 수 있다는 취약점으로 이어진다. 현재는 iframe에 별도의 렌더러 프로세스를 부여하고 있다.
이제 간단한 웹 브라우징 사례를 살펴보자. 사용자가 URL을 타이핑하면, 브라우저가 인터넷에서 데이터를 받아와 페이지를 출력한다. 여기서는 사용자가 요청을 보내고 브라우저가 페이지를 렌더링 하기 위해 준비하는 과정만 다룰 것이다.
브라우저 프로세스는 탭 외부에서 일어나는 모든 것을 담당한다. 브라우저 프로세스는 UI 스레드, 네트워크 스레드, 스토리지 스레드를 포함한다.
사용자가 주소창에 타이핑 하면, 가장 먼저 UI 스레드가 사용자 입력값이 쿼리인지 URL인지 분석한다.
사용자가 엔터를 치면, UI 스레드는 웹 사이트에서 내용을 받아오기 위해 network call을 생성한다. 로딩 스피너가 탭에 표시되고, 네트워크 스레드가 DNS를 조회하고 TLS 연결을 생성하는 등 적절한 프로토콜을 거치게 된다.
만약 리다이렉트 의미를 가진 300번대의 HTTP 상태코드를 받게 되면, 네트워크 스레드는 UI 스레드에게 서버가 리다이렉트를 요청했다고 전달한다.
네트워크 스레드는 응답을 받으면 필요시에 스트림의 첫 몇바이트를 확인한다. 응답이 HTML 파일인 경우, 다음 단계에서 렌더러 프로세스가 데이터를 받아 작업을 이어나간다.
응답이 압축 파일이거나 다른 파일인 경우, 다운로드 매니저에게 넘겨 필요한 데어터를 다운로드 한다.
네트워크 스레드는 응답에 대한 검사가 끝나면 UI 스레드에게 데이터가 준비 되었다고 전한다. 그러면 UI 스레드는 렌더러 프로세스를 찾는다.
현재 단계에서 렌더러 프로세스를 찾는 것은 비효율적이다. UI 스레드가 네트워크 스레드에게 URL 요청을 전달할 때, UI 스레드는 이미 어떤 사이트를 요청했는지 알고 있다. 따라서 UI 스레드는 네트워크 요청과 동시에 렌더러 프로세스를 찾을 수 있다. 네트워크 스레드가 데이터를 수신했을 때, 이미 렌더러 프로세스는 대기 상태에 있다.
이제 데이터와 렌더러 프로세스가 모두 준비되었으므로, 브라우저 프로세스에서 렌더러 프로세스로 commit을 하기 위한 IPC가 전송된다. 이후 브라우저 프로세스에서 렌더러 프로세스가 commit 되었다는 확인을 받으면 문서가 로딩된다.
해당 단계에서 주소창 갱신되고 보안 표시 및 사이트 설정 UI에 새로운 페이지의 정보가 반영된다. 이외에도 세션 히스토리가 갱신되는 등 다양한 작업이 이루어진다.
이번엔 렌더러 과정을 살펴보자. 렌더러 프로세스는 탭 내부에서 일어나는 모든 것(=web contents)을 담당한다.
렌더러의 메인 스레드는 대부분의 코드를 담당한다. 만약 웹 워커나 서비스 워커를 사용하고 있다면 이들이 코드의 일부를 나누어 담당한다. Compositor와 Raster 스레드는 페이지를 효과적이고 매끄럽게 렌더링 하도록 도와주는 역할이다.
렌더러 프로세스의 주요 업무는 HTML, CSS, 자바스크립트를 웹 페이지로 변환하는 것이다.
렌더러 프로세스가 navigation을 위한 commit 메시지를 받고, HTML 데이터를 받기 시작하면, 메인 스레드는 HTML을 DOM으로 변환한다.
DOM : (Document Object Model) 문서의 구조화된 표현을 제공하며 프로그래밍 언어가 DOM 구조에 접근할 수 있는 방법을 제공하여 문서 구조/스타일/내용 등을 변경할 수 있도록 도와준다.
웹 사이트는 주로 HTML을 단독으로 사용하지 않고, 이미지, CSS, 자바스크립트와 같은 외부 자원을 함께 사용한다. 메인 스레드는 파싱하면서 필요한 외부 자원을 하나 하나 호출할 수 있다. 하지만 빠르게 작업을 수행하기 위해 preload scanner를 동시에 실행한다.
preload scanner는
<img>
나<link>
등을 발견하면 브라우저 프로세스의 네트워크 스레드에게 요청을 보낸다.
💥 자바스크립트는 파싱을 block 한다
HTML 파서가 <script>
태그를 만나면, HTML 파싱을 멈추고 자바스크립트 코드를 로드하고 파싱하여 실행해야 한다. 이는 자바스크립트가 document.write()
처럼 문서를 변경할 수 있기 때문이다.
앞에서도 말했듯이 웹 사이트는 일반적으로 HTML을 단독으로 사용하지 않는다. 웹 사이트를 그리기 위해서는 CSS의 스타일이 필요하다. 메인 스레드는 CSS를 파싱하고, 각 DOM 노드의 스타일을 결정한다.
이제 렌더러 프로세스는 각 노드의 문서 구조(= DOM 트리)와 스타일을 알고 있다. 하지만 이것만으로는 페이지를 렌더링할 수 없다.
"빨간색 원과 파란색 사각형을 그려라."
해당 문장만으로는 주어진 도형들을 어느 위치에 얼만큼 크게 그려야 하는지 알 수 없다.
레이아웃은 요소들의 기하학적인 위치를 찾는 과정이다. 메인 스레드는 DOM과 computed styles를 살펴보고 xy 좌표와 상자 크기와 같은 정보를 포함한 레이아웃 트리(=CSSOM 트리)를 만든다. 이는 DOM tree와 유사하지만 스타일 정보를 담고 있다는 점에서 차이가 있다.
이제 요소의 크기, 모양, 위치를 알고 있으므로 색상만 결정해주면 된다. 메인 스레드는 색상을 칠하기 위해 레이아웃 트리를 검토한다.
지금까지 브라우저가 문서 구조, 각 요소의 스타일, 기하학적 위치, 페인트 순서를 파악하는 과정을 살펴보았다. 이제 실제로 페이지가 그려지는 과정을 살펴보자. 앞서 등장한 Compositing, Raster 스레드가 여기서 활용된다.
렌더링 과정에서 얻은 정보를 픽셀로 전환하는 과정을 래스터화라고 한다. 가장 간단하게 래스터하는 방법은 뷰포트 내부 부분을 래스터하는 것이다. 사용자가 페이지를 스크롤하면 래스터된 프레임을 이동하고 새롭게 추가된 부분을 래스터화한다.
🔗 초기 래스터 방식 확인하기
위 방법은 크롬이 처음 출시되었을 때 사용한 방식이다. 현재는 보다 정교한 합성 프로세스를 실행한다.
Compositing
합성은 페이지의 일부를 레이어로 분리하고 개별적으로 래스터화하여 다음 compositor 스레드라 불리는 별도의 스레드에서 페이지로 합성하는 과정을 의미한다. 스크롤이 발생하면 이미 레이어들이 래스터화 되어 있기 때문에 새 프레임을 합성하기만 하면 된다.
🔗 합성을 통한 래스터 방식 확인하기
💥 레이어 나누기
메인 스레드는 어떤 요소가 어떤 레이어에 있는지 알아내기 위해 레이아웃 트리를 돌아다니며 레이어 트리를 만든다.
💥 래스터와 합성
레이어 트리가 생성되면 메인 스레드는 해당 정보를 Compositor 스레드에 전달한다. Compositor 스레드는 각 레이어를 래스터 하고, 레이어를 타일로 나누어 각 타일을 raster 스레드로 전송한다. Raster 스레드는 각 타일을 래스터하여 GPU 메모리에 저장한다.
Compositor 스레드는 raster 스레드의 우선순위를 지정하여 뷰포트 내의 항목을 먼저 래스터할 수 있다.
compositor 스레드는 타일이 래스터되고 나면 draw quads라는 타일 정보를 수집하여 compositor frame을 생성한다.
compositor frame을 성공적으로 생성하고 나면 IPC를 통해 브라우저 프로세스에 제출된다. compositor frame은 GPU로 전송되어 화면에 표시되고, 스크롤 이벤트가 발생하면 다른 compositor frame을 생성하고 GPU로 전송한다.
합성은 메인 스레드를 거치지 않고 수행된다. 즉, 합성 스레드는 스타일 계산이나 자바스크립트 실행을 기다릴 필요가 없다. 하지만 레이아웃이나 페인트를 다시 계산해야 하는 경우에는 메인 스레드가 관여해야 한다.
참고자료
Inside look at modern web browser #1
Inside look at modern web browser #2
Inside look at modern web browser #3
상세하게 정말 잘 적어주셨네요. 좋은 글 잘 보고 갑니다 :)