오늘은 웹 기초 공부의 첫 번째로 브라우저가 코드를 해석해서 화면에 표시하는 과정을 공부해서 정리해보려고 한다. 이 글은 네이버 D2에서 번역한 이스라엘 개발자 탈리 가르시엘(Tali Garsiel)의 글을 내가 이해한 대로 재구성하고 설명하는 것이다. 아래에 원문링크, 네이버 번역 링크를 남긴다.
P.S. 아래 해석과 설명은 온전히 본인이 글을 읽고 이해한 바를 바탕으로 재구성한 것이다. 예제들은 조금씩 수정하였다. 본문의 일부 설명들은 생략하였다.읽으면서 설명이 매우 어렵게 다가왔다. 따라서 잘못 이해한 부분이 있을 수도 있다. 그런 경우에는 댓글을 남겨주시면 바로 수정할 예정이다.
윈문링크 : https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/
네이버 번역 링크 : https://d2.naver.com/helloworld/59361
브라우저는 무엇인가?
HTML 문서와 그림, 멀티미디어 파일등 월드 와이드 웹을 기반으로 한 인터넷의 컨텐츠를 검색 및 열람하기 위한 응용 프로그램의 총칭. - 나무위키 -
대부분의 사람들은 브라우저가 무엇인지 이미 경험으로 알고 있다. 인터넷을 사용하고 싶을 때 켜는 것이 인터넷 브라우저이다. 본문(네이버 D2의 번역글을 말한다, 이하 본문이라고 칭한다)에서는 브라우저의 기능을 아래와 같이 설명하고 있다.
사용자가 선택한 자원을 서버에 요청하고 브라우저에 표시하는 것
"사용자"는 당연히 우리를 의미하고 "선택한 자원"은 우리가 보고 싶어하는 웹 사이트를 말한다. "서버에 요청"한다는 것은 인터넷 주소(URL이라 칭하는 것, 최근에는 URI라고 많이 부른다)를 입력하고 엔터를 누르는 것이다. 그러면 서버라는 곳에서 뚝딱뚝딱 처리해서 원하는 자료를 찾아다 전달해주면 그것을 화면에 표시하는 것이 "브라우저"이다.
서버에서 받은 자료는 다양한 형태일 수 있지만, 기본적으로 HTML과 CSS, JavaScript 파일를 전제하고 이야기를 진행한다. 참고로 W3C(웹 표준화 기구, World, Wide Web Consortium)라는 곳에서는 서버에서 받은 이러한 자료들을 어떻게 해석해야 하고, 표시해야 하는지 표준을 만들어 놓았다. 대부분의 브라우저는 이 표준을 중심으로 자료를 해석해서 화면에 표시한다.
아래에는 서버에서 뚝딱거리는 과정이 있었다는 전제하에, 전달 받은 자료를 브라우저가 어떻게 화면에 나타내는지에 대한 설명이다.
브라우저의 구성
-
사용자 인터페이스
→ 브라우저 창 안에 있는 주소 입력 창이라던지, 북마크 버튼, 뒤로가기, 앞으로가기 버튼 등을 말한다.
-
브라우저 엔진
→ 사용자 인터페이스와 아래 설명할 렌더링 엔진 사이의 상호작용을 조절하는 곳이다.
-
렌더링 엔진
→ 통신으로부터 자료를 받아와서 화면에 표시하는 역할을 담당한다.
-
통신
→ HTTP 요청 등을 처리한다. 서버에서 자료를 받아오는 역할이다.
-
JS 해석기
→ 말 그대로 JavaScript를 해석하고 실행하는 곳이다. 우리가 브라우저 콘솔에서 JS 코드를 입력하면 잘 돌아가는 것은 JS 해석기 덕분일 것이다.
-
UI 백엔드
→ 운영체제의 인터페이스 시스템을 이용해 기본적인 요소를 구성한다고 한다. 잘 이해가 가지 않는다. 추측컨데 아마도 기본적으로 어떤 프로그램을 실행하면 볼 수 있는 맨 위 오른쪽 닫기 버튼 같은 것을 만드는 것과 같이 간단하고 기본적인 UI를 만드는 것이 아닐까 생각해본다.
-
자료저장소
→ 브라우저 콘솔 창에서 쓸 수 있는 localStorage와 같은 것을 의미한다. 즉, 자료를 저장해야 될 때, 굳이 매번 서버까지 가지 않아도, client(사용자 측, 즉 브라우저)측에서 정보를 저장하고 사용할 수 있다는 것이다.
위에서 설명된 것 중, 본문은 특히 렌터링에 관한 설명을 자세히 한다.
렌더링 엔진의 작동흐름을 알아보자
위에서 설명했듯이 렌더링 엔진은 통신으로 받아온 자료를 해석해서 화면에 표시하는 것을 담당한다. 브라우저마다 사용하는 렌더링 엔진이 다르다. 파이어 폭스는 Gecko, 사파리와 크롬에서는 Webkit이라는 렌더링 엔진을 쓴다.
렌더링 엔진의 전체 작업과정은 다음과 같다.
P.S. 이번 공부를 통해 DOM이라는 것이 무엇인지, 어떻게 만들어진 것인지 더 잘 이해할 수 있었다.
아래에서는 파싱부터 그리기까지 순서대로 설명한다.
파싱(parsing)이란
파싱이란 어떤 데이터를 다른 형태로 가공하는 것을 말한다. HTML 파서, CSS 파서의 파서(parser)는 이런 파싱 과정을 처리하는 과정을 말한다. 쉽게 말하면 HTML파일과 CSS파일을 받아서 새로운 형태, 더 정확히는 "브라우저가 다룰 수 있는 형태"로 바꾸는 것을 말한다.
보통 노드(node, 트리 구조의 데이터에서 하나의 항목을 말한다) 트리 형식으로 표현된다.
파싱 = 어휘분석 + 구문분석
어떤 문서를 파싱해서 파싱트리(파싱의 결과물, 노드 트리 형식이다)를 만들려면 어휘분석과 구문분석을 거쳐야 한다.
하나의 영어문장에 비유하자면, 어휘분석은 각 단어로, 품사로 쪼게는 과정이고, 구문분석은 어휘분석이 넘겨준 조각들을 영어 문법에 맞게 분석하는 것이다. 이 때, 어휘분석 과정에서 쪼개진 것들을 토큰이라고 부른다. 어휘분석에서 문서에 포함된 의미없는 공백, 줄바꿈 등은 제외된다.
HTML 파싱과 DOM(Document Object Model) 트리
HTML 파싱은 실제로 예시를 보면 좀 더 확실히 이해할 수 있다.
<html>
<head>
<title>Swimmingkiim's Blog</title>
</head>
<body>
<h1>Welcome^^</h1>
<p>This is Swimmingkiim's Blog!</p>
</body>
</html>
위와 같은 코드를 파싱한다고 생각해보자. 이 html 코드를 파싱트리 형태로 표현하면 다음과 같다.
HTML 문서를 파싱하면 다음과 같은 모양의 트리 객체가 생긴다. DOM(Document Object Model)이라는 단어를 여기서 더 잘 이해할 수 있게 된다. DOM은 문서를 객체화한 것이다. HTML 파싱의 결과인 것이다.
DOM은 마크업(HTML)과 대칭 관계이다. HTML의 어떤 위치에 어떤 요소가 있다면 DOM에도 같은 위치에 같은 요소를 대표하는 노드가 있다.
HTML은 파싱하기 어려운 언어이다. 언어 자체의 속성도 너그러운 면이 있고, HTML 코드를 기괴하게 써도 브라우저는 이를 알아서 잘 해석해 화면에 표시하는 경향이 있기 때문이다. 예외가 많고, 오류도 너그럽게 받아들여서 규칙과 해석 알고리즘을 세우기 어려운 것 같다.
HTML 파일을 DOM 트리 형식으로 파싱하는 알고리즘을 표현하면 다음과 같다.
통신을 통해서 HTML 코드를 받는다. 그러면 순서대로 토큰화를 진행하면서 토큰들을 트리구축으로 넘겨준다. 트리 구축 단계에서는 계속 넘어오는 토큰들을 분석하여 DOM으로 만든다. 이 때, <html>, 즉 시작 태그가 들어오면 상태처리기계라는 것이 "아, 태그가 시작되었구나"하고 상태를 업데이트한다. 그리고 html토큰을 발행한다. </html> 태그를 만날 때까지 "스택"이라는 곳에 머문다. 스택에 머물 때도 DOM에는 이미 html 노드가 추가된 상태이다. </html>종료 태그를 만날 때까지 스택에서 기다리다가, 종료 태그를 만나면 스택에서 빠져나온다. 간혹 코드를 잘못 써서 태그를 열기만 하도 닫지 않는 경우가 있다. 이럴 때는 스택이 그것을 분석하고, 알아서 닫아준다. 이상하게 중첩된 태그(예를 들어 <p><span></p></span>)들도 스택이 알아서 중첩을 풀어 말이 되는 코드로 바꿔준다.
파싱이 끝나면 파싱트리를 통해 브라우저가 문서와 상호작용을 할 수 있게 된다. 문서의 상태는 "완료"로 바뀌게 되고 JS의 load 이벤트가 발생하는 것도 이 시점이다.
CSS 파싱
남은 건 CSS와 JS 파일(스크립트 파일)이다. 여기에도 우선순위가 있다. CSS가 먼저 파싱되고, 그 다음 스크립트 파일이 파싱된다. 왜냐하면 CSS가 파싱되지 않은 상태에서(즉, 브라우저가 스타일을 이해하지 못한 상태에서) 스크립트를 다루다 보면 잘못된 결과가 도출될 수 있기 때문이다.
아래와 같은 CSS코드가 있다고 해보자.
p, div {
margin: 1em;
}
a {
color: black;
}
위 코드를 파싱트리로 표현하면 다음과 같다.
두 가지를 잘 비교해보면 HTML 파싱과 마찬가지로 대칭을 이루고 있다는 것을 알 수 있다.
스크립트 파싱
HTML5에서는 스크립트 파싱을 비동기로 처리하는 속성이 추가되었다고 한다. 즉, 스크립트를 실행하는 동안 다른 스레드는 계속 문서의 나머지 부분을 파싱하고 있는 것이다.
본문에서 HTML이나 CSS처럼 구체적인 예시는 들지 않았다. 하지만 두 가지를 파싱한 것과 비슷한 논리로 JS도 파싱할 것이라고 상상해 볼 수 있다. 구체적인 상상은 너무 복잡할 것 같아서 넘어가겠다.
렌더트리란 무엇인가
HTML 파싱을 통해 DOM트리가 구축되는 동안 동시에 브라우저는 렌더 트리라는 것을 만든다. 렌더 트리는 간단히 이해하자면 DOM 트리와 CSS 파싱으로 나온 스타일 규칙을 합친 결과물이다. 이 렌더 트리와 아래에서 설명할 배치(위치를 다루는 부분)를 합치면 완성된 도안이 나온다. 그 후는 화면에 그 도안을 그리는 과정이다.
렌더 트리에는 CSS가 가미되기 때문에 DOM 트리와는 완전한 대칭 관계가 아니다. 예를 들어 DOM 트리에는 display: none; 인 노드도 포함되지만 렌더 트리로 변환되는 과정에서 이 노드는 포함되지 않는다. 마찬가지로 head태그도 시각적 요소가 없기 때문에 제외된다. 위치나 개수가 바뀌는 것들도 있다. 예를 들어 select 태그는 드롭다운과 버튼 등으로 나뉠 수 있기 때문에 3개의 렌더러가 된다. float나 position: absolute;는 원래 자리에 자리표시자가 남겨지고, 실제로는 다른 위치로 가게 된다.
Style sheet를 계산하는 과정
렌더 트리를 만들려면 CSS적 요소를 계산해서 각 렌더 객체에 적용해야 한다. 사실 여기는 복잡해서 이해가 잘 안 간다. 앞으로 더 조사해서 채워가려고 한다.
다만 내가 지금까지 이해한 것을 대략적으로 설명하자면 (파이어 폭스의 경우) 스타일 시트는 규칙 트리와 문맥 트리라는 것으로 나뉜다. 규칙 트리는 CSS 코드를 타고 타고 내려가는 트리 형식으로 만든 것이고, 문맥 트리는 DOM 노드와 그것이 가리키는 CSS 규칙 트리의 노드 정보를 포함한 트리이다. 웹킷의 경우 규칙트리가 없다. 따라서 파이어 폭스의 방법보다 더 많은 탐색을 해서 적절한 CSS요소를 찾아낸다. 파이어 폭스의 방법이 탐색 횟수 측면에서 더 효율적인 이유는 다음과 같이 이해했다.
파이어 폭스는 미리 사다리 타기 게임 경로를 계산해서 작성해 놓은 것이고, 웹킷의 경우 그런 경로 분석이 없이 탐색하는 것이다. 규칙 트리라는 사다리 도안이 있어서 어떤 경로로 들어온 input(특정 조건)은 다른 결과들을 탐색할 필요 없이 주어진 사다리 경로 끝에 있는 결과를 채택하면 되는 것이다.
구조체 이야기도 나오고 좀 복잡했지만, 대략적으로 나는 이렇게 이해했다.
어떤 선택자에 대한 선언이 여러 스타일 시트에 등장할 수 도 있고, 같은 스타일 시트 안에서도 중복해서 등장할 수 있다. 이럴 경우 어떤 선언을 적용하는지, 그 우선순위가 중요해진다.
스타일 시트 중 우선순위가 높은 순서대로 나열하면 다음과 같다.
- 사용자 중요선언
- 저작자 중요선언
- 저작자 일반선언
- 사용자 일반선언
- 브라우저 선언
여기서 저작자가 코드 작성자이고, 사용자는 브라우저에서 받아보는 client를 말하는 것 같다.(추측이다. 본문에서는 이 둘을 정확히 설명하지 않았다)
여기서 중요선언이라는 것은 코드 뒤에 !important를 붙인 경우인 것 같다.(역시 추측이다)
하나의 스타일 시트 안에서 같은 선택자에 대한 선언이 나오면 선택자 특정성(specificity)라는 수치를 적용한다. 이것은 length=4 인 배열처럼 생겼다. [a, b, c, d] 이렇게 말이다.
a는 선택자 없이 style 속성이 선언된 것이라고 한다. style="" 이라는 예시를 들어줬는데, CSS에서 이런 사용법을 본 적이 없어서 무슨 뜻인지 모르겠다. 아무튼 이런 경우에는 a가 1이 되고, 아닐 경우 0이 된다.
b는 선택자에 있는 id 요소의 갯수를 의미한다. 예를 들어 선택자가 table 이란 태그이면 b는 0이 되고, #hello #world일 경우 b는 2가 된다.
c는 클래스, 속성, 가상 클래스 선택자의 개수이다. 마지막으로 d는 요소 선택자와 가상 요소 선택자를 의미한다. 간단히 말하면 div나 a:hover와 같은 것이다. 예시에서는 li:first-line 이라는 선택자의 d는 2라고 알려주는 것으로 보아 li와 :first-line을 따로 세는 것 같다.
위 a, b, c, d에 사용되는 진법은 가장 큰 수를 기준으로 결정된다. 17이 나올 경우는 매우 희박하겠지만 이런 경우에는 17진법이 되는 것이다.
배치란 무엇인가?
여기서 배치란 트리에 화면에 배치된 크기와 위치를 계산하는 것이다. 리플로라고도 부른다. 기본적으로 표(table)을 제외한 것들은 상→하 / 좌→우의 방향으로 배치된다.
크기와 위치를 계산하기 위해서는 좌표계가 필요하다. 좌표계는 기준점이 있고, 그것으로부터 상대적으로 적용된다. X, Y축을 사용한다. 배치에서 적용되는 것인지는 모르겠지만, 위치를 결정할 때는 z-index로부터 Z축, 그러니까 레이어의 순서도 고려된다.
<html>이 최상위 렌더러이며, 이것을 기준으로 (0,0) 중심좌표가 결정된다. position으로 위치 조정할 때를 돌이켜보면 기본적으로 (0, 0)은 상위요소의 좌상단이었던 것 같다. 또한 최상위 렌더러는 viewport만큼의 면적을 가진다.
배치는 DOM 트리가 업데이트 되고, 렌더 트리가 업데이트 되면서 계속 바뀔 수 있다. 이럴 때 변화의 종류는 크게 두 가지로 나눌 수 있다. 첫 번째는 전체에 적용되는 변화이고, 두 번째는 일부 요소에만 적용되는 변화이다. 배치가 바뀌어야 할 때마다 전체를 다시 그리는 것은 매우 비효율적이다. 따라서 두 번째 변화를 커버하기 위해 "더티 비트 체제"라는 시스템이 존재한다.
더티 비트 체제는 변화가 필요한 곳에 flag(메모지를 붙이는 것과 비슷하다)를 단다. flag는 두 가지가 있다. 본인이 다시 배치되어야 할 경우 더티 flag를 붙이고, 본인은 괜찮은데 자식 요소들이 바뀌어야 할 경우 자식이 더티 flag를 붙인다. (어감이 조금 이상...)
전체에 적용되어야 할 변화는 보통 동기적으로 일어나고, 일부가 변화해야 할 때는 비동기적으로 일어난다.
본문에서는 배치의 과정을 다음과 같이 설명하고 있다.
- 부모 렌더러가 자신의 너비(width)를 결정
- 부모가 자식 요소를 검토한다.
- 자식 렌더러를 배치한다(자식 렌더러의 x,y 좌표 결정됨)
- 필요하다면 자식 배치를 불러와서 자식의 높이를 계산한다.
- 부모는 자식의 전체적 높이를 고려하여 자신의 높이를 결정한다.
- 더티 비트 flag를 제거한다.
본격적으로 화면에 그리자
길고 긴 여정에 마지막으로 화면에 그리는 순간이 왔다. 렌더 트리의 요소인 렌더러에는 paint라는 method가 있다. 그리기 과정에서는 이 method를 호출한다.
그리기에도 순서가 있다고 한다. 우선순위대로 적으면, 배경색, 배경 이미지, 테두리, 자식, 아웃라인의 순서이다.
지금 공부한 것을 까먹지 않기 위해 블로그에 나만의 방식으로 해석한 글을 올린다. 이 해석을 보고 좀 더 쉽게 이해하시는 분이 계시다면 내가 잘 이해한 것이라고 생각한다. 잘 이해했으면 쉽게 설명할 수 있다고 생각한다. 읽으면서 이해가 잘 안 가는 부분들은 모른다고 이야기 한 후, 나의 추측을 덧붙였다. 앞으로 공부하면서 더 잘 이해하게 되면 계속 글을 업데이트 할 계획이다.
0 comments:
댓글 쓰기