티스토리 뷰
앞에서 우리는 브라우저에 대한 기본적인 지식을 알아보았다. 이번엔 브라우저가 어떠한 방식으로 렌더링을 하는지 알아보자. 사실 초창기 브라우저에서는 기본적인 마크업 문법을 지키지 않으면, HTML을 파싱 하지 못하였다. 예를 들면, body 태그를 안적거나 닫는 태그를 넣지 않으면 파싱자체가 안되었다. 그러나 현재 브라우저(모던 브라우저) 자체의 성능이 좋아지고, 예외처리도 잘 되어 있어서 많은 기초지식이 없더라도 웹 페이지와 application 동작에는 무리가 없이 실행이 된다. 그럼에도 우리는 서비스 사용자가 웹 페이지를 이탈하지 않고 조금 더 좋은 UX를 보여주기 위해서는, 브라우저 렌더링에 대한 지식을 정확히 적용하여 응답 속도를 높이는 과정이 필요하다.
응답 속도를 높이는 방법에는 Infra / backend / frontend와 같이 전체 영역에서 작업이 필요하지만, 사실상 가장 큰 영향을 미치는 부분은 사용자와 가장 가까이서 상호작용을 하는 브라우저라고 할 수 있겠다. 브라우저 응답속도를 높이기 위해, 렌더링에 대해 알아보도록 하자.
Critical Rendering Path
우선 Critical Rendering Path (CRP) 에 대해 알아보자. Critical Rendering Path이란 웹 브라우저 렌더링 과정을 의미 한다. 아래의 그림이 CRP의 과정이라고 할 수 있다.
우리가 기본적으로 HTML, CSS를 서버에서 다운로드하고, DOM과 CSSOM과 같은 Object Model로 변환한다. 각각의 HTML은 DOM 트리를 빌드하고, CSS는 CSSOM 트리를 빌드한다. 그 후에 DOM과 CSSOM를 결합하여 Render Tree를 형성한다. 그 후 Render Tree에서 Layout을 실행하여 각 노드의 기하학적 모향을 게산을 한다. 각각의 노드를 화면에 Paint 하므로 브라우저 렌더가 완료 된다. 우리가 여기서 중점을 둬야 할 부분은 Critical Rendering Path를 최적화하여 각 단계를 줄인다면, 초기 화면뿐만 아니라, 화면 업데이트 시간도 줄일 수 있다. 이러한 관점을 유지하면서 각각의 프로세스에 대해 알아보자.
우선 브라우저를 열어서 주소를 입력한다고 가능을 해보자. 주소 입력 시 DNS 서버로 가서 데이터를 요청 할 주소(ip)를 찾는다. 그리고 ip 주소로 웹 페이지를 표시하기 위한 자료를 요청한다. 브라우저는 웹 사이트에서 받은 정보를 조립하여 유저에게 보여주는 것이다. 서버로 요청(Request)과 응답(Response)을 받는데, 받은 응답정보(Bytes)를 통해 DOM과 CSSOM을 그리는 부분이 시작된다.
DOM (Document Object Model)
브라우저는 수신받은 HTML을 통해 Bytes > Characters > Tokens > Nodes > DOM 과정을 거처 DOM Tree를 만든다. 우선은 브라우저가 원시 Bytes를 네트워크에서 읽어와서 지정된 인코딩(UTF-8)에 따라 개별 문자로 변환을 한다. 그리고 변환된 문자열을 W3C HTML5 포준에 지정된 고유 token(<html>, <body>와 같은 태그들)으로 변환을 한다. 이때 변환 된 token을 Nodes로 변환하는데, 변환된 Node는 각각의 토큰의 속성과 규칙이 포함되어 있는 객체라고 할 수 있다. 마지막으로 각각의 Node들을 DOM으로 변경된다. 이때, 생성된 각체들은 트리 데이터 구조에서 연결되고 태그들은 상하관계를 가지게 된다.(HTML 객체는 body 객체의 상위라는 것은 이 단계에서 적용이 된다고 할 수 있다.)
CSSOM (CSS Object Model)
CSSOM은 JavaScript에서 CSS를 조작할 수 있는 객체라고 할 수 있다. DOM에서는 HTML을 Node로 변경한다면, CSSOM은 HTML이 아닌 CSS를 Node로 변경해서 동적으로 읽고 수정할 수 있다고 할 수 있다. CSS를 브라우저에서 이해할 수 있도록 DOM에서 진행한 방식과 동일하게 Bytes > Characters > Tokens > Nodes > CSSOM 순서로 적용이 된다.
DOM 트리는 순차적으로 파싱을 한다. 그렇다면, CSSOM도 동일할까? 기본적으로 CSS는 HTML의 가장 윗부분에 위치해야한다. (<head> 아랫부분) 왜냐하면 Render tree를 구성 시 CSSOM 트리는 CSS를 모두 해석이 완료된 후에 CSSOM 트리가 생성되기 때문에, CSSOM이 구성되지 못하면, Render 트리를 만들 수 없고 렌더링이 차단이 된다. 이러한 이유 때문에 CSS는 렌더링 차단 리소스로 불리기도 한다. DOM 트리 파싱 시 렌더링이 차단되지 않도록, CSS는 상단에 배치하도록 하자.
JavaScript
DOM과 CSSOM을 공부할 때, 반드시 나오는 개념이 바로 JavaScript이다. DOM과 CSSOM을 동적으로 변화시키는 존재 JavaScript이다. HTML에서 순차적으로 DOM이 생성이 되다가 <script> 태그를 만나면, script을 다운로드하고 실행이 완료될 때까지 DOM 트리 생성은 중단된다. 따라서 JavaScript도 렌더링 차단 리소스라 불린다. 따라서 가능하면 HTML 문서 최하단 </body> 앞에 두는 것이 좋다.
물론, HTML 파싱 시, script를 만나더라도 파싱을 멈추지 않게 할 수 있다. script 태그에 defer나 async를 추가하면 DOM / CSSOM을 변경하지 않겠다는 의미로 HTML 파싱을 멈추지 않는다. 관련 내용은 여기를 참고하자.
JavaScript를 간단히 다루고 있지만, 사실 싱글 스레드인 JavaScript의 실행 시간은 렌더링 시간에 직결된다. 또한 JavaScript에 의한 DOM / CSSOM 변경도 렌더링 속도에 관련이 된다고 할 수 있다. 아래의 내용부터는 렌더링 성능 최적화에 관련된 내용도 조금씩 넣어보려 한다.
Render Tree
앞에서 우리는 DOM과 CSSOM을 만들었다. 이때, DOM은 콘텐츠를 포함하는 객체이고, CSSOM은 스타일 규칙을 설명하고 있는 독립적인 객체이다. 이러한 객체를 가지고 화면에 pixel을 찍어 보여주기 위해서는 두 객체를 합치는 과정이 필요하다. 이러한 각각의 객체를 합치는 과정으로 DOM과 CSSOM을 합쳐서 Render Tree를 만든다. 세부 과정은 아래와 같다.
DOM 트리의 Node을 상위 root를 기준으로 하나씩 변환을 진행한다. 이때, script 태그, meta 태그, display none과 같은 속성을 가진 요소들은 rendering 출력에 반영되지 않고, Render Tree에서 생략이 된다. 변환된 Node들에 일치하는 CSSOM을 찾아 적용을 진행하여 Render Tree를 만든다.
Render Tree에는 스타일 정보가 포함되어 있고, 실제 화면에 표현되는 노드 들로만 구성된다.
기본적으로 display:none 과 visibility: hidden은 다르다고 할 수 있다. 둘다 눈으로 보여지지 않는 것은 맞다. 하지만, display: none은 element 자체를 제거하여, 차지하고 있는 공간 자체를 제거한다. 그러나 visibility: hidden은 공간은 유지한체로 화면에서만 보이지 않는다. 이때, display:none을 사용하면 공간 자체를 지우기 때문에 layout을 다시 그리게 된다. 만약 다시 보여지게 해야한다면, display 보다는 visibility를 사용하는 것이 더 좋다고 할 수 있다. 정리하면, display: none 은 DOM 조작 및 스타일 변경이 있더라도, 레이아웃과 리페인트가 발생하지 않는다. visibility: hidden은 보이지 않고 공간만 차지하고 있기 때문에, 리페인트는 발생하지 않고 레이아웃만 발생한다. 만약 변경이 많이 일어나는 변경을 한다면, display:none을 한 상태에서 변경 후에 다시 보여지게 한다면, 레이아웃 발생을 최대한 줄이는 것이 가능하다. |
Render Tree가 생성되면 Layout 단계를 진행한다.
Layout (=Reflow)
Render Tree에는 DOM에서 변환된 Node와 CSSOM에서 나온 Node 스타일만 만들어져 있다고 할 수 있다. Layout에서는 실제로 화면에 표시될 노드의 정확한 위치와 크기를 계산한다. 정확한 위치와 크기란 픽셀단위로 표현된다고 이야기할 수 있다. 즉, % 나 vw로 지정했던 모든 값들이 픽셀값으로 나타난다고 할 수 있다. 이때, RenderTree를 탐색하여 각 노드의 Box Type을 확인한다. 그리고 type이 inline / block에 따라 위치를 계삭하여 노드를 위치시킨다. Layout 단계를 거치면 각 노드는 정확한 위치를 얻게 되는 것이다.
화면에 표시되는 브라우저의 영역과 크기를 Viewport(뷰포트)라고 한다. 뷰포트는 모바일의 경우 디스플레이의 크기이고, PC의 경우는 브라우저 차으이 크기라고 할 수 있다. 대부분 화면을 그릴 때, 요소의 크기와 위치는 %, vw, vh로 적용되어 있기 때문에, Viewport가 달라진다면, px 계산을 다시 해야 하므로 Layout을 다시 그린다고 할 수 있다.
Layout은 변경된 위치를 계산하한다. 즉 JavaScript 실행 시 Layout이 계산을 하는 시간을 적게 하는 것이 필요하다. 기본적으로 JavaScript로 DOM의 위치나 스타일을 변경하면, Layout이 변경되고, paint를 다시 진행한다. 이때, Layout을 변경하지 않고 Paint만 다시 할 수 있도록 해서 성능을 높일 수 있다.
우선 주의해야 할 점은 Forced Synchronous Layout(강제 동기 레이아웃)을 최소화하거나, 관련 로직을 for문을 돌면서 하나씩 하는 것보다는 변수로 전부 참조한 이후에 한꺼번에 변경하는 것이 더 좋다. 만약 페이지 슬라이더를 만들기 위해 const slideWidth = tableWrapper.clientWidth; 를 사용한다고 해보자. 여기서 사용한 clientWidth / offsetHeight 등등을 사용할 때, DOM 조작이 없는데도 레이아웃을 강제로 발생시킨다. 즉, javascript를 통해 기하학적인 수치를 알아낼 때는 Forced Synchronous Layout(강제 동기 레이아웃)이 발생하므로, 사용할 때는 잠시 고민을 하는 것이 좋다. 이러한 로직을 for 문 안에 넣어서 반복적으로 사용하는 하면, 레이아웃이 반복적으로 발생하여 성능이 안 좋아진다. 이를 어려운 말로 Layout Thrashing이라고 하는데, 사실 클린 코드를 작성하면 크게 문제 될 것 없다고 생각한다.
기본적으로 넓은 범위, 즉 부모 element를 수정을 하면 내부의 element들이 모두 Layout이 발생할 수 있다. 따라서 가능하면 작은 요소들을 변경하는 것이 좋고, 변경이 많이 필요하다면 따로 Layout을 만들면 주변 Layout에 영향을 미치지 않는다. 따로 Layout을 만드는 방법은 absolute나 fixed로 position을 설정해 주면 된다. 또한 Layout 변경을 최소화하기 위해서는, position:absolute;에서 애니메이션을 줄 때 top/bottom/left/right를 변화하는 것보다는 transform을 사용하는 것이 좋다.
Paint(=Resterizing)
Layout 을 통해 화면에 표현하기 위한 계산이 끝나면, UI를 화면에 표현하기 위한 Paint 과정이 이루어진다. Paint는 ㄱRender Tree의 내용을 화면의 픽셀로 변환하는 프로세스로, 텍스트 /color / image/ border 등등이 모든 시간적인 부분을 그린다.
정리하면, DOM과 CSSOM 트리를 결합하여 Render tree를 생성한다. 생성된 Render tree는 페이지 렌더에 필요한 node만 가지고 있다. Layout에서는 각각의 정확한 객체의 위치와 사이즈를 계산한다. 그리고 마지막 paint 단계에서는 스크린의 픽셀에 렌더링 한다.
Composite
Paint 단계가 완료되면, 생성된 레이어를 합성한다. 적절한 위치에 조합하여 스크링에 이미지를 만들어 내는 작업이라고 할 수 있다. 이 단계가 끝나면 웹 페이지를 볼 수 있다. 사실 Composite는 일반적으로 매우 빠르게 실행된다.
지금까지 기본적인 브라우저 렌더링에 대해 전반적으로 알아 보았다. 다음 글은, 각각의 세부내용도 조금 더 적어보도록 하겠다.
'Web' 카테고리의 다른 글
브라우저(web browser)의 기본 정리 (0) | 2023.07.05 |
---|---|
[React] HTMLElement 기초 정리 (1) | 2023.01.24 |
MERN Stack 으로 가장 빠르고 쉽게 Web만들기 (0) | 2022.04.19 |
[Error] fatal: in unpopulated submodule (0) | 2022.03.16 |
[아키텍쳐] Layered pattern (0) | 2022.03.10 |
- Total
- Today
- Yesterday
- 자료구조
- next.config.js
- useHistory 안됨
- NextJS
- react
- read_csv
- nextjs autoFocus
- DFS
- TensorFlow
- nodejs
- typescript
- logout
- Python
- 자연어처리
- Vue
- django
- login
- pandas
- mongoDB
- vuejs
- Deque
- error:0308010C:digital envelope routines::unsupported
- UserCreationForm
- 클라우데라
- JavaScript
- useState
- Queue
- BFS
- react autoFocus
- Express
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |