0. 브라우저 렌더링
- 브라우저가 서버로부터 요청해 받은 내용을 브라우저 화면에 그래픽 형태로 표시해주는 작업
- 즉, 브라우저가 서버로부터 HTML,CSS,JavaScript 문서를 전달받아 브라우저 엔진이 각 문서를 해석해 브라우저 화면을 그려주는 것
- ex) 크롬 브라우저에 접속해 주소창에 www.devepeople.kr 을 입력한다면 브라우저는 데프피플 서버로부터 데브피플 사이트에 대한 정보를 받아 브라우저 화면에 데브피플 홈페이지를 그려주게 됨.
1. 브라우저 렌더링 과정
1) 브라우저는 HTML,CSS,JS,이미지, 폰트 등 리소스를 서버에 요청한다.
화면을 구성할때 HTML,CSS,JS로 이루어지는데 이것들은 서버가 가지고 있다. 그래서 서버에게 달라고 요청을 해야한다. So, 브라우저는 HTML,CSS,JS,이미지,폰트를 서버에 요청하고 응답으로 받아와야한다.
(ex: 네이버 화면을 띄워야 하는데 네이버에 대한 정보가 없으니 정보가 있는 주소에 가서 요청을 해야함. 네이버 서버와 통신하여 데이터를 가져와야함)
🔸서버에 달라고 요청은 어떻게 함?
주소창에 직접 입력하거나, 클릭을 통해 해당 웹 페이지에 접근한다
예를 들어, 네이버 페이지에 들어간다고 해보자.
네이버 서버의 주소(ip주소)를 확인하기 위해 사용자(클라이언트)는 DNS 서버에 검색하기 전에 캐싱된 DNS 기록들을 먼저 확인한다. 만약 해당 도메인 (naver.com) 이름에 맞는 IP 주소 (125.209.222.142)가 존재하면, DNS 서버에 해당 도메인 이름에 해당하는 IP주소를 요청하지 않고 캐싱된 IP주소를 바로 반환한다.
인터넷 세상에서는 IP 주소를 기반으로 동작하지만 인간인 우리는 그것을 식별하기 어렵기 때문에 IP주소 대신 문자로 이루어진 도메인주소를 사용한다. 때문에 도메인 주소를 IP주소로 변환해주는 DNS(Domain Name Server)가 필요한 것이다. 이를 수행하는 것이 DNS 서버이다. DNS서버는 도메인 주소에 대응하는 IP 주소를 찾아주는 역할을 수행한다. URL의 host 이름이 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) 서버에서 응답으로 받은 HTML 데이터를 파싱한다 (바이트 -> 문자 -> 토큰 -> 노드 -> DOM)
응답으로 받아온 HTML 데이터는 오직 텍스트로만 이루어져 있다. 텍스트인데..대체 어떻게 화면에 보이게 되는거임?
이 HTML 데이터를 브라우저가 이해할 수 있는 형태로 파싱이 필요하다.
즉, 서버로부터 전송 받은 문서의 문자열을 브라우저가 이해할 수 있는 구조로 변환하는 과정이다.
서버는 브라우저가 요청한 HTML 파일을 읽어들여 메모리에 저장한 다음 메모리에 저장된 바이트(2진수) 를 인터넷을 경유하여 응답한다.
|
서버는 브라우저에게 2진수 형태의 HTML 문서를 응답으로 준다. |
2. 문자열 (Characters) |
응답받은 byte 형태의 HTML 문서를 meta 태그의 charset 에 지정된 인코딩 방식(UTF-8)에 따라 문자열로 변환한다.(ex. UTF-8) 서버는 이 인코딩 방식은 응답 헤더에 담아준다. |
3. 토큰 | 문자열로 변환된 HTML문서를 문법적 의미를 갖는 최소 단위인 토큰(token)으로 분해한다. 토큰 : 언어가 사용하는 기본단어, 구문적으로 의미를 갖는 최소의 단위 |
4. 노드 | 각 토큰을 객체로 변환해, 노드를 생성한다. (DOM을 구성하는 기본 요소) tree구조에서 root노드를 포함한 모든 개개의 개체를 node라고 표현합니다. head, body, title, script, p, h1 등의 태그 뿐아니라 태그안의 텍스트나 속성 등도 모두 node에 속합니다. ✅노드 종류 1. Document Node: 문서 전체를 나타냅니다. 문서의 루트 노드이며, 모든 노드의 부모 노드입니다. 2. Element Node: HTML 요소를 나타냅니다. 시작 태그와 종료 태그 사이에 포함된 텍스트와 속성을 포함할 수 있습니다. 3.Attribute Node: HTML 요소의 속성을 나타냅니다. Element 노드에 속한 자식 노드입니다. 4. Text Node: HTML 문서 내의 텍스트를 나타냅니다. Document Node는 HTML 문서 전체를 나타내고, Element Node는 <div> 요소를 나타냅니다. Attribute Node는 id 속성을 나타내고, Text Node는 <div> 태그 내의 텍스트를 나타냅니다. |
5. DOM | 이 노드들을 계층 형태의 트리구조로 구성하여 DOM 트리 생성! |
3) HTML 마크업을 바탕으로 DOM 트리를 생성한다.
🤔근데 왜 굳이 HTML 파일을 DOM 트리로 힘들게 바꿔야하지?
DOM은 Document Object Model의 줄임말인데, 우리말로는 문서 객체 모델이라 할 수 있다. 말 그대로 문서를 객체로 바꾼 모델이다. 브라우저는 문자열 형태로 된 HTML,CSS,JS를 객체로 변환하여 다루는 것이 편리하다고 판단하고 있다.
1. JavaScript를 사용하여 문서를 동적으로 조작할 수 있다.
문서를 객체로 바꾸면 JavaScript를 사용하여 문서의 요소에 접근하고, 요소의 속성과 내용을 동적으로 변경할 수 있다. 이를 통해 HTML 요소의 위치, 스타일, 콘텐츠 등을 동적으로 변경할 수 있습니다.
var heading = document.getElementById('myHeading');
heading.style.color = 'blue';
2. 문서의 구조와 내용을 접근 가능한 모델로 만들 수 있다.
DOM은 문서의 구조와 내용을 객체 모델로 나타내기 때문에, 스크린 리더(Screen Reader)와 같은 보조 기술을 사용하는 사용자들도 쉽게 웹 페이지에 접근할 수 있다.
3. 웹 페이지의 검색 엔진 최적화(SEO)를 향상시킬 수 있다.
DOM을 사용하면 검색 엔진이 웹 페이지의 구조와 내용을 더욱 잘 이해할 수 있다. 이를 통해 검색 엔진은 웹 페이지를 더 잘 인덱싱하고 검색 결과에서 더 잘 노출시킬 수 있다.
따라서 HTML parser라는 것이 HTML을 DOM이라는 객체 모델로 바꾸어주는 것이다.
4) CSS 마크업을 바탕으로 CSSOM(CSS Object Model) 트리를 생성한다.
앞서 브라우저 렌더링 엔진은 HTML문서를 한 줄 씩 파싱하면서 DOM을 생성한다고 했다.
이 과정에서 CSS 문서를 연결한 <link> or <style> 태그를 만나면 CSS문서를 파싱하여 스타일 규칙을 담고있는 CSSOM 트리를 만들기 시작하는 것이다.
CSS 파일도 HTML과 마찬가지로 파싱을 한다. 서버에서 받아온 2진수 파일을 문자열로 인코딩하고, 토큰 단위로 나누고, 노드를 생성하고, 트리를 만들고.. 이렇게 파싱해 만든 트리는 CSSOM 이라고 한다. CSS Object Model의 줄임말이다.
즉, CSS 문서를 객체 모델로 바꾼 것이다. CSSOM을 생성하고 나면, HTML파일은 다시 본론으로 돌아가 파싱을 멈췄던 부분부터 다시 파싱을 시작해 DOM을 마저 생성한다.
5) DOM트리와 CSSOM트리를 결합하여 렌더 트리를 형성한다
DOM과 CSSOM은 굉장히 비슷하게 생겼지만, 서로 다른 속성들을 가진 독립적인 트리들이. 서로 다른 트리들을 합치는 작업이 필요하다. 렌더 트리는 이름처럼 렌더링을 목적으로 만드는 트리이다. 렌더링은 브라우저가 이제 진짜로 사용자에게 보여주기 위한 화면을 그리는 과정이기 때문에, 보이지 않을 요소들은 이 트리에 포함하지 않는다.
🔸 대표적으로 렌더트리에 포함되지 않는 것들
- DOM에서는 meta태그같은 정보전달 목적의 태그
- CSSOM에서는 display:none으로 보이지 않게 해둔 요소 (정확히는 노드)들은 렌더 트리에서는 제외된다.
- <head> 요소와 같은 비시각적 DOM 요소는 렌더 트리에 추가되지 않는다.
- 하지만 visibility : hidden 속성은 자리는 화면상에서 공간을 차지하는 속성이기 때문에 렌더트리에 반영된다.
6) 렌더트리를 기반으로 HTML 요소의 레이아웃(위치,크기) 를 계산한다.
- 렌더트리의 노드를 화면에 배치하는 과정을 레이아웃이라고 한다.
- 렌더트리 생성이 끝나면 웹페이지 화면 안에서 렌더트리에 있는 각 노드의 위치와 크기, 너비, 높이 등을 계산하고 화면에 배치하는 레이아웃 과정이 실행된다.
브라우저는 각 요소들이 전체 화면에서 어디에, 어떤 크기로 배치되어야 할 지 파악하기 위해 렌더트리의 맨 윗부분부터 아래로 내려가며 계산을 진행한다. 모든 값들은 절대적인 단위인 px값으로 변환된다.
예를들어 우리가 div요소 하나만 띄우도록 코드를 작성했고, width를 50%로 지정해두었다면, 이 값은 전체 화면 크기(viewport)의 절반 크기로 계산되고, 절대적인 값인 px 단위로 변환되는 식이다.
레이아웃은 전체의 배치과정이 필요한 경우인 글로벌 레이아웃, 페이지의 특정 부분만 레이아웃 변화가 일어나는 상황인 로컬 레이아웃으로 구분할 수 있다.
🔸글로벌 레이아웃 예제코드
<style>
body {
margin: 0;
padding: 0;
}
.box {
width: 100%;
height: 100vh;
background-color: red;
}
</style>
<div class="box"></div>
이 예제에서, 브라우저 창의 크기가 변경될 때마다 .box의 너비와 높이가 재계산되어야 한다. 이것은 전체 페이지 레이아웃의 재계산을 유발하는 글로벌 레이아웃.
로컬 레이아웃은 초기 배치 이후 일부 DOM 노드에 변경이 생기는 것처럼, 특정 부분만 재배치가 필요할 때 발생한다.
<style>
.box {
width: 300px;
height: 300px;
background-color: red;
}
</style>
<button onclick="changeSize()">Change size</button>
<div class="box" id="box"></div>
<script>
function changeSize() {
document.getElementById('box').style.width = "500px";
}
</script>
버튼을 클릭하면 .box 요소의 너비가 변경되는데, 이는 .box 요소에 대한 로컬 레이아웃의 변화를 유발한다. 이 경우 다른 요소들에는 영향을 주지 않고 .box 요소만 레이아웃이 재계산된다. 로컬 레이아웃의 변화는 그 영역 내에서만 리플로우가 발생하게 하므로, 전체 페이지의 성능에 미치는 영향을 상대적으로 줄일 수 있다.
초기 배치 이후 요소의 크기나 위치가 변해 다시 계산되어야할때를 리플로우(reflow) 라고 한다.(예를 들어, 요소의 너비나 높이가 변경된 경우, 그 요소의 레이아웃이 변경되며, 이 변경사항은 자식 노드, 부모 노드, 이웃하는 노드 등에 영향을 미치므로 브라우저는 이 모든 것을 다시 계산해야함)
리플로우는 페이지의 성능에 상당한 영향을 줄 수 있다. 왜냐하면 레이아웃 변경에 따라 계산을 재진행하고 화면을 다시 그리는 작업이 많은 자원을 요구하기 때문이다. 따라서 리플로우가 빈번하게 발생하면 페이지의 성능이 저하될 수 있다.
7) 개별 노드를 화면에 페인트한다
브라우저 화면은 픽셀이라고 하는 정말 작은 점들로 이루어져 있다. 각각 정보를 가진 픽셀들이 모여 하나의 이미지, 화면을 구성하는 것이다. 따라서 화면에 색상을 입히고, 어떤 요소를 보여주기 위해서는 이 픽셀에 대한 정보가 있어야 한다. 페인팅은 이러한 픽셀들을 채워나가는 과정이다. 이때, 한꺼번에 하나의 레이어로 화면을 만드는것이 아니라 겹겹의 레이어로 만들어 화면에 그린다. 이렇게 레이어를 분리해두면 다시 Paint 해야하는 일이 발생했을 때 모든 레이어가 아니라 하나의 레이어만 Paint 해도된다는 장점이 있다. 따라서 이 과정을 마지막으로 우리는 단순한 텍스트에 불과했던 파일 내용들을 이미지화된 모습으로 브라우저 화면을 통해 볼 수 있게되는 것이다.
리페인트 : 새로운 렌더트리를 바탕으로 다시 페인트를 하는 것
🔸reflow 와 repaint 의 차이
reflow | repaint |
초기 배치 이후 요소의 크기나 위치가 변해 수치를 다시 계산하여, 렌더트리를 재 생성하는 과정 | 새로운 렌더트리를 바탕으로 다시 페인트를 하는 것 |
브라우저 리사이징 시 (Viewport 크기 변경) 노드 추가 또는 제거 DOM 노드의 위치 변경 DOM 노드의 크기 변경(margin, padding, border, width, height 등..) 요소의 위치, 크기 변경 폰트 변경과 이미지 크기 변경 |
Reflow만 수행되면 실제 화면에는 반영되지 않기 때문에 다시 Painting이 일어나야 한다.이 과정을 Repaint라고 한다. Reflow 가 실행된 순간 뒤에 실행된다. Reflow가 발생하지 않아도 background-color , opacity, visibility, 같이 레이아웃에 영향을 주지 않는 스타일 속성이 변했을 때는 reflow 없이 repaint만 일어난다. |
8) 컴포지트(합성)
- 여러 레이어로 나누어진 픽셀값들을 우리가 실제로 보는 화면처럼 합성해주는 단계이다. 페인팅 단계에서 그려진 레이어들을 순서에 맞추어 합성해 유저가 보는 화면을 만든다.
2. 브라우저 렌더링 과정에서 자바스크립트는 어떻게 동작할까?
- HTML/CSS 파싱 과정과 마찬가지로 렌더링 엔진은 HTML을 한 줄씩 순차적으로 파싱하며 DOM을 생성해 나가다가 자바스크립트 파일을 로드하는 <script> 태그나 자바스크립트 코드를 콘텐츠로 담은 <script> 태그를 만나면 DOM 생성을 일시 중단한다.
- 그리고 <script> 태그의 src 어트리뷰트에 정의된 자바스크립트 파일을 서버에 요청하여 로드한 자바스크립트 파일이나 <script> 태그 내의 자바스크립트 코드를 파싱하기 위해 ② 자바스크립트 엔진에 제어권을 넘긴다. 이후 자바스크립트 파싱과 실행이 종료되면 ① 렌더링 엔진으로 다시 제어권을 넘겨 HTML 파싱이 중단된 지점부터 다시 HTML 파싱을 시작하여 DOM 생성을 재개한다.
- 자바스크립트 파싱과 실행은 ① 브라우저 렌더링 엔진이 아닌 ② 자바스크립트 엔진이 처리한다. ② 자바스크립트 엔진은 자바스크립트 코드를 파싱하여 CPU가 이해할 수 있는 저수준 언어(low-level language)로 변환하고 실행하는 역할을 한다.
🔸자바스크립트 엔진은 JS 를 파싱해 AST 를 생성하고, 바이트코드로 변환해 실행한다.
JS엔진은 js파일의 코드를 파싱해서 CPU가 이해할 수 있는 기계어로 변환(바이트코드)하고 실행한다. 좀 더 구체적으로 살펴보면, 먼저 단순한 텍스트 문자열인 코드를 토큰 단위로 분해한다. 이렇게 분해된 토큰에 문법적인 의미와 구조가 더해져, AST(추상 구문 트리) 라는 트리가 완성된다. 구체적인 속성은 다르지만, 이전에 봤던 과정들과 비슷해 보인다. 아래 그림에서 맨 왼쪽의 코드가 바로 다음의 트리 구조로 바뀌는 부분이 여기까지의 내용에 해당한다.
LHS (left hand side:좌변) 변수는 다른 변수값을 지정하여 저장할 변수를, RHS (right hand side:우변) 변수는 다른 변수에 저장될 변수값에 해당하는 변수를 뜻한다.
이제 이렇게 코드를 해석해서 만든 AST라는 트리를 실제로 실행할 수 있도록 만들어야 한다. 코드의 실제 실행은 인터프리터가 담당하는데, 인터프리터가 알아들을 수 있도록 하기 위해서는 AST트리를 바이트 코드라는 중간 수준의 코드로 변환해야 한다. 이 변환은 바이트코드 생성기가 담당해준다. 이제 위의 그림에서 가장 오른쪽에 있는 형태로 바뀌어 받아온 js파일 내용이 실제로 실행된다.
3. 자바스크립트를 body 태그의 가장 아래에 위치시키는 이유는?
🔸 head 태그 안에 script 태그가 있을 경우
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="style.css" />
<script>
const $apple = document.getElementById("apple");
$apple.style.color = "red";
</script>
</head>
<body>
<ul>
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
</body>
</html>
🔸body 태그 중간에 script 태그가 있는 경우
<script> 태그가 <body> 태그 중간이나 혹은 head 태그 안에 위치할 경우 DOM 트리가 완성되지 않은 상태에서 JS 가 DOM 을 조작한다면 에러가 발생할 수 있다. (script태그를 만나면 DOM 트리 생성을 멈추고 JS를 파싱하기 때문)
동기적인 해석(위에서부터 아래로 차례대로 코드를 실행) 방식 때문에, 첫 번째 스크립트는 DOM에 <div> 엘리먼트가 부착되기도 전에 접근하려 했기 때문이다. 이처럼 DOM의 특정 엘리먼트와 인터렉션 하는 비즈니스 로직이 있다면 문제가 될 수 있다.
스크립트 파일을 읽는 도중에는 DOM 파싱이 멈추기 때문에, 용량이 큰 스크립트 파일을 불러올 때는 스크립트 아래의 HTML 문서가 제대로 보이지 않게 된다. 사용자에게는 마치 웹페이지가 멈춘 것처럼 보일 수도 있기 때문에, 부정적인 경험을 끼칠 수 있다.
4. script 태그의 속성으로 로딩 순서 제어하기(script의 async, defer)
기존에는 <script> 태그를 만나면, 스크립트 파일을 다운로드 하기 위해 HTML 파싱이 멈추었었다. 이는 웹 페이지의 로딩 속도를 느리게 만들 수 있다.
1) async
async 스크립트는 DOM 렌더 과정을 방해하지 않도록 병렬로 로드한다.
브라우저가 HTML 파싱을 하는 동시에 백그라운드에서 스크립트를 불러올 수 있음을 의미한다.
즉 async 속성 적용하면 스크립트를 불러오는 과정에서 DOM 렌더를 차단하지 않도록 보장합니다.
하지만 async 스크립트는 오직 파일을 불러오는 것만 병렬로 실행한다는 것이 중요하다.
파일의 로딩을 마치게 된다면, 그 즉시 DOM 렌더를 멈추고 async 방식으로 불러온 스크립트 파일의 해석을 시작한다.
때문에 async 속성으로 파일을 불러온다고 해도, 스크립트의 해석이 얼마나 오래 걸릴지는 스크립트의 파일의 오버헤드에 달려 있습니다. 따라서 DOM에 접근하는 스크립트를 async 방식으로 불러오는 것은 권장되지 않는다.
이러한 특성 때문에 async 스크립트는 실행 순서가 보장되지 않는다.
위의 예시처럼 불러오는데 서로 다른 시간이 걸리는 async 스크립트가 있다면, 먼저 로드가 되는 스크립트가 먼저 실행된다. 스크립트의 실행 순서를 조정할 수 없기 때문에, 만약 두 스크립트가 서로 의존성이 있다면 제대로 동작하지 않을 수 있다. 따라서, async 속성은 주로 독립적인 분석 도구, 광고 픽셀, 소셜 미디어 위젯 등과 같이 다른 컴포넌트에 의존하지 않고 독립적으로 작동하는 스크립트에 사용됩니다.
2) defer
defer 스크립트 역시 DOM 렌더를 방해하지 않고 병렬로 로드합니다. 하지만 로드가 완료된 후 즉시 그 내용이 실행되는 async 스크립트와는 다르게, defer 스크립트는 모든 DOM이 로드된 후에야 실행됩니다.
실제로 더 빨리 로드되는 스크립트가 있다고 하더라도, 실행은 항상 선언한 순서대로 실행된다. 물론 스크립트 파일을 제외한 DOM 구성이 끝난 이후에 말이다.
이 때문에 기본적으로 DOM의 모든 엘리먼트에 접근할 수 있고, 실행 순서도 보장하기 때문에 가장 범용적으로 사용할 수 있는 속성이다. 또한 스크립트 파일끼리의 의존성이 있는 경우에도 정답이 될 수 있다.
참고자료
https://joooing.tistory.com/entry/rendering
https://wormwlrm.github.io/2021/03/01/Async-Defer-Attributes-of-Script-Tag.html
'프론트엔드 개발' 카테고리의 다른 글
[사용자 인증방식] 세션,JWT,OAuth (0) | 2023.09.08 |
---|---|
XSS, CSRF 위협 (0) | 2023.08.09 |
브라우저 저장소의 차이점 (로컬/세션 스토리지), 쿠키와 세션 (1) | 2023.08.09 |
데이터를 주고 받는 JSON 형식 (0) | 2023.07.05 |
REST API 란 뭘까? (0) | 2023.06.29 |