파싱(
Parsing
)이란?
프로그래밍 언어로 작성된 파일을 실행시키기 위해 구문 분석(Syntax Analysis
)을 하는 단계이다.
파일의 문자열들을 문법적 의미를 갖는 최소 단위인 '토큰'으로 분해하고, 이 토큰들을 문법적인 의미와 구조에 따라 노드라는 요소로 만든다.
이러한 단계에서 노드들은 상하관계를 반영해 트리를 형성하는데, 이 트리를Parse Tree
라고 한다.
렌더링(
Rendering
)이란?
HTML
,CSS
,JavaScript
파일을 파싱해, 브라우저에 시각적으로 출력하는 과정이다.
브라우저는 HTML
, CSS
, JS
, 이미지, 폰트 등 리소소를 서버에 요청하고, 응답으로 받아온다.
브라우저 렌더링 엔진은 받아온 HTML
, CSS
를 파싱해 Dom
, CSSOM
을 생성하고, 이들을 결합해 Render Tree
를 생성한다.
브라우저 JS
엔진은 받아온 JS
를 파싱해 AST
를 생성하고, 바이트 코드로 변환해 실행한다.
렌더트리를 기반으로 HTML
요소의 레이아웃(위치, 크기)을 계산한다.
화면에 HTML
요소를 페인팅한다.
브라우저는
HTML
,CSS
,JS
, 이미지, 폰트 등 리소스를 서버에 요청하고, 응답으로 받아온다.
서버가 가지고 있는 HTML
, CSS
, JavaScript
파일을 브라우저애서 서버에게 요청하고, 응답으로 받아와야 한다.
브라우저에 있는 주소창에 위와 같은 URL
을 입력하고 엔터키를 누르면, URL
의 호스트 이름이 DNS
(도메인 네임 서비스)를 통해 진짜 주소인 IP
주소로 변환되고, 이 IP
주소를 갖는 서버에게 요청을 보낸다.
서버는 기본적으로 보통 index.html
을 응답으로 주도록 설정되어 있다.
예를 들어, https://www.google.com 을 검색하면 사실은 https://www.google.com/index.html을 요청하는 것과 다름 없다.
이 요청에 대해 구글 서버는 클라이언트에 index.html
파일을 전달 해주며, 다른 파일을 요청하고 싶다면 뒤에 다른 파일 경로를 적거나, JavaScript
를 통해서 동적으로 요청할 수도 있다.
HTML
파싱, DOM
생성브라우저 렌더링 엔진은 받아온
HTML
,CSS
를 파싱해DOM
,CSSOM
을 생성하고, 이들을 결합해 렌더 트리를 생성한다.
응답으로 받아온 HTML
문서는 오직 텍스트로만 이루어져 있다.
이 텍스트들 우리가 알아볼 수 있도록 이 문서를 브라우저가 이해할 수 있는 형태로 바꾸는 작업이 필요한데, 여기서 말하는 형태가 바로 DOM
구조이다.
위의 그림은 서버에서 받아온 파일을 브라우저가 이해하기까지의 과정이다.
바이트 (Bytes
) : 서버는 브라우저에게 2진수 형태의 HTML
문서를 응답으로 준다.
문자열 (Characters
) : 문서는 <meta>
의 charset
속성에 지정된 방식으로 문자열로 인코딩 된다.(ex : UTF-8
) 서버는 이 인코딩 방식은 응답 헤더에 담아준다.
토큰 (Tokens
) : 문자열 형태의 HTML문서를 '토큰'단위로 분해한다. (문법적 의미를 갖는 코드의 최소 단위)
노드 (Nodes
) : 각 토큰을 객체로 변환해, 노드를 생성한다. (DOM
을 구성하는 기본 요소)
DOM
: HTML
문서의 요소들의 중첩관계를 기반으로 노드들을 트리 구조로 구성한다. 이 트리를 DOM
이라고 한다.
위와 같은 과정을 거쳐서 HTML
문서가 파싱되고, DOM
이라는 결과물을 생성하게 된다.
위와 같은 복잡한 과정을 거치는 이유는 DOM
은 Document Object Model
의 줄임말로, 우리말로는 문서 객체 모델이라 할 수 있다.
말 그래도 문서를 객체로 바꾼 모델로, 브라우저는 JavaScript
언어만 알아듣는데, JavaScript
는 HTML
의 태그나 속성들을 바로 다룰 수 없기 때문에, 다룰 수 있는 형태인 '객체'로 바꿔주어야 한다.
그래야 브라우저도 HTML
문서를 이해할 수 있게 되기 때문에 이러한 과정을 거친다.
앞서 html 파일을 파싱하다가 <link>
, <style>
태그를 만나면 파싱을 잠시 멈추고 리소스 파일을 서버로 요청한다.
이 태그들은 CSS
파일을 가져올 때 보통 쓰는데, 이렇게 가져온 CSS
파일도 HTML
과 마찬가지로 파싱을 한다. 서버에서 받아온 2진수 파일을 문자열로 인코딩하고, 토큰 단위로 나누고, 노드를 생성하고, 트리를 만들고.. 이렇게 파싱해 만든 트리는 CSSOM
이라고 한다.
위의 그림처럼 CSSOM
은 CSS
의 속성이 상속되기 때문에, 이를 반영한다는 점이다.
예를 들어 ul
이 부모 요소이고, li
를 자식 요소로 가진다고 생각하면, 아래 코드처럼 ul
는 파란색 속성을 가지고 있는데, 이 속성은 자식 요소인 li
도 상속받게 된다.
따라서 li
는 상속받은 color: blue
와 자신이 가지고 있던 font-size
속성 두 가지를 갖게 된다.
DOM
과 CSSOM
은 굉장히 비슷하게 생겼지만, 서로 다른 속성들을 가진 독립적인 트리들이다.
HTML
은 구조를, CSS
는 디자인을 담당하기 때문에 둘을 합치는 작업이 필요하다.
렌더 트리는 이름처럼 렌더링을 목적으로 만드는 트리이며, 렌더링은 브라우저가 이제 진짜로 사용자에게 보여주기 위한 화면을 그리는 과정이기 때문에, 보이지 않을 요소들은 이 트리에 포함하지 않는다.
예를 들어, DOM
에서는 meta
태그같은 정보 전달 목적의 태그나, CSSOM
에서는 display: none
으로 보이지 않게 해둔 요소(정확히는 노드)들은 렌더 트리에서는 제외된다.
단,
visibility: hidden
은 레이아웃 트리에 포함되니 주의해야 한다.
위의 그림처럼 DOM
, CSSOM
에 있던 속성들이 합쳐져 렌더트리를 구성하는 것을 확인할 수 있지만, 렌더 트리는 아직까지도 텍스트로 구성된 객체로 밖에 보이지 않는다.
실제로 우리가 보는 페이지를 만들기 위해서는 '페인팅'이라는 작업을 거쳐야 한다.
페인팅 작업은 렌더트리의 노드들이 가지고 있는 속성들을 바탕으로 이루어지는데, 이 작업은 잠시 후에 다시 자세히 알아보도록 하고 우선은 JavaScript
파일은 어떻게 해석이 되는지부터 잠시 살펴본다.
JavaScript
파싱브라우저
JS
엔진은 받아온JS
를 파싱해AST
를 생성하고, 바이트 코드로 변환해 실행한다.
렌더링 엔진은 HTML
파일을 한줄씩 파싱하며 DOM
을 생성하다가 JavaScript
코드들 불러오는 <script>
태그를 만날 때도 파싱을 잠시 멈춘다.
그리고 나서 src
속성에 적혀있는 파일을 서버에 요청해 받아오고, 이렇게 받아온 js
파일도 마찬가지로 파싱을 해야하는데, 이 파싱은 브라우저 렌더링 엔진이 직접하지 않고, JavaScript
엔진이 담당하게 된다.
이 때 렌더링 엔진은 JS
엔진에게 제어권을 아예 넘겨주기 때문에, HTML
파싱을 멈췄다가 JS
파싱이 전부 되면 다시 제어권을 돌려받아 파싱을 다시 시작하는 것이다.
JS
엔진은 js
파일의 코드를 파싱해서 컴퓨터가 이해할 수 있는 기계어로 변환하고 실행한다.
좀 더 구체적으로 살펴보면, 먼저 단순한 텍스트 문자열인 코드를 토큰 단위로 분해하며, 이렇게 분해된 토큰에 문법적인 의미와 구조가 더해져, AST
(추상 구문 트리) 라는 트리가 완성된다.
구체적인 속성은 다르지만, 이전에 봤던 과정들과 비슷해 보이며, 아래 그림에서 맨 왼쪽의 코드가 바로 다음의 트리 구조로 바뀌는 부분이 여기까지의 내용에 해당한다.
이제 이렇게 코드를 해석해서 만든 AST
라는 트리를 실제로 실행할 수 있도록 만들어야 한다.
코드의 실제 실행은 인터프리터가 담당하는데, 인터프리터가 알아들을 수 있도록 하기 위해서는 AST트리를 바이트 코드라는 중간 수준의 코드로 변환해야 한다.
이 변환은 바이트 코드 생성기가 담당해주며, 이제 위의 그림에서 가장 오른쪽에 있는 형태로 바뀌어 받아온 js
파일 내용이 실제로 실행된다.
렌더 트리를 기반으로
HTML
요소의 레이아웃(위치, 크기)을 계산한다.
레이아웃은 요소의 기하학적인 속성들을 찾는 과정이며, 아까 만들었던 렌더트리가 여기서 사용되니 다시 떠올려야 한다.
렌더 트리에는 요소들의 위치나 크기와 관련된 정보들이 들어있었다.
하지만 이 정보들은 각 요소들에 대한 정보일 뿐, 전체 화면에서 정확히 어디에 위치할 것인지에 대해서는 아직 알지 못하며, 이런 계산을 하는 단계가 레이아웃 단계이다.
브라우저는 각 요소들이 전체 화면에서 어디에, 어떤 크기로 배치되어야 할 지 파악하기 위해 렌더트리의 맨 윗부분부터 아래로 내려가며 계산을 진행하며, 모든 값들은 절대적인 단위인 px
값으로 변환된다.
예를들어 우리가 <div>
요소 하나만 띄우도록 코드를 작성했고, width를 50%로 지정해두었다면, 이 값은 전체 화면 크기(viewport
)의 절반 크기로 계산되고, 절대적인 값인 px 단위로 변환되는 식이다.
화면에
HTML
요소를 페인팅한다.
이제 위치에 대한 계산도 마쳤으니, 정말로 화면에 보여줄 차례이다.
브라우저 화면은 픽셀이라고 하는 정말 작은 점들로 이루어져 있으며, 각각 정보를 가진 픽셀들이 모여 하나의 이미지, 화면을 구성하는 것이다.
따라서 화면에 색상을 입히고, 어떤 요소를 보여주기 위해서는 이 픽셀에 대한 정보가 있어야 하고, 페인팅은 이러한 픽셀들을 채워나가는 과정이다.
따라서 이 과정을 마지막으로 우리는 단순한 텍스트에 불과했던 파일 내용들을 이미지화된 모습으로 브라우저 화면을 통해 볼 수 있게되는 것이다.
Reflow
(리플로우), Repaint
(리페인트)리플로우(
Reflow
) = 레이아웃 계산을 다시하는 것
리페인트(Repaint
) = 새로운 렌더트리를 바탕으로 다시 페인트를 하는 것
만약 사용자가 브라우저 화면을 늘리거나 줄이는 등 크기를 조절하거나, 어떤 버튼을 눌러 화면에 요소가 추가되거나 삭제되는 경우가 생기면, 당연히 화면에 있던 요소들의 위치나 크기 등이 바뀌는 일이 생기게 될 것이다.
굉장히 당연하게 여겨지지만, 이렇게 화면에 나타나는 모습을 바꾸기 위해서는 모든 요소들의 위치와 크기를 다시 계산하고, 다시 그려서 보여주어야 한다.
이렇게 어떤 인터랙션으로 인해 앞서 보았던 레이아웃, 페인팅 과정을 반복하는 것을 리플로우, 리페인트라고 한다.
const makeDiv = document.createElement('div');
위의 코드는 JavaScript
를 통해 DOM을 조작하는 코드이다.
DOM
은 단순히 HTML
파일의 정보만 담고있게 아니라, 이렇게 JavaScript
를 통해 요소들을 동적으로 조작할 수 있도록 DOM API
라는 것을 제공한다. CSS
도 마찬가지로 이런식으로 조작이 가능하다. 이렇게 JavaScript
조작으로 변경이 일어나면, DOM
트리를 다시 구성하는 것으로 시작해 CSSOM
와 합쳐져 새 렌더 트리를 생성한다.
그리고 레이아웃과 페인트 과정을 또다시 거쳐서 화면에 보여진다.
DOM
조작은 리플로우, 리페인팅이 일어나는 대표적인 예시라고 할 수 있다.
물론 레이아웃과 페인트는 별개의 작업이기 때문에, 하나 씩만 발생할 수도 있고, 둘 다 발생할 수도 있으며, 위치나 크기에 대한 변경만 있다면 레이아웃 작업만 다시하면 되고, 요소의 색상이나 보이는지 여부 같은 스타일에 대한 사항에 대한 변경만 있다면 페인팅 작업만 다시하면 되기 때문이다.
Reflow
Reflow
는 렌더링 엔진에서 요소를 배치하는 과정을 의미한다.
렌더 트리 구축 단계에서 DOM
트리와 스타일 규칙을 합쳐서 렌더 트리를 만들고, 여기에서 Reflow
를 통해 각각의 요소들의 레이아웃을 위치시킨다.
여기서 렌더 트리는 DOM
요소를 기반으로 만들어지지만, 완전히 대응되지는 않는다.
DOM
트리가 문서의 구조를 나타낸다면 렌더 트리는 문서의 시각적 구조를 나타낸다.
예를 들어 스타일에 display: none
속성이 있다면 DOM
에는 존재하지만 시각적으로는 없기에 렌더 트리에는 할당되지 않는다.
Reflow
가 발생하는 경우는 다음과 같다.
DOM
노드의 추가, 제거
DOM
노드의 위치 변경
DOM
노드의 크기 변경
margin
,padding
,border
,width
,height
등
CSS3
애니메이션과 트랜지션
폰트 변경, 텍스트 내용 변경
이미지 크기 변경
offset
, scrollTop
, scrollLeft
와 같은 계산된 스타일 정보 요청
페이지 초기 렌더링
윈도우 리사이징
Repaint
Repaint
는 렌더 트리가 탐색되고 paint
메서드가 호출되어서 UI
기반의 구성 요소를 사용해서 그리는 과정이다.
Repaint
가 이루어지기 위해서는 렌더 트리가 있어야 하고 따라서 Reflow
작업이 이루어진 후에 Repaint
작업이 이루어지는 것을 알 수 있다.
화면의 구조가 변경이 될 때는 Reflow
와 Repaint
가 모두 발생한다.
다만 Repaint
가 발생하기 위해서 항상 Reflow
가 발생해야 하는 것은 아니다.
Reflow
가 발생하지 않고 Repaint
만 발생하는 경우도 있는데, 예를 들면 레이아웃에 영향을 주지 않는 엘리먼트 개별의 변화에 대해서는 Repaint
만 발생한다.
color
,background-color
,visibility
같은 속성의 경우, 곧바로Repaint
과정만 발생한다.
Reflow
최적화
Reflow
는 비용을 발생시키는 절차이므로 가능한 안 하는 것이 성능 측면에서 유리하며, 아래의 사항을 통해Reflow
작업을 줄일 수 있다.
DOM
노드의 크기 또는 위치가 변경되면 하위 노드와 상위 노드에도 영향을 미칠 수 있다.
이 때 가장 하위 노드의 스타일을 변경할 경우, 전체 노드가 아닌 일부 노드로 Reflow
를 영향을 최소화 할 수 있다.
전체적으로 큰 노드를 뒤흔드는 것보다 그 하위의 변경할 부분만을 변경하여
Reflow
의 규모를 줄이는 방식으로 정리할 수 있다.
inline
) 사용 줄이기인라인 스타일은 HTML
이 파싱될 때, 레이아웃에 영향을 미쳐 추가 Reflow
를 발생시킨다.
또한 관심사 분리가 제대로 이루어지지 않으면 유지 보수가 힘들어 진다.
position
을 fixed
또는 absolute
로 지정애니메이션 효과는 많은 Reflow
비용을 발생시킨다.
position
속성을 fixed
또는 absolute
의 값으로 지정해서 지정된 노드를 전체 노드에서 분리시켜 해당 노드에서만 Reflow
가 발생하도록 제한시킬 수 있다.
애니메이션 효과를 줘야 하는 노드에 position
속성이 적용이 되지 않았다면 애니메이션 시작 시 position
속성 값을 fixed
또는 absolute
로 변경하였다가 애니메이션 종료 후 다시 원복시켜서 렌더링을 최적화 할 수 있다.
<table>
은 점진적으로 렌더링 되지 않고, 모두 로드되고 테이블 너비가 계산된 후에 화면에 그려진다.
테이블 안의 컨텐츠의 값에 따라 테이블 너비가 계산된다는 의미로, 테이블 컨텐츠의 작은 변경만 있어도 테이블 너비가 다시 계산되고 테이블의 모든 노드들이 Reflow
가 발생한다.
이러한 이유로 <table>
을 레이아웃 용도로 사용하는 일은 피해야 한다.
CSS
하위 선택자 최소화CSS
하위 선택자를 최소화 하는 것은 Reflow
횟수를 줄이는 방법이 아니라 렌더 트리 계산을 최소화 하는 방법에 대한 내용이다.
<div class="reflow_box">
<ul class="reflow_list">
<li>
<button type="button" class="btn">버튼</button>
<li>
<li>
<button type="button" class="btn">버튼</button>
<li>
</ul>
</div>
CSS
코드/* 잘못된 예 */
.reflow_box .reflow_list li .btn{
display:block;
}
/* 올바른 예 */
.reflow_list .btn {
display:block;
}
위의 코드처럼 CSS
하위 선택자를 최소화하는 것 또한 렌더링 성능 향상에 도움이 된다.
렌더 트리는 DOM
과 CSSOM
이 합쳐져서 만들어지는데, DOM
은 HTML
이 파싱되어 만들어진 트리이고, CSSOM
은 CSS
가 파싱되어 만들어진 트리이다.
두 트리를 결합하여 렌더 트리를 만드는데, CSS
하위 선택자가 많아지면 CSSOM
트리의 깊이(Depth
)가 깊어지게 되고 결국 렌더 트리를 만드는 시간이 더 오래 걸리게 된다.
Reflow
비용을 줄이기 위해서 DOM 노드 사용을 최소화 해야 한다.
한 가지 방법은 DOM Fragment
를 사용하여 DOM
을 추가할 때 마다 DOM
접근을 최소화 하는 방법이다.
const frag = document.createDocumentFragment();
const ul = frag.appendChild(document.createElement('ul'));
for (let i = 1; i <= 3; i++) {
li = ul.appendChild(document.createElement('li'));
li.textContent = `item ${ i }`;
}
document.body.appendChild(frag);
위의 코드처럼 createDocumentFragment
를 사용하여 한 번에 DOM
을 추가하여 DOM
자체에 직접적인 접근을 최소화할 수 있다.
브라우저는 레이아웃 변경을 큐에 저장했다가 한 번에 실행하여 Reflow
를 최소화 한다.
하지만 offset
, scrollTop
과 같은 계산된 스타일 정보를 요청할 때마다 정확한 정보를 제공하기 위해 큐를 비우고 모든 변경 사항을 적용한다.
// 안좋은 예시
for (let i = 0; i < len; i++) {
el.style.top = `${ el.offsetTop + 10 }px`;
el.style.left = `${ el.offsetLeft + 10 }px`;
}
// 좋은 예시
let top = el.offsetTop, left = el.offsetLeft, elStyle = el.style;
for (let i = 0; i < len; i++) {
top += 10;
left += 10;
elStyle.top = `${ top }px`;
elStyle.left = `${ left }px`;
}
이런 낭비를 해결하기 위해 위의 코드와 같이 스타일 정보를 변수에 저장하여 offset
, scrollTop
등의 값 요청을 최소화해야 한다.
참고 사이트
joooing - 웹페이지가 사용자에게 보여지기까지 (브라우저 렌더링 과정)
CHANYEONG - 브라우저 렌더링과 최적화
Web Frontend Developer - Reflow와 Repaint에 대하여