옛날부터 HTML 파일이 어떻게 렌더링 되는지에 대해서 궁금증만 있었지 제대로 알아본 적은 없었다. 프론트엔드 개발자로서 더 이상 넘어갈 수 없다. 이번 글을 통해서 브라우저의 HTML 렌더링 순서를 각 잡고 정리해보겠다.
1. 불러오기
로더(Loader)가 서버로부터 전달 받는 리소스 스트림을 읽는 과정.
읽으면서 어떤 파일인지, 데이터인지 파일을 다운로드할 것인지 등을 결정한다.
2. 파싱 (Phasing)
브라우저 웹 엔진이 가지고 있는 HTML/XML 파서가 문서를 파싱해서 DOM Tree를 만든다.
<head> -> <body> 순서로 파싱이 진행한다. 그런데 만약 <head> 내부의 <script>, <link> 태그를 만나게 된다면 해당 파일이 로드, 파싱이 완료 될 때까지 HTML의 파싱이 중단된다. 이는 DOM 트리가 만들어지는 것을 지연시키기 때문에, 두가지 문제를 발생시킬 수 있다.
- HTML을 읽는 과정에 스크립트를 만나면 중단 시점이 생기고 그만큼 렌더링이 지연된다.
- DOM 트리가 생성되기전에 자바스크립트가 생성되지도 않은 DOM의 조작을 시도할 수 있다.
이러한 문제를 막기 위해서는 <script> 태그의 로딩 순서를 제어해야만 한다. <body> 태그의 끝, 즉 </body>의 윗부분에 <script> 태그를 위치시키는 것이 대표적인 방법이다.
렌더링 엔진은 더 나은 사용자경험을 위해 가능한 빠르게 내용을 표시하게 만들어졌다. 따라서 모든 HTML 파싱이 끝나기도 전에 이후의 과정을 수행하여 미리 사용자에게 보여줄 수 있는 일부 내용들을 출력하게 된다. 이렇게 하면 DOM 트리의 요소들이 이미 생성 되어있기 때문에 문제들을 해결할 수 있다. 이외에도 <script>의 defer, async 속성을 이용하는 방법이 있다.
3. CSS 결정
한편, CSS는 DOM과 상관이 없기 때문에 동시에 진행된다. CSS는 선택자에 따라서 적용되는 태그가 다르기 때문에 모든 CSS 스타일을 분석해 태그에 스타일 규칙이 적용되게 결정하고, CSSOM(CSS Oject Model) 트리를 생성한다.
4. 렌더링 트리 만들기
DOM Tree는 내용을 저장하는 트리로 자바스크립트에서 접근하는 DOM객체를 쓸 때 이용하는 것이고 별도로 그리기 위한 트리가 만들어져야 하는데 그것이 렌더링 트리다. (실제 화면에 표현되는 노드들로만 구성됨)
렌더링 트리는 DOM과 CSSOM을 이용하여 만들어진다. 스타일을 결정하고 렌더러를 만드는 과정을 어태치먼트(attachment) 라고 부른다. 모든 DOM 노드에는 attach 메서드가 있다. 어태치먼트는 동기적인데 DOM 트리에 노드를 추가하면 새 노드의 attach 메서드를 호출한다. 즉, 먼저 DOM 트리에 추가된 것들을 화면에 출력하고, 이후 추가된 부분들은 이후에 구축한다는 것이다.
5. Layout
브라우저의 사이즈에 따라서 달라지는 CSS 값들(%, vh, vw)을 계산하여, 브라우저 화면의 어느 위치에 어느 크기로 출력할지 계산되는 단계이다. 렌더링 트리에서 위치나 크기를 가지고 있지 않기 때문에 객체들에게 위치와 크기를 구체적인 픽셀 값으로 정의해준다.
6. Paint
렌더링 트리를 탐색하면서 화면에 출력한다.
렌더링 최적화 - Reflow, Repaint 줄이기
완성된 페이지가 끝이 아니다. 사용자와 상호작용하며 html 요소의 속성을 수정하면 그에 영향받은 노드들을 포함하여 Layout 과정을 다시 수행하게 된다. 이렇게 되면 렌더링 트리를 다시 계산하게 된다. 이 과정을 Reflow라고 한다. 이어서, 수정된 렌더링 트리를 다시 화면에 출력해주는 과정이 필요하다. 결국은 Paint 단계가 다시 수행되는 것이며 이를 Repaint 라고 한다. Reflow를 일으키는 이벤트들은 다음과 같다.
- DOM 엘리먼트 추가, 제거 또는 변경
- CSS 스타일 추가, 제거 또는 변경
- CSS 스타일을 직접 변경하거나, 클래스를 추가함으로써 레이아웃이 변경될 수 있다. 엘리먼트의 길이를 변경하면, DOM 트리에 있는 다른 노드에 영향을 줄 수 있다.
- CSS3 애니메이션과 트랜지션. 애니메이션의 모든 프레임에서 리플로우가 발생한다.
- offsetWidth 와 offsetHeight 의 사용. offsetWidth 와 offsetHeight 속성을 읽으면, 초기 리플로우가 트리거되어 수치가 계산된다.
- 유저 행동. 유저 인터랙션으로 발생하는 hover 효과, 필트에 텍스트 입력, 창 크기 조정, 글꼴 크기 변경, 스타일시트 또는 글꼴 전환등을 활성화하여 리플로우를 트리거할 수 있다.
따라서, 최적화를 위해서는 해당 이벤트들을 최소화 해야 한다.
최소화 방법
1. 인라인 스타일 사용 X
인라인 스타일은 가독성을 해칠뿐 아니라, 수차례 리플로우를 발생 시킨다. 그에 반해 외부 스타일을 사용할 경우 한번만 리플로우를 발생시킨다.
2. 작업 그루핑
DOM 요소의 정보를 요청하고 변경하는 코드는 같은 형태의 작업끼리 묶어 실행시키는 것이 좋다.
3. 노출 제어
display 속성의 값을 none으로 하면, 렌더링 트리에서 노드가 빠지게 된다. 따라서 노드를 노출시킨채 스타일을 변경하는 것보다, 노드를 감추고 스타일을 변경한 후 노드를 노출 시키는 것이 리플로우와 리페인트 발생횟수를 줄이는 방법이다.
4. 노드 복제
변경하려는 요소의 노드를 복제한 후 복제된 노드에 작업을 하고, 교체를 해주게 되면 리플로우와 리페인트는 한번만 발생한다.
5. 캐싱
여기서 말하는 캐싱은 별도의 변수에 자주 사용하는 값을 저장하는 것이다. scrollWidth와 같은 값을 호출할 경우 리플로우를 유발하기 때문에, 반복 구문을 실행하기 전에 이를 변수에 담아 놓으면 리플로우 발생을 최소화 할 수 있다.
6. 프레임 줄이기
단순히 생각하면 0.1초에 1px씩 이동하는 요소보다 3px씩 이동하는 요소가 Reflow, Repaint 연산비용이 3배가 줄어든다고 볼 수 있습니다. 따라서 부드러운 효과를 조금 줄여 성능을 개선할 수 있다.
7. React, Angular 이용
이어서, React의 Virtual DOM이 왜 생겨났는가에 대한 이유로 이어진다.
일반적으로 dom에 접근하여 여러번의 속성 변화, 여러번의 스타일 변화를 수행하면 그에따라 여러번의 Reflow, Repaint가 발생하게 된다. 하지만 Virtual DOM은 이렇게 변화가 일어나 Reflow, Repaint가 필요한 것들을 한번에 묶어서 dom에 전달하게 된다. 따라서 처리되는 Reflow, Repaint의 규모가 커질 수는 있지만 한번만 연산을 수행하게 된다. 이를 통해 여러번 Reflow, Repaint를 수행하며 연산이 반복적으로 일어나는 부분이 줄어들어 성능이 개선된다.
물론 프레임워크 없이 순수한 JavaScript로 똑같은 알고리즘을 구현할 수 있겠지만 실제로 구현하기에는 매우 어렵기 때문에 React, Angular가 이를 대신해주어 인기를 얻게 되었다.
velog.io/@ru_bryunak/%EB%A0%8C%EB%8D%94%EB%A7%81%EC%9D%B4%EB%9E%80
'javascript' 카테고리의 다른 글
[JS] Promise.race를 사용하여 timeout 구현하기 (0) | 2023.10.15 |
---|---|
정규표현식으로 사이에 있는 내용 가져오기 (0) | 2023.02.12 |
fetch (0) | 2020.11.15 |
XMLHttpRequest (0) | 2020.11.10 |
Promise (0) | 2020.11.03 |