웹 개발자라면 알아두면 좋을 브라우저의 작동 원리에 대해서 한번 다뤄보자🏃♂️
브라우저의 작동 원리에 대해서 다뤄보기 전에 우선 브라우저, 웹 브라우저란 도대체 무엇이고 무슨 역할을 하는지에 대해서 먼저 알아보자.
사용자가 웹 브라우저를 통해서 어떠한 요청을 전달하면 웹 서버는 해당 요청에 대한 응답을 하게 된다.
이때 웹 서버가 웹 브라우저에게 전달한 응답인 HTML, CSS, JS 파일등을 브라우저가 읽고 사용자가 이해할 수 있는 내용으로 해석한 후 보여주는 역할을 하게 된다.
브라우저마다 조금씩의 차이는 있을 수 있지만 보통은 위와같은 구조로 구성되어 있다. 각 요소들을 설명하면 다음과 같다.
1. UI
브라우저 사용자가 상호작용 할 수 있는 뒤로가기/앞으로가기
, 새로고침
, 주소창
등과 같은 부분을 의미한다.
2. Browser Engine
UI와 Rendering Engine 사이의 동작을 제어, 사용자가 주소창에 무언가를 입력하면 브라우저는 이에 대해 응답을 가져올 수 있도록 Rendering Engine을 제어한다.
3. Rendering Engine
브라우저에서 가장 중요한 역할을 하는, 사용자가 요청한 사이트를 브라우저에 그려주는 역할을 한다.
4. Networking
브라우저가 인터넷에서 리소스를 가져올 때 사용한다. HTTP 요청과 같은 네트워크 호출에 사용된다.
5. JavaScript Interpreter
JavaScript 코드를 파싱하고 실행한다.
6. UI Backend
콤보박스, 윈도우 등 우리가 직접 만들지는 않았지만 이미 브라우저에 존재하는 UI들을 그려주는 역할을 한다.
7. Data Persistence
쿠키와 같은 로컬 데이터를 저장하는 역할을 한다.
위에서 언급했듯이 Rendering Engine (렌더링 엔진)은 브라우저에서 가장 핵심적인 역할을 하며, 그 역할은 요청 받은 내용을 브라우저에 그려주는 것이다.
브라우저마다 사용하는 렌더링 엔진이 다른데 가장 대표적인 엔진은 다음과 같다.
브라우저 | Rendering Engine |
---|---|
파이어 폭스 | Gecko |
크롬, 사파리 | Webkit |
브라우저마다 렌더링 엔진이 다르므로 세부적인 과정은 다르겠지만 기본적인 단계는 다음과 같다.
1. HTML 파싱 후 DOM Tree 만들기 2. 렌더 트리 (Render Tree) 만들기 3. 렌더 트리 (Render Tree) 레이아웃 만들기 4. 렌더 트리 페인팅 (Render Tree Painting)
렌더링 엔진은 우선 전달받은 HTML을 파싱(Parsing) 하여 각 요소들을 DOM Tree의 각 DOM 노드 들로 전환한다.
⚡
DOM(Document Object Model) : HTML 태그를 Object 모델의 형태로 바꿔 놓은 것
Parsing : 브라우저가 코드를 이해하고 사용할 수 있는 구조로 변환하는 것
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>
만약 위와 같은 코드를 DOM Tree로 전환하게 되면 다음과 같이 생성된다.
사진출처
DOM Tree를 생성 한 후 렌더링 엔진은 CSS/Style 데이터를 파싱하여 CSSOM Tree (<link>
, <style>
를 통해)를 생성한다. 그 두개의 Tree를 합쳐서 Render Tree를 생성하게 된다.
즉 DOM Tree는 브라우저에 나타나게 될 내용 (Content) 그 자체를 나타낸다면, Render Tree는 그 내용을 어떤 색상, 어떤 스타일 (Style) 등으로 나타낼지 또한 정하게 된다.
Layout을 만든다는 것은 각 노드들에게 화면의 어느 공간, 어느 장소에 위치해야 할지에 대한 각각의 값 (Position, Size)을 부여하는 것을 의미한다.
Render Tree가 만들어지고 Layout까지 구성이 되었다면 UI Backend가 동작하여 각 노드들을 정해진 스타일 및 위치 값대로 화면에 배치하게 된다.
<script>
태그를 만난 HTML 파서HTML파서는 <script>
태그를 만나게 되면 DOM 생성을 중지하고, JavaScript 코드를 처리하는 JavaScript Engine에게 제어 권한을 넘기게 된다.
제어 권한을 받은 JavaScript Engine은 <script>
태그 내의 코드 혹은 script의 src attribute에 정의 된 js파일을 로드하고 컴파일하여 실행하게 된다.
JavaScript 실행이 종료되면 다시 제어 권한을 HTML 파서에게 넘기고, HTML 파서는 아까 중지했던 DOM 생성을 그대로 이어서 진행하게 된다.
이러한 특징 때문에 DOM 생성 지연을 줄이기 위해 보통은 <body>
의 가장 아래에 <script>
태그를 작성하는 방식을 이용한다.
이 방식을 이용하면 HTML 요소들의 렌더링이 지연되지 않기 때문에 화면 로딩이 줄고, DOM이 아직 다 생성되지 않은 상태에서 JS가 DOM을 조작하면 생기는 에러를 방지할 수 있다는 장점이 있다.
만약 script 파일이 위와 같은 방식이 아니라 <head>
에 삽입이 되는 경우에는 async, defer 속성을 사용하여 처리 시점을 설정할 수가 있다.
async 속성을 사용할 시에는 브라우저에 스크립트 파일이 비동기적으로 실행될 수 있음을 나타내게 된다.
위의 그림을 보면 HTML 파싱과 동시에 Script 파일이 다운로드 되고 다운로드가 끝나게 되면 실행이 되는 것을 확인 할 수 있다.
일반적인 방식과 비교하면 Script를 다운로드 하는 시간 만큼의 공백을 줄일 수 있다.
defer 속성을 사용할 시에는 async를 사용할 때와 같이 HTML 파싱과 동시에 Script 파일을 다운로드 한다. 하지만 async와는 다르게 다운로드가 끝났음에도 실행은 하지 않고 기다렸다가 HTML 파싱이 끝나면 실행하는 것을 확인 할 수 있다.
1. <script>
의 위치
만약 <script>
가 <body>
의 가장 아래에 위치해 있으면 굳이 async나 defer 속성을 사용할 필요가 없다.
2. Script 파일의 의존성 여부
다른 파일들과 딱히 교류를 할 필요가 없는 Script 파일의 경우에는 파일의 실행 지점을 정확하게 알 필요가 없기 때문에 async 속성을 사용하는 것이 좋고,
다른 파일들과 활발한 교류를 통해 실행되는 Script 파일의 경우에는 defer 속성을 사용하는 것이 좋다.