본문 바로가기
Web

웹페이지가 사용자에게 보여지기까지 (브라우저 렌더링 과정)

by joooing 2021. 6. 14.
반응형

우리가 주소창에 어떤 웹사이트의 주소를 입력하고 엔터키를 누르면 몇 초도 지나지 않아서 해당 사이트의 화면이 뜨게된다. 사실 굉장히 자주 하는 일이고, 당연한 일이지만 주소 입력만으로 화려한(?) 화면이 등장하기까지는 사실 생각보다 복잡한 과정들을 거쳐야 한다. 이번 글에서는 바로 이 복잡한 과정에 대해 알아보고자 한다.

 

미리 알아야할 개념들

파싱(parsing)

프로그래밍 언어로 작성된 파일을 실행시키기 위해 구문 분석(syntax analysis)을 하는 단계이다. 파일의 문자열들을 문법적 의미를 갖는 최소 단위인 '토큰'으로 분해하고, 이 토큰들을 문법적 의미와 구조에 따라 노드라는 요소로 만든다. 노드들은 상하관계를 반영해 트리를 형성하는데, 이 트리를 파스트리라고 한다.

 

렌더링(rendering)

HTML, CSS, JavaScript 파일을 파싱해, 브라우저에 시각적으로 출력하는 과정이다.

 

 

브라우저 렌더링 전체 과정 미리보기

글에서 전반적으로 다룰 과정을 미리 소개한다. 렌더링이 되는 과정을 다섯 단계로 구분했는데 한 단계씩 차근차근 살펴보려고 한다.

1. 브라우저는 HTML, CSS, JS, 이미지, 폰트 등 리소스를 서버에 요청하고, 응답으로 받아온다.
2. 브라우저 렌더링 엔진은 받아온 HTML, CSS를 파싱해 DOM, CSSOM을 생성하고, 이들을 결합해 렌더 트리를 생성한다.
3. 브라우저 JS 엔진은 받아온 JS를 파싱해 AST를 생성하고, 바이트코드로 변환해 실행한다.
4. 렌더트리를 기반으로 HTML 요소의 레이아웃(위치, 크기)을 계산한다.
5. 화면에 HTML요소를 페인팅한다.

 

 

1. 요청, 응답

Step1. 브라우저는 HTML, CSS, JS, 이미지, 폰트 등 리소스를 서버에 요청하고, 응답으로 받아온다.

 

일단은 화면을 구성할 준비물들을 구해오는 것부터 시작해야 한다. 준비물에는 뭐가있을까? 우리가 잘 알고있는 HTML, CSS, Javascript 파일, 그리고 필요하다면 이미지나 폰트같은 파일들이 준비물에 해당한다. 그렇다면 이 파일들은 누가 가지고 있을까? 바로 서버가 가지고 있다. 브라우저는 우선 필요한 리소스(HTML, CSS, JS, 이미지, 폰트..)를 서버에 요청하고, 응답으로 받아와야 한다.

 

 

그렇다면 서버에 요청은 어떻게 전송할까? 브라우저에 있는 주소창이 그 역할을 한다. 주소창에 URL을 입력하고 엔터키를 누르면, URL의 호스트 이름이 DNS(도메인 네임 서비스)를 통해 진짜 주소인 IP 주소로 변환되고, 이 IP 주소를 갖는 서버에게 요청을 보낸다.

 

서버는 기본적으로 보통 index.html을 응답으로 주도록 설정되어 있다. 예를들어, 우리가 https://www.google.com 을 검색하면 사실은 https://www.google.com/index.html 을 요청하는 것과 다름 없다. 이 요청에 대해 구글 서버는 클라이언트에 index.html 파일을 전달해줄 것이다. 다른 파일을 요청하고싶다면 뒤에 다른 파일 경로를 적거나, Javascript를 통해서 동적으로 요청할 수도 있다.

 

개발자 도구의 네트워크창을 열어서 보면 이렇게 서버에 요청을 보내고, 응답을 받아오는 과정을 직접 확인할 수도 있다. 참고로, 아래 네트워크창은 주소창에 https://www.google.com 을 검색하고 아무것도 하지 않았을 때의 모습이다.

 

 

그런데 자세히 보면, 여기에는 index.html 뿐만 아니라 요청한 적도 없는 이미지같은 다른 리소스까지 딸려오고 있다. 브라우저 렌더링 엔진은 HTML 파일을 파싱할 때, 위에서 아래로 한 줄 한 줄 파싱한다. 그러다가 외부 리소스를 가져오는 태그를 만나면  리소스 파일을 서버로 요청한다. 보통 CSS파일은 <link>, Javascript파일은 <script>, 이미지는 <img>로 가져오는데, 이런 태그들을 만나 리소스들을 서버에서 받아오는 것이다. 위에서도 이처럼 index.html 문서에 있던 요청들에 대한 응답들까지 계속 받아오고 있다.

 

 

2-1. HTML 파싱, DOM 생성

Step2. 브라우저 렌더링 엔진은 받아온 HTML, CSS를 파싱해 DOM, CSSOM을 생성하고, 이들을 결합해 렌더 트리를 생성한다.

 

응답으로 받아온 HTML 문서는 오직 텍스트로만 이루어져있다. 이 텍스트들이 대체 어떻게 우리가 보는 화면이 되는걸까? 일단은 이 문서를 브라우저가 이해할 수 있는 형태로 바꾸는 작업이 필요한데, 여기서 말하는 형태가 바로 DOM 구조이다.

 

 

아래 그림과 함께, 서버에서 받아온 파일을 브라우저가 이해하기까지의 과정을 한 단계씩 살펴보자!

 

1. 바이트(Bytes) : 서버는 브라우저에게 2진수 형태의 HTML 문서를 응답으로 준다.

2. 문자열(Characters) : 문서는 <meta>의 charset 속성에 지정된 방식으로 문자열로 인코딩 된다.(ex. UTF-8) 서버는 이 인코딩 방식은 응답 헤더에 담아준다.

3. 토큰(Tokens) : 문자열 형태의 HTML문서를 '토큰'단위로 분해한다. (문법적 의미를 갖는 코드의 최소 단위)

4. 노드(Nodes) : 각 토큰을 객체로 변환해, 노드를 생성한다. (DOM을 구성하는 기본 요소)

5. DOM : HTML문서의 요소들의 중첩관계를 기반으로 노드들을 트리 구조로 구성한다. 이 트리를 DOM이라고 한다.

 

 

위와 같은 과정을 거쳐서 HTML 문서가 파싱되고, DOM 이라는 결과물을 생성하게 된다. 근데 왜 굳이 HTML파일을 이렇게까지 복잡하게 바꿔야할까? DOM은 Document Object Model의 줄임말인데, 우리말로는 문서 객체 모델이라 할 수 있다. 말 그래도 문서를 → 객체로 바꾼 모델이다. 브라우저는 Javascript 언어만 알아듣는데, Javascript는 HTML의 태그나 속성들을 바로 다룰 수 없기 때문에, 다룰 수 있는 형태인 '객체'로 바꿔주어야 한다. 그래야 브라우저도 HTML 문서를 이해할 수 있게 된다.

 

 

2-2. CSS 파싱, CSSOM 생성

앞서 html 파일을 파싱하다가 <link>, <style> 태그를 만나면 파싱을 잠시 멈추고 리소스 파일을 서버로 요청한다고 언급했었다. 이 태그들은 CSS 파일을 가져올 때 보통 쓰는데, 이렇게 가져온 CSS 파일도 HTML과 마찬가지로 파싱을 한다. 서버에서 받아온 2진수 파일을 문자열로 인코딩하고, 토큰 단위로 나누고, 노드를 생성하고, 트리를 만들고.. 이렇게 파싱해 만든 트리CSSOM 이라고 한다.

눈치 챘겠지만, CSS Object Model의 줄임말이다. 즉, CSS 문서를 객체 모델로 바꾼 것이다. CSSOM을 생성하고 나면, HTML파일은 다시 본론으로 돌아가 파싱을 멈췄던 부분부터 다시 파싱을 시작해 DOM을 마저 생성한다.

 

조금 다른 점은 CSS의 속성은 상속이 되기 때문에, 이를 반영한다는 점이다. 예를들어 ul 이 부모 요소이고, li를 자식요소로 가진다고 생각해보자. 아래 코드처럼 ul는 파란색 속성을 가지고 있는데, 이 속성은 자식 요소인 li도 상속받게 된다. 따라서 li는 상속받은 color:blue와 자신이 가지고 있던 font-size 속성 두 가지를 갖게 된다.

 

ul {
  color : blue;
}
li {
	font-size : 10px;
}

 

 

2-3. 렌더 트리 생성

DOM과 CSSOM은 굉장히 비슷하게 생겼지만, 서로 다른 속성들을 가진 독립적인 트리들이다. 알다시피 HTML은 구조를, CSS는 디자인을 담당하기 때문에 둘을 합치는 작업이 필요하다.

 

렌더 트리는 이름처럼 렌더링을 목적으로 만드는 트리이다. 렌더링은 브라우저가 이제 진짜로 사용자에게 보여주기 위한 화면을 그리는 과정이기 때문에, 보이지 않을 요소들은 이 트리에 포함하지 않는다. 예를들어, DOM에서는 meta태그같은 정보전달 목적의 태그나, CSSOM에서는 display:none으로 보이지 않게 해둔 요소 (정확히는 노드)들은 렌더 트리에서는 제외된다. (단, visibility: hidden은 레이아웃 트리에 포함되니 주의하기!)

 

 

위의 그림처럼 DOM, CSSOM 에 있던 속성들이 합쳐져 렌더트리를 구성하는 것을 확인할 수 있다. 그런데 화면에 보여주는데 쓰인다고 했지만, 렌더트리는 아직까지도 텍스트로 구성된 객체로밖에 보이지 않는다. 실제로 우리가 보는 페이지를 만들기 위해서는 '페인팅'이라는 작업을 거쳐야 한다. 페인팅 작업은 렌더트리의 노드들이 가지고 있는 속성들을 바탕으로 이루어지는데, 이 작업은 잠시 후에 다시 자세히 알아보도록 하고 우선은 Javascript 파일은 어떻게 해석이 되는지부터 잠시 살펴보자.

 

 

3. Javascript 파싱

Step3. 브라우저 JS 엔진은 받아온 JS를 파싱해 AST를 생성하고, 바이트코드로 변환해 실행한다.

 

렌더링 엔진은 HTML 파일을 한줄씩 파싱하며 DOM을 생성하다가 Javascript 코드들 불러오는 <script> 태그를 만날 때도 파싱을 잠시 멈춘다. 그리고나서 src 속성에 적혀있는 파일을 서버에 요청해 받아온다. 이렇게 받아온 js파일도 마찬가지로 파싱을 해야하는데, 이 파싱은 브라우저 렌더링 엔진이 직접하지 않고, Javascript 엔진이 담당하게 된다. 이 때 렌더링 엔진은 JS엔진에게 제어권을 아예 넘겨주기 때문에, HTML 파싱을 멈췄다가 js파싱이 다되면 다시 제어권을 돌려받아 파싱을 다시 시작하는 것이다. (제어권에 대한 이야기는 이 글에서 다룬다)

 

JS엔진은 js파일의 코드를 파싱해서 컴퓨터가 이해할 수 있는 기계어로 변환하고 실행한다. 좀 더 구체적으로 살펴보면, 먼저 단순한 텍스트 문자열인 코드를 토큰 단위로 분해한다. 이렇게 분해된 토큰에 문법적인 의미와 구조가 더해져, AST(추상 구문 트리) 라는 트리가 완성된다. 구체적인 속성은 다르지만, 이전에 봤던 과정들과 비슷해 보인다. 아래 그림에서 맨 왼쪽의 코드가 바로 다음의 트리 구조로 바뀌는 부분이 여기까지의 내용에 해당한다.

 

이제 이렇게 코드를 해석해서 만든 AST라는 트리를 실제로 실행할 수 있도록 만들어야 한다. 코드의 실제 실행은 인터프리터가 담당하는데, 인터프리터가 알아들을 수 있도록 하기 위해서는 AST트리를 바이트 코드라는 중간 수준의 코드로 변환해야 한다. (인터프리터 관련 글) 이 변환은 바이트코드 생성기가 담당해준다. 이제 위의 그림에서 가장 오른쪽에 있는 형태로 바뀌어 받아온 js파일 내용이 실제로 실행된다.

 

 

4. 레이아웃(리플로우)

Step4. 렌더트리를 기반으로 HTML 요소의 레이아웃(위치, 크기)을 계산한다.

 

레이아웃은 요소의 기하학적인 속성들을 찾는 과정이다. 아까 만들었던 렌더트리가 여기서 사용되니 다시 떠올려야 한다. 렌더트리에는 요소들의 위치나 크기와 관련된 정보들이 들어있었다. 하지만 이 정보들은 각 요소들에 대한 정보일 뿐, 전체 화면에서 정확히 어디에 위치할 것인지에 대해서는 아직 알지 못한다. 이런 계산을 하는 단계가 레이아웃 단계이다. 브라우저는 각 요소들이 전체 화면에서 어디에, 어떤 크기로 배치되어야 할 지 파악하기 위해 렌더트리의 맨 윗부분부터 아래로 내려가며 계산을 진행한다. 모든 값들은 절대적인 단위인 px값으로 변환된다.

 

예를들어 우리가 <div>요소 하나만 띄우도록 코드를 작성했고, width를 50%로 지정해두었다면, 이 값은 전체 화면 크기(viewport)의 절반 크기로 계산되고, 절대적인 값인 px 단위로 변환되는 식이다.

 

5. 페인팅

Step5. 화면에 HTML요소를 페인팅한다.

 

이제 위치에 대한 계산도 마쳤으니, 정말로 화면에 보여줄 차례이다. 브라우저 화면은 픽셀이라고 하는 정말 작은 점들로 이루어져 있다. 각각 정보를 가진 픽셀들이 모여 하나의 이미지, 화면을 구성하는 것이다. 따라서 화면에 색상을 입히고, 어떤 요소를 보여주기 위해서는 이 픽셀에 대한 정보가 있어야 한다. 페인팅은 이러한 픽셀들을 채워나가는 과정이다. 따라서 이 과정을 마지막으로 우리는 단순한 텍스트에 불과했던 파일 내용들을 이미지화된 모습으로 브라우저 화면을 통해 볼 수 있게되는 것이다.

추가. 리플로우, 리페인트

리플로우 = 레이아웃 계산을 다시하는 것
리페인트 = 새로운 렌더트리를 바탕으로 다시 페인트를 하는 것

 

만약 사용자가 브라우저 화면을 늘리거나 줄이는 등 크기를 조절하거나, 어떤 버튼을 눌러 화면에 요소가 추가되거나 삭제되는 경우가 생기면, 당연히 화면에 있던 요소들의 위치나 크기 등이 바뀌는 일이 생기게 될 것이다. 굉장히 당연하게 여겨지지만, 이렇게 화면에 나타나는 모습을 바꾸기 위해서는 모든 요소들의 위치와 크기를 다시 계산하고, 다시 그려서 보여주어야 한다. 이렇게 어떤 인터랙션으로 인해 앞서 보았던 레이아웃, 페인팅 과정을 반복하는 것리플로우, 리페인트라고 한다.

 

const makeDiv = document.createElement('div');

 

익숙한 코드일 수도 있을 것 같다. 위의 코드는 Javascript를 통해 DOM을 조작하는 코드이다. DOM은 단순히 HTML 파일의 정보만 담고있게 아니라, 이렇게 Javascript를 통해 요소들을 동적으로 조작할 수 있도록 DOM API라는 것을 제공한다. CSS도 마찬가지로 이런식으로 조작이 가능하다. 이렇게 Javascript 조작으로 변경이 일어나면, DOM트리를 다시 구성하는 것으로 시작해 CSSOM와 합쳐져 새 렌더 트리를 생성한다. 그리고 레이아웃과 페인트 과정을 또다시 거쳐서 화면에 보여진다. DOM 조작은 리플로우, 리페인팅이 일어나는 대표적인 예시라고 할 수 있다.

 

물론 레이아웃과 페인트는 별개의 작업이기 때문에, 하나씩만 발생할 수도 있고, 둘다 발생할 수도 있다. 위치나 크기에 대한 변경만 있다면 레이아웃 작업만 다시하면 되고, 요소의 색상이나 보이는지 여부 같은 스타일에 대한 사항에 대한 변경만 있다면 페인팅 작업만 다시하면 되기 때문이다.

 

마무리

1. 브라우저는 HTML, CSS, JS, 이미지, 폰트 등 리소스를 서버에 요청하고, 응답으로 받아온다.
2. 브라우저 렌더링 엔진은 받아온 HTML, CSS를 파싱해 DOM, CSSOM을 생성하고, 이들을 결합해 렌더 트리를 생성한다.
3. 브라우저 JS 엔진은 받아온 JS를 파싱해 AST를 생성하고, 바이트코드로 변환해 실행한다.
4. 렌더트리를 기반으로 HTML 요소의 레이아웃(위치, 크기)을 계산한다.
5. 화면에 HTML요소를 페인팅한다.

 

맨처음에 미리보기로 보았던 브라우저 렌더링의 전체 과정이다. 브라우저의 랜더링 엔진은 HTML, CSS파일로 DOM과 CSSOM을 생성한다. 이 둘은 렌더트리로 결합되고, 이 렌더트리는 요소들의 크기, 위치를 계산하는 레이아웃 작업과 픽셀을 통해 화면에 실제로 그리는 작업인 페인팅 단계에 활용된다. 여기까지의 과정을 마치면 사용자는 웹페이지를 볼 수 있게 된다.

 

추가로, DOM은 DOM API를 제공하기 때문에, Javascript로 이미 생성된 DOM을 조작할 수 있다는 것도 소개했다. 이런 조작으로 인해 요소들에 변경 사항이 생기면, 앞의 과정을 다시 수행하게 된다. 레이아웃단계를 다시 반복하는걸 리플로우, 페인팅단계를 다시 반복하는걸 리페인팅이라 한다.

 

이렇게 우리는 단순히 주소창에 주소를 검색할 뿐이지만, 그 내부 과정을 살펴보면 굉장히 복잡한 과정이 빠르게 수행되고 있었다는 것을 알게되었다. 이번 글에서는 렌더링이 되는 과정에 집중했는데, 이후 글에서는 브라우저 내부 구조에 대해서도 좀 더 자세히 알아보고자한다.

 

참고

 

반응형

댓글