나는 개발을 할 때, 내가 개발하고 있는 서비스를 폭넓게 이해한 상태에서 실사용자의 입장을 생각하며 개발을 하는 사람이 좋은 개발자라고 생각한다.
개발을 공부하며 내가 무언가 만들 수 있다는 사실에 재미를 느껴서 정신없이 만들기만 했었는데, 실제로 서비스의 사용자 경험을 고려했을 때
웹페이지를 좋은 사용자 경험과 함께 제공하기 위해서는 브라우저의 작동 방법을 알고 있는 게 좋겠다는 생각이 들었다.
여러 곳에서 찾아본 브라우저가 작동하는 방식은 지금 내가 이해하기 너무 어렵고 오래 걸릴 것 같아서 이번 글에서는 대략적으로 내가 알게 된 정보와 이해한 부분을 정리하려고 한다.
🌎 브라우저는 웹에서 사용자에게 필요한 정보를 출력해주는 GUI 기반 응용 소프트웨어로, 주요 기능은 사용자가 선택한 자원을 서버에 요청하고 응답받은 자원을 브라우저에 표시하는 것이다.
자원은 주로 HTML 문서이고 PDF나 이미지 혹은 다른 형태일 수도 있다.
브라우저는 W3C(World Wide Web Consortium)에서 정한 HTML, CSS 명세에 따라 HTML 파일을 해석해서 표시하게 된다.
브라우저에서 화면을 표시하기까지의 과정은 대략적으로
🔍
사용자 인터페이스를 통해 요청 =>
브라우저 엔진에서 요청 확인 =>
렌더링 엔진에서 파싱 진행 및 렌더 트리 구축 =>
렌더 트리를 참조하여 화면에 배치
🔍
이런 식으로 이루어진다고 이해하면 될 것 같다.
이 중 렌더링 엔진은 서버에서 전달받은 파일들을 해석하고, 화면에 보일 트리를 구축하는 역할을 맡고 있기 때문에 브라우저 핵심이라고 볼수 있다.
그렇기 때문에 렌더링 엔진에 대한 글이 가장 길 것이다...! 😜
브라우저의 구조
- 사용자 인터페이스: 주소 표시줄, 이전/다음 버튼, 북마크 메뉴 등. 요청한 페이지를 보여주는 창을 제외한 나머지 모든 부분이다.
(ex. 주소창, 뒤로 가기, 앞으로 가기, 새로고침, 북마크, 환경설정 등 ) - 브라우저 엔진: 사용자 인터페이스와 렌더링 엔진 사이의 중재자 역할을 한다.
만약 사용자 인터페이스 레이어의 새로고침 버튼을 눌렀다면 브라우저 엔진은 이를 이해하고 새로고침 명령을 수행하게 된다. - 렌더링 엔진: 요청한 콘텐츠를 표시. 예를 들어 HTML을 요청하면 HTML과 CSS를 파싱 하여 화면에 표시함.
- 통신: HTTP 요청과 같은 네트워크 호출에 사용됨. 이것은 플랫폼 독립적인 인터페이스이고 각 플랫폼 하부에서 실행됨.
- UI 백엔드: 콤보 박스와 창 같은 기본적인 장치를 그림. 플랫폼에서 명시하지 않은 일반적인 인터페이스로서, OS 사용자 인터페이스 체계를 사용.
- 자바스크립트 해석기(자바스크립트 엔진): 자바스크립트 코드를 해석하고 실행.
- 자료 저장소: 이 부분은 자료를 저장하는 계층이다. 쿠키나 로컬 스토리지, 세션 스토리지, indexedDB, 웹 SQL, 파일 시스템 등에 접근하고 데이터를 저장하는 데 사용된다.
UI 백엔드 부가설명
기본적인 UI 장치.
예를 들어, <Button>이나 <Input> 태그를 쓸 때, 이 태그에 관한 스타일을 따로 적용하지 않아도 브라우저는 이에 맞는 UI 화면을 그리게 된다.
이러한 요소들은 해당 서버나 플랫폼에 명시하지 않은 일반적인 인터페이스이며, OS 사용자 인터페이스 체계를 사용한다.
렌더링 엔진
렌더링 엔진은 전달받은 자원들을 브라우저의 화면에 표시하는 역할을 하게 된다.
렌더링 엔진에서 일어나는 작업은 크게 4가지로 나눌 수 있다.
- 파싱
- 렌더 트리 구축
- 레이아웃
- 페인트
렌더링 엔진은 여러 가지가 있지만 대표적으로
- 파이어폭스: Gecko
- 사파리: Webkit
- 크롬: Blink
등이 있다.
자바스크립트는 렌더링 엔진이 아닌, 자바스크립트 해석기(자바스크립트 엔진)에서 별도로 해석된다.
📍 파싱
파싱은 파서 생성기를 통해 생성된 파서를 사용하여 최소 단위로 나눠진 코드(토큰화)를 구조화하는 과정이다.
ex) <div></div> (토큰화)=> ['<','div','>','</','div','>']
파서는 어휘 분석과 구문분석을 통해 파싱의 과정을 진행하게 된다.
어휘 분석
의미 없는 공백과 줄 바꿈을 제거하고 토큰(의미 있는 문자) 단위로 분해하는 과정이다.
구분 분석
언어의 구문 규칙을 적용하는 과정이다. 어휘 분석기로부터 새 토큰을 받아서 구문 규칙과 일치하는지 확인한다.
HTML 파싱
HTML은 각 렌더링 엔진 내에 존재하는 HTML파서를 통해 파싱의 과정을 거치게 된다.
HTML 파싱 과정
1. 변환(Conversion): HTML의 원시 바이트(raw bytes)를 읽어와 해당 파일에 지정된 인코딩(UTF-8 등…)에 따라 문자열로 변환하는 과정
2. 토큰화(Tokenizing): 문자열을 W3C HTML5 표준에 따라 고유 토큰(<html>, <body>등, 꺽쇠 괄호로 묶인 문자열)으로 변환하게 된다. 각 토큰은 특별한 의미와 고유한 규칙을 가진다.
3. 렉싱(Lexing): 토큰을 해당 속성 및 규칙을 정의한 객체(Nodes)로 변환
4. DOM 생성(Dom construction): HTML은 상위-하위 관계로 정의할 수 있어, 트리 구조로 나타낼 수 있다. 렉싱 과정을 거쳐 생성된 노드들을 트리 구조로 변환한다.
브라우저는 토큰화 된 HTML 문자열을 이용하여 파스 트리(Parse tree)를 생성한 후, 생성된 파스 트리를 이용하여
DOM(Document Object Model) Tree를 구축한다.
파스 트리: 토큰화 된 문자열을 단순하게 구조화한 트리
DOM 트리: 실제로 상호작용할 수 있는 HTML 엘리먼트로 이루어진 트리
HTML파서는 여러 가지의 이유에서 다른 파서와는 다른 특징을 가지고 있다.
- forgiving nature(오류에 너그럽다)
- 파싱 중단
- Reentrant(재시작)
대부분의 프로그래밍 언어는 문맥 자유 문법(Context-free grammar)에 의해 정의되지만, HTML의 경우 DTD(Document Type Definition)에 의해 정의되기 때문에 명세된 규칙을 따라주어야 한다.
DTD(Document Type Definition): SGML계열 언어의 정의
SGML(Standard Generalized Markup Language): 국제 표준 마크업 언어
1. forgiving nature(오류에 너그럽다)
<!--잘못 작성한 코드-->
<body>
<p class=highlight>Hello
<div><span>World
<!--브라우저 결과-->
<body>
<p class="highlight">Hello</p>
<div><span>World</span></div>
</body>
위의 예제처럼 브라우저가 스스로 HTML파싱 과정에서 발생한 오류를 수정하게 되는데, 이러한 예제를 통해 '오류에 너그럽다' 라는 의미를 알 수 있다.
2. 파서 중단
HTML파서는 파싱 도중 <script> 혹은 <link>와 같은 외부 태그를 만나게 되면 파싱을 중단하고 해당 태그에 대한 해석을 시작한다.
(이러한 이유 때문에 script태그를 body태그 마지막에 삽입하는 것이 좋다.)
파싱을 중단하지 않도록 설정하기 위한 속성으로 defer와 async가 있다.
defer와 async의 차이
먼저 받아오는 대로 해석이 가능한 HTML과 달리 외부 콘텐츠는 증분적으로 해석이 불가능하기도 하고, script 태그 같은 경우는 직접적으로 DOM을 수정하는 코드를 포함하고 있을 수도 있기 때문에 HTML에 대한 파싱을 끝내기 전에 외부 태그를 먼저 해석하게 된다.
3. Reentrant(재시작)
HTML파싱 도중 외부의 요인으로 인해 DOM이 추가, 변경, 삭제될 경우, HTML은 처음부터 다시 파싱을 시작하게 된다.
📌 이 같은 특성들로 인해 HTML은 다른 언어의 파싱과는 다른 예외를 처리해주기 위해, 렌더링 엔진에 따로 HTML파서가 필요하다. 📌
CSS 파싱
CSS파싱은 보통 HTML 문서 내에 CSS를 링크하는 코드가 삽입되어 있기에 HTML파싱 도중에 시작된다. (파싱의 과정은 HTML파싱 과정과 동일하다.)
받아오는 순서대로 해석이 가능한 HTML과 달리 CSS는 전체 파일을 받아오기 전까지는 파싱을 시작할 수 없다.
파싱이 끝나게 되면 DOM트리와 같은 CSSOM(CSS Object Model) Tree를 생성한다.
📍 렌더 트리 구축
렌더링 엔진은 파싱의 과정을 거친 후에, 생성된 DOM 트리와 CSSOM 트리를 기반으로 렌더 트리를 구축하게 된다.
렌더 트리 구축 단계
- DOM 트리의 루트에서 시작하여 화면에 표시되는 노드 각각을 탐색
- 화면에 표시되지 않는 일부 노드들(script, meta 태그 등..)은 렌더 트리에 반영되지 않는다.
- CSS에 의해 화면에서 숨겨지는 노드들은 렌더 트리에 반영되지 않는다.
(위 그림에서 span 노드의 경우 display:none이 설정되기 때문에 렌더 트리에 반영되지 않는다.)
- 화면에 표시되는 각 노드에 대해 적절하게 일치하는 CSSOM 규칙을 찾아 적용합니다.
- 화면에 표시되는 노드를 콘텐츠 및 계산된 스타일과 함께 내보낸다.
화면에 표시되지 않는 태그들(meta, head 등)과 css 속성으로 인해 표시되지 않는 노드들은 렌더 트리에 포함되지 않는다.
(dispaly: none 속성은 렌더 트리에 포함되지 않지만 visibility:hidden 속성은 렌더 트리에 포함된다)
📌 즉 DOM 트리와 렌더 트리는 1:1 매칭이 아니다 ! 📌
📍 레이아웃
아직 안 끝났다. 😭
렌더 트리를 구축했으니!
이제 페이지에 보여질 각 노드들의 레이아웃을 잡아주어야 한다.
이 과정은 HTML의 루트 객체에서 재귀적으로 실행하며, 노드들의 크기와 레이어 순서 등을 좌표로 나타나게 되는데, 계산 범위에 따라
전역적 레이아웃(Global Layout)과 증분적 레이아웃(Incremental Layout)으로 나눌 수 있다.
전체적인 레이아웃은 전역적 레이아웃을 통해 계산된다.
전역적 레이아웃은 모든 렌더 트리 노드들에 대해 계산을 실행하기 때문에, 사소한 스타일 변경에도 이러한 작업이 일어나게 된다면 불필요한 자원의 소모가 크다.
그렇기 때문에 브라우저에서는 자체적으로 더티 비트 시스템(Dirty bit system)이라는 로직을 적용하고 있다.
더티 비트 시스템은 특정 엘리먼트의 레이아웃이 변경되었을 때, 전체가 아닌 변경된 부분만 다시 계산하는 최적화 방법이다.
증분적 레이아웃은 더티 비트 시스템을 이용하여 변경된 노드들에 대한 계산을 비동기로 일괄 처리하게 된다.
📌 아주 복잡한 레이아웃의 경우에는 브라우저 단에서의 최적화만으로는 충분하지 않다.
그렇기 때문에 프론트엔드 개발자는 DOM의 레이아웃과 관련된 값을 직접 읽어오거나, 변화를 주는 JavaScript 코드를 작성해야 한다면, 그러한 구문들을 최대한 묶는 것으로 레이아웃 과정의 연산을 최소화하도록 신경을 써야 한다. 📌
📍 페인트
말 그대로 레이아웃 계산을 마친 요소들의 값대로 화면에 배치하고 스타일을 입혀주는 작업이다.
페인팅에는 순서가 있는데, CSS 페인팅 명세에 따르면 z-index가 낮은 순서대로 페인팅 작업이 진행되며
블록단위에서는
- background-color
- background-image
- border
- children
- outline
순서대로 진행된다.
지금 여기 정리해 놓은 글을 내가 전부 다 기억하고 있지는 않다.
하지만 이렇게 시간을 들여서 정리를 해놓으니 전체적인 브라우저가 작동하는 틀과 순서의 개념이 어느 정도 잡힌 것 같다.
확실히 투자할 만한 가치가 있는 시간이었다고 생각하고, 계속해서 개발을 하며 좀 더 세세하고 코어한 부분까지 알게 될 것이라고 확신한다.
나는 오늘도 성장했다. 😁
참고
https://d2.naver.com/helloworld/59361
https://helloinyong.tistory.com/286
https://beomy.github.io/tech/browser/browser-rendering/
'개발 > WEB' 카테고리의 다른 글
TIL #29 CORS(Cross-Origin Resource Sharing) (0) | 2021.06.05 |
---|---|
TIL # 27 HTTP 통신 (0) | 2021.05.24 |
SSR, CSR (0) | 2021.03.21 |
TIL #12 인증 & 인가 (0) | 2021.03.09 |
서비스 워커(Service Worker) (0) | 2021.02.14 |