브라우저에서 페이지를 렌더링하는 방식

프론트 개발자가 꼭 알아야하는 내용중 하나이다. 주기적으로 여러번 봐서 숙지하고 있자.

웹 개발을 처음 할 때 열심히 외웠던 페이지가 만들어지는 과정. 아무런 경험 없이 무작정 외우다 보니 휘발성 기억이 돼버렸다. 지금도 물론 배워나가는 단계지만, 이제는 암기가 아닌 이해를 해보려고 이 글을 작성한다. 다시 한번 보자.

참고로 사실 웹 브라우저가 어떻게 동작해야 한다는 표준은 없다. 이 글은 크롬을 기반으로 설명하며, 모든 브라우저에 대응되는 글은 아님을 꼭 인식하자.

렌더러 프로세스(Renderer Process)

렌더러 프로세스는 브라우저 탭 안에서 일어나는 모든 일들을 담당한다. 렌더러 프로세스 안에서는 메인 스레드가 우리가 구현한 대부분의 코드를 처리하게 된다. 렌더러 프로세스의 핵심 역할은 HTML, CSS, JS를 사용자가 인터렉션 할 수 있는 웹 페이지로 만드는 것이다.

DOM 생성

사용자의 브라우저는 웹 서버에서 HTML을 받아 파싱하며, 이를 **Document Object Model(DOM)**으로 변환한다. DOM이란 페이지에 대한 브라우저의 내부 표현일 뿐만 아니라 웹 개발자가 자바스크립트를 통해 상호 작용할 수 있는 데이터 구조 및 API이다.

이 과정은 바이트를 문자로, 문자열을 <body>와 같은 토큰으로, 토큰을 프로퍼티와 규칙을 가진 객체로 만들고 최종적으로 이 객체들을 서로 연결하면서 데이터 구조를 만드는 순서로 진행된다. 여기까지의 과정이 아래 그림의 1번이라고 할 수 있다.

참고로 HTML 문서를 DOM으로 파싱하는 방법은 HTML 표준에 정의되어 있으며, 혹시 궁금하다면 맨 아래 참고자료를 보자.

브라우저가 HTML을 파싱하는 과정에서 스타일시트를 만나면, 브라우저는 모든 작업을 일시 중지하고 서버에 파일을 요청한다. 브라우저가 파일을 받으면 앞에서 한 작업과 유사한 작업을 반복한다. 그것이 CSS의 객체 모델인 CSSOM(CSS Object Model)을 만드는 2번 과정이다.

또한 브라우저가 HTML을 파싱하는 과정에서 자바스크립트(<script> tag)를 만나면, 브라우저는 모든 작업을 일시 중지하고 자바스크립트 코드를 로드, 파싱, 그리고 실행한다. 왜냐하면 자바스크립트는 전체 DOM 구조를 바꾸는 document.write()와 같은 방법으로 문서의 구성을 바꿀 수 있기 때문이다. 이것이 HTML 파서가 HTML 문서를 다시 파싱 하기 전에 자바스크립트를 기다려야만 하는 이유이다.

Q) 리소스 로드에 대한 최적화는 어떻게 해야 할까? A) 중요 렌더링 경로(critical rendering path) 최적화를 진행한다. 렌더-블로킹 문제를 최대한 없애는 것으로 시작한다. 1. 자바스크립트 코드를 <body> 맨 아래 위치 시킨다. 2. document.write()를 사용하지 않는다면, async나 defer attribute를 <script> 태그에 추가한다. defer 속성의 경우 백그라운드에서 스크립트를 다운로드하며, 다운로드 도중에 HTML 파싱이 멈추지 않는다. 외부 스크립트에만 유효하며, src가 없다면 defer 속성은 무시되게 된다. defer 속성이 있는 스크립트가 실행되는 시점은 DOM이 준비되고 DOMContentLoaded 이벤트 발생 전에 실행된다. 이와 비슷하게 async 속성도 백그라운드에서 HTML 파싱이 멈추지 않고 스크립트를 다운로드 하지만, 실행 순서를 예측할 수 없으며, 실행되는 순간에는 HTML 파싱이 멈추게 된다. load-first order라고 하여 먼저 실행되는 스크립트부터 실행되며, 광고 관련 스크립트 같은 독립적인 서드 파티 스크립트에 유용하다. 다만 의존성 문제를 일으킬 수 있다는 점을 주의하자. 3. <head>에서 CSS를 로딩한다. 4. 미디어 타입과 미디어 쿼리를 통해 CSS로 인한 렌더링을 멈추는 일을 조건화 한다. ex) <link href="big-screen.css" rel="stylesheet" media="(min-width: 1980px)"> 5. 타사 스크립트를 최대한 제거하고, webpack 같은 번들러를 이용하여 스크립트와 리소스를 번들 화하면서 서버에 대한 요청을 최대한 줄인다.

스타일 계산

DOM을 생성한 것만으로 페이지가 어떻게 보이는지 알기에는 부족하다. CSS로 페이지 요소들에 대한 스타일을 정의할 수 있기 때문이다. 그래서 메인 스레드는 CSS를 파싱 하여 각 DOM 노드에 대한 계산된 스타일(Computed Style)을 결정한다. 이는 CSS selector를 기반하여 각 요소들에 어떤 스타일이 적용되었는지에 대한 정보다.

Q) CSS를 전혀 사용하지 않았다면, 스타일 계산 과정이 빠질까? A) 모든 브라우저는 "사용자 에이전트 스타일"이라고하는 기본 스타일 세트를 제공한다. 여기에 대해 더 알아본다면, "<head>, <meta>, <title> 태그 등은 왜 화면에 안 나올까?"라는 이런 궁금증에 대한 답도 얻을 수 있다. Q) 복합 selector 최적화란 어떻게 하는 것일까? A) CSS Selector를 단순화하고 최적화해야 한다. 그 전에 간단한 개념부터 설명하자면, 브라우저는 CSS selector를 오른쪽에서 왼쪽으로 읽으며, #id. class > ul a 같은 복합 셀렉터에서 맨 오른쪽 셀렉터를 "키 셀렉터"라고 한다. 브라우저는 복합 셀렉터를 파싱 할 때, 모든 키 셀렉터를 찾고, 왼쪽 셀렉터를 계속 필터링하며, 맨 왼쪽 셀렉터에 도달할 때까지 이 과정을 반복한다. 따라서 복합 셀렉터 최적화를 위해서는 셀렉터를 최대한 짧게 하며, id나 class를 이용하여 구체적으로 지정한다. 사실 브라우저 최적화가 잘 돼서 이걸로 얻는 성능적 이득이 별로 없다고 생각할지라도, 복합 셀렉터로 구현된 코드를 인수인계받았다고 하면 얼마나 끔찍한가. 유지보수를 생각해서라도 피하는 게 좋다. Q) body에 복합 셀렉터를 이용해 스타일을 변경하는 방법보다 단일 셀렉터를 통한 방식이 왜 더 좋은가? A) 위 질문에서 대답한 유지보수의 용이성도 이유에 포함되지만, body요소의 클래스를 변경하면 모든 하위 요소의 계산된 스타일을 재계산해야 하는 경우가 발생한다(물론 지금의 크롬은 그렇게 처리되지 않는다고 하지만, 일부 브라우저에서는 여전히 발생할 수 있는 문제이다). 따라서 최대한 CSS를 구체적으로 지정하여 범위를 줄여 스타일 무효화를 줄이는 것이 좋으며, 이렇게 하면 전체 노드에 대한 재계산이 트리거 되지 않기 때문에 위의 과정을 반복하지 않아도 된다.

레이아웃(Layout)

이제 렌더러 프로세스는 문서의 구조(DOM)와 각 노드에 대한 스타일(CSSOM)을 알게 되었다. DOM 트리와 CSSOM 트리를 **렌더 트리(Render Tree)**로 결합되어, 표시 요소의 레이아웃을 계산하는데 사용되며, 픽셀을 화면에 렌더링 하는 페인트 프로세스에 대한 입력 역할을 한다. 렌더 트리에는 페이지를 렌더링 하는데 필요한 노드만 포함한다.

렌더 트리가 제자리에 있으면 "레이아웃(Layout)" 단계로 진행할 수 있다. 레이아웃 단계는 리플로우라고도 하며, 계산된 스타일을 디바이스의 뷰포트 내에서 정확한 위치와 크기를 계산하게 된다. 이때 모든 상대 측정은 절대 픽셀로 변환된다. 마지막으로 어떤 노드가 표시되는지, 계산된 스타일 및 지오메트리를 알았다면 최종 단계로 전달할 수 있으며, 렌더링 트리의 각 노드가 화면의 실제 픽셀로 변환되게 되는데 이 단계를 페인팅 또는 레스터화라고 한다.

Q) 레이아웃에 영향을 미치는 스타일에는 무엇이 있을까? A) width, height, padding, margin, display, border-width, border, top, position, font-size, float, text-align, overflow-y, font-weight, overflow, left, font-family, line-height, vertical-align, right, clear, white-space, bottom, min-height Q) 어떤 블로그를 보다보니, cssText를 사용해서 스타일을 업데이트 하라고 했다. 이렇게 하면 reflow가 안일어난다고 하던데 맞는 말인가? A) 반은 맞고 반은 틀리다. 먼저 cssText로 스타일을 업데이트 하는 방법은 여러 스타일을 한번에 업데이트할때 쓰이는 방식이다. 크롬에서는 리플로우가 안일어나지만, IE에서는 일어난다.

****

페인트(Paint)

위 과정에서 요소들의 크기, 모양, 위치까지 알았지만, 아직 어떤 순서로 그릴지 판단하는 과정이 남았다. 예를 들어 z-index를 고려하지 않으면 잘 못된 레더링 이미지가 나올 수 있다. 이 페인트 단계에서는 메인 스레드는 레이아웃 트리를 따라가 페인트 기록을 생성한다.

* 각 단계에서는 그 전 단계 실행 결과물이 새로운 데이터 생성에 쓰이고 있으며, 따라서 렌더링 파이프라인 업데이트 비용이 많이 든다는 점을 알 수 있다. Q) 페인트에 영향을 미치는 스타일은 무엇일까? A) color, visibility, text-decoration, background-position, outline-color, outline-style, outline-width, background-size, border-style, background, background-image, background-repeat, outline, border-radius, box-shadow

컴포지팅(합성, Composite)

이제 브라우저는 문서의 구조, 각 요소의 스타일, 페이지의 기하학 구조, 페인트 순서를 알았으며, 이러한 정보를 스크린의 픽셀로 바꾸는 것을 레스터 라이징이라고 합니다. 처음 크롬이 공개되었을 때 사용한 방법이 레스터 라이징이었지만, 모던 브라우저는 컴 포지팅이라는 방식으로 동작한다.

컴포지팅은 한 페이지의 부분들을 여러 레이어로 나누고 그것들을 각각 레스터 하며 컴포지터 스레드에서 페이지를 합성하는 기술이다. 어떤 요소들이 어떤 레이어에 있어야 하는 지 알기 위해서, 메인 스레드는 레이아웃 트리를 순회하여 **레이어 트리(Layer Tree)**를 생성한다. 그리고 레이어 트리가 생성되고 페인트 순서가 결정되고 나면, 메인 스레드는 컴포지터 스레드에게 커밋한다. 그러면 컴포지터 스레드가 각 레이어를 레스터 라이징 한다.

참고

리페인트(Repaint)

같은 페이지에 있는 요소의 위치에 영향을 주지 않는 요소의 스타일을 변경하는 경우, 브라우저는 새로운 스타일을 적용하여 다시 요소를 그린다.

리플로우(Reflow) or 레이아웃

변경 사항이 문서 내용이나 구조 또는 요소 위치에 영향을 미치면 리플로우(또는 릴레이아웃, relayout)가 발생한다. 리플로우는 아래와 같은 경우에 트리거 된다.

  • DOM 조작(요소 추가, 삭제, 변경 또는 요소 순서 변경)

  • 양식 필드(form fields)의 텍스트 변경을 포함한 내용 변경

  • CSS 속성의 계산 또는 변경

  • 스타일 시트 추가 또는 제거

  • class 속성 변경(크롬에서는 안되지만, IE에서는 리플로우 된다.)

  • 브라우저 창 조작(크기 조정, 스크롤)

  • 의사 클래스 활성화(:hover)

참고 자료

Last updated