다른 사람들도 많이 다루는 블로그 단골 주제 ‘브라우저의 작동 원리’ 에 대해서 공부한 내용을 정리한 글입니다.
단순 작동원리에 대해서 정리하기 보다, 제가 ‘왜?’ 라고 생각되거나 궁금한 부분에 대해서 조금 더 공부 후 정리하였습니다.
저는 평소 사과는 왜 떨어지는가에 대해서 궁금증을 잘 품지 않습니다.
브라우저도 마찬가지로 애써 공부하지 않을때는 어떻게 작동하는가에 대해서 궁금증을 품지 않았습니다.
그만큼 일상속에 잘 녹아있고, 사용성이 갈수록 편해져 어떻게 작동하는지에 대해 의문을 품지 않았다고 생각합니다.
하지만 프론트엔드 개발을 하며 브라우저와 제일 친해야 하는게 아닌가 하는 마음에 브라우저가 어떻게 작동하는지에 대해서 알고싶어 공부하게 되었습니다.
공부를 하며 왜 CSR은 SSR에 비해 SEO 최적화가 불리한지에 대해 알게 되었고, 렌더링 과정을 알고나니 제가 만든 결과물이 어떤 과정을 거쳐 화면에 출력되는지 알게되어 최적화 과정에서 여러 방면으로 생각해볼 거리가 생겨 유익했습니다.
주소창에 구글을 검색하게 되면 다음과 같은 과정을 거쳐 저희에게 화면을 보여주게 됩니다.
1. 주소창에 입력된 HostName을 DNS에서 조회하여 IP 주소를 받아옴
2. 받은 IP 주소 웹 서버에 http/https 프로토콜을 이용해 리소스 요청을 보냄
3. 웹 서버로 부터 HTML 문서를 받으면 DOM Tree와 CSSOM을 생성
4. DOM Tree와 CSSOM을 이용해 레이아웃 계산, 그리기를 진행
5. 화면에 출력
어떤 과정이 있는지에 대해 간략하게 풀어써서 생각보다 많은 과정을 거치지 않지만 잘 만들어진 프로그램은 원리 속 숨어있는 디테일이 많다고 생각하여 조금 더 세세한 과정과 개인적으로 궁금증을 자아낸 것 위주로 차례대로 풀어보겠습니다.
💡 거의 대부분의 브라우저는 웹 표준을 따르고 있기 때문에 엔진과 최적화 작업을 제외한 기본적인 작동 방식에 대해서는 거의 똑같게 처리됩니다.
브라우저는 각 핵심 기능을 담당하는 컴포넌트들로 이루어져 있는데, 이 컴포넌트들 먼저 알아보도록 하겠습니다.
흔히 UI로 불리는 부분을 담당하는 컴포넌트 입니다. 이 컴포넌트는 주소창, 홈 버튼, 브라우저 설정 등 브라우저를 켰을 때, 저희가 브라우저에서 할 수 있는 모든 의사소통 할 수 있도록 해주는 역할을 담당합니다.
💡 html로 만들어진 버튼, 입력창 역시 User Interface 컴포넌트에서 사용자의 입력을 감지하여 Browser Engine 으로 요청 합니다.
User Interface와 Rendering Engine / Data Persistence을 이어주는 중간 다리 역할을 하는 컴포넌트 입니다.
이 컴포넌트는 리소스를 받고, 이 리소스를 어떻게 처리할지 결정하게 됩니다.
만약 JS를 처리해야 하면 Javascript Interpreter
로, HTML이나 CSS를 처리해야 하면 Rendering Engine
으로, 네트워크를 처리해야 하면 Networking
컴포넌트로 처리를 명령하게 됩니다.
💡 Browser Engine은 모든 렌더링 과정과 거의 대부분의 네트워크 작업을 하기 전에 거쳐야하는 컴포넌트 입니다.
Parsing, Layout, Reflow, Paint 등
화면에 보여지기 위한 모든 과정을 처리하는 컴포넌트 입니다.
거의 대부분 화면에 보이는 모든 영역을 이 컴포넌트에서 담당합니다.
💡 거의 대부분의 화면을 이 컴포넌트에서 처리되지만 예외가 존재합니다.
- 일반적인 텍스트 (json, txt 등) => Rendering Engine 과정 거침
- 이미지, 비디오 등 => Rendering Engine 과정 거침
- PDF => 브라우저에 내장된 PDF 렌더 모듈을 통해 렌더링
Networking은 브라우저 내에서 웹 서버와 모든 통신을 책임지고, 그 결과물을 다른 컴포넌트에 전달하는 역할을 합니다.
프로토콜 처리
캐시 및 쿠키 관리
Redirection / CORS 정책
Data Streaming
💡 HttpOnly 쿠키는 Storage일까? Networking일까?
쿠키 자체는 Storage에 보관되지만, 자바스크립트(document.cookie)에서 접근할 수 없어 서버의 요청/응답으로만 자동 처리합니다.
따라서 쿠키의 저장은 Storage에서 담당하지만HttpOnly
요청/응답에서 Set-Cookie 해석은 Networking 컴포넌트의 책임이기 때문에 서로 분담하는 형태입니다.
웹 페이지 안의 자바스크립트 코드를 Interpreter 형식(순차적)으로 읽고, 실행하는 역할을 담당합니다.
하지만 단순 읽고, 실행의 역할만 하지 않고 다음의 역할도 같이 진행합니다.
파싱(Parsing) : 자바스크립트의 구문 분석(Parsing)을 하고, 추상 구문 트리(Abstract Syntax Tree)를 생성합니다.
바이트코드 생성(Bytecode Generation) : 추상 구문 트리를 바탕으로 중간 언어(ByteCode)로 변환합니다.
실행(Execution) : 변수 선언, 함수 호출, 이벤트 실행, DOM 조작 같은 동작을 실행합니다.
런타인 환경 제공 : 런타임 환경(Web API, DOM API, Event Loop 등)을 연결해줍니다.
JIT 컴파일러 : 성능 최적화를 위해 자주 실행되는 코드는 JIT(Just-In-Time) 컴파일러로 네이티브 머신 코드로 바꿔줍니다.
브라우저의 UI 요소(버튼, 스크롤바, 입력 등)와 렌더링 엔진이 그려야 할 요소(DOM, CSS, 레이아웃)를 운영체제 UI 시스템과 연결해주는 컴포넌트
UI Backend 컴포넌트가 하는 역할은 구체적으로 다음과 같다.
기본 위젯 제공
<button>
, <input>
같은 Element들을 운영체제의 네이티브 컨트롤로 활용렌더링 추상화
폰트와 텍스트 렌더링
컴포지팅 / 레이어 관리
구글을 입력하기 위해서는 브라우저에 있는 주소창을 입력하는것 부터 시작합니다.
이 과정에서 브라우저는 주소창에 입력된 문자열을 파싱하는데 이 과정에서 어떤 프로토콜(http/https 등)을 사용할지와 어떤 리소스를 요청할지를 인식하게 됩니다.
url에 포함된 도메인으로 DNS의 IP 주소를 조회 합니다.
https://example.com/index.html
위와 같이 url 주소를 입력하면 브라우저는 파싱을 통해, 어떤 프로토콜(https)
을 이용하는지, 어떤 host로 접속
을 하고, 어떤 자산
을 받을지를 결정하고, 서버에 요청을 보낼 준비를 합니다.
💡 어떤 host로 접속할지 결정하게 되면 DNS 서버를 조회해서 IP주소를 받아옵니다.
💡 http/https에서 '://'은 어떤 프로토콜(fpt,http,https 등)을 사용하는지 파싱하기 위해 만든 토큰입니다.
브라우저는 변환된 IP 주소를 이용해 웹 서버에 http/https 요청을 보냅니다.
웹 서버는 요청받은 리소스를 브라우저에 해당 프로토콜 응답으로 돌려줍니다.
💡 만약 이미지, CSS, JS 같은 경우 렌더링 과정 없이 출력 혹은 다운로드를 진행합니다. 하지만 react같은 SPA 애플리케이션의 경우 index를 요청받아 js로 DOM Tree를 구성하기 때문에 JS 파일을 보내는것이 아닌, html을 요청받는 것입니다.
브라우저는 받은 html을 다음과 같이 처리합니다.
1. HTML 파싱
- DOM(Document Object Model) 트리 생성
2. CSS 파싱
- CSSOM(CSS Object Model) 생성
3. 렌더 트리 생성
- DOM Tree와 CSSOM을 합쳐 렌더 트리를 생성하게 됩니다.
DOM Tree 생성 순서
1. HTML 로딩
2. 파싱 -> 토큰화 -> 태그 열고 닫고 -> 노드 생성
3. 트리 구조로 연결
4. DOM 완성
CSSOM 생성 순서
1. CSS 리소스 로드
2. 토큰화
3. 파싱
4. 노드 생성
5. CSSOM 트리 구조화
💡 DOM Tree와 CSSOM이 생성될 때, 일반적으로 DOM Tree가 먼저 생성되지만, 병렬처리되어 거의 동시에 완료됩니다.
<html> <head> <link rel="stylesheet" href="style.css"> </head> <body> <h1>Hello</h1> </body> </html>
<html>
- DOM 생성 시작
<head>
- DOM 계속 구성
<link />
- CSS 파일 다운로드 시작
<body>
- DOM 계속 구성
</html>
- DOM 트리 완성웹페이지는 일반적으로 HTML 문서로 시작되기 때문에 HTML 파싱을 하면서
<style>
태그를 만나야 ref에 있는 stylesheet 파일을 다운로드 하고, CSSOM 생성을 시작합니다.
HTML에 포함된 <script>
태그를 만나면 자바스크립트 엔진이 실행하게 됩니다.
💡
<script>
태그를 만나면 DOM Tree 생성이 중단됩니다.<body> <h1>Hello</h1> <!-- 이 부분 h1 --> <script> document.querySelector("h1").textContent = "Hi!"; </script> <p>World</p> </body>
결과: Hi! World
<head> <script> document.querySelector("h1").textContent = "Hi!"; </script> </head> <body> <h1>Hello</h1> <!-- script가 먼저 실행되고 h1이 렌더링 되기 때문에 에러 --> <p>World</p> </body>
결과: Hello World
Error: script가 실행될때 DOM Tree에 <h1> 태그가 없어서 에러 발생
렌더 트리를 기반으로 각 요소의 위치와 크기를 계산합니다. 폰트크기 등
각 요소를 픽셀 단위로 그리는 단계. 색상 테두리, 글자 등이 이 단계에서 실제로 화면에 표시될 준비를 합니다.
domtree, cssom이 완성되기 전에는 페인팅이 되지 않는데 이것을 rendering blocking이라고 한다.
여러 레이어로 나뉜 요소들을 최종적으로 합쳐서 화면에 출력합니다.
📄 요약
1. 주소창에 URL을 입력한다.
2. 브라우저는 URL에 포함된 Protocol, Host Name, Content를 파싱하고, 분류합니다.
3. DNS에서 Host의 IP주소를 조회합니다.
4. 조회된 IP주소 웹 서버에 어떤 Protocol을 사용하고, 어떤 Content를 받을지 요청을 보냅니다.
5. 서버는 요청을 받고, 요청된 Content를 브라우저에 보냅니다.
6. 브라우저는 받은 Content를 어떻게 처리할지 결정합니다.
7. html을 받았다면 DOM Tree 생성을 시작합니다.
8. DOM Tree를 생성하다가 <style> 태그를 만나면 CSSOM 생성을 시작하고,
<script> 태그를 만나면 잠시 DOM Tree멈추고,
</html> 태그가 나오면 DOM Tree 생성을 끝마치고 Render Tree를 생성합니다.
9. 생성된 Render Tree를 기반으로 각 요소의 위치와 크기를 계산합니다.
10. 우리가 화면에 볼 수 있게 브라우저에 각 요소의 색깔, 테두리 등을 그립니다.
11. Compositing을 통해 여러 레이어로 나뉜 요소들을 최종적으로 합쳐 화면에 출력합니다.