1. 브라우저의 주요 기능
브라우저의 가장 핵심적인 기능은 사용자가 선택한 자원을 서버에 요청하고 브라우저에 표시하는 것입니다. 여기서 말하는 자원이란 보통 HTML 문서이지만, 이미지나 비디오 혹은 PDF 등이 될 수도 있습니다. 자원이 존재하는 곳(주소)은 URI(Uniform Resource Identifier)로 나타내집니다.
HTML 문서의 경우 HTML과 CSS 명세에 따라 HTML 문서를 해석해서 표시하는데, 여기서 말하는 명세는 W3C(World Wide Web Consortium)에서 정합니다.
이런 기본적인 동작 이외에도 브라우저는 다운로드 매니저 같은 추가적인 동작을 위한 GUI를 제공합니다.
2. 브라우저의 기본 구조
2-1. 사용자 인터페이스 (User Interface)
유저가 웹페이지의 모든 시각적 요소들과 상호작용을 할 수 있게 해줍니다. 주소 표시줄, 이전/다음 버튼, 북마크 메뉴 등, 요청한 페이지를 보여주는 창을 제외한 나머지 모든 부분을 나타냅니다.
2-2. 브라우저 엔진 (Browser Engine)
모든 브라우저의 핵심 요소이며 GUI와 렌더링 엔진 사이의 동작을 제어합니다.
2-3. 렌더링 엔진 (Rendering Engine)
요청한 콘텐츠(HTML, XML, 이미지 등)를 표시하는 역할을 합니다. 예를들어 HTML 요청시 HTML과 CSS를 파싱해서 화면에 표시합니다. 크롬 브라우저와 사파리 브라우저가 사용하는 Webkit, 파이어폭스 브라우저가 사용하는 Gecko 등이 있습니다. 크롬은 대부분의 브라우저와 달리 각 탭마다 별도의 렌더링 엔진 인스턴스를 유지하고, 그렇기에 각 탭은 독립된 프로세스로 처리됩니다.
2-4. 통신 (Networking)
HTTP 요청, FTP 통신과 같은 네트워크 호출에 사용됩니다. 플랫폼 독립적인 인터페이스이고 각 플랫폼 하부에서 실행됩니다.
2-5. 자바스크립트 인터프리터 (Javascript Interpreter)
웹 페이지에 있는 자바스크립트 코드를 해석하고 실행합니다. 여기서의 결과물은 UI에 표시되기 위해 렌더링 엔진으로 향합니다.
2-6. UI 백엔드 (UI Backend)
콤보 박스와 창(윈도우) 같은 기본적인 위젯을 그립니다. 플랫폼에서 명시하지 않은 일반적인 인터페이스로서, OS에 탑재된 사용자 인터페이스 체계를 사용합니다.
2-7. 데이터 저장소 (Data Persistence)
쿠키를 저장 등 모든 종류의 자원을 하드 디스크에 저장합니다.
3. 렌더링 엔진의 주요 동작 과정
렌더링 엔진의 기본적인 동작 흐름은 다음과 같습니다.
(1) 통신을 통해 요청한 문서(Documents)의 내용을 받습니다. chunck 단위로 나눠진 패킷을 받으며, 그 수신 패킷의 크기는 14KB부터 시작해 28KB, 56KB으로 늘어나며 네트워크나 하드웨어 자원이 허용 가능한 선까지 늘어납니다. 이러한 방식은 흔히 TCP Slow Start라고 불립니다. 이는 네트워크 계층을 통해 이루어지게 됩니다.
(2) HTML을 파싱하고 DOM tree를 생성합니다. 패킷을 받음과 동시에, 요청된 HTML 페이지가 외부 CSS 파일이나 스타일링 요소들과 함께 HTML token 단위로 파싱됩니다. 콘텐츠 트리 내부에서 태그들을 DOM 노드로 변환하고, 외부 CSS 파일을 통해 여기에 포함된 스타일 요소를 파싱하면서 작업이 이루어지게 됩니다.
(3) 위 과정과 동시에 Render tree를 생성합니다. 스타일 정보를 통해 만들어진 CSSOM와 HTML 표시 규칙을 이용해 만들어진 DOM 등을 조합하여 Render tree를 생성하게 됩니다. 이 과정은 DOM트리의 root부터 시작해 모든 시각적인 요소를 거쳐가며 생성됩니다. Render tree는 색상이나 차원같은 시각적 측면을 가진 사각형들로 이루어져있고, 이 사각형들은 화면에 표시될 올바른 순서가 됩니다.
(4) Render tree를 바탕으로 배치(Layout)를 합니다. 렌더 트리가 생길 땐 위치나 크기에 대한 값은 할당되어있지 않습니다. 그렇기에 렌더 트리의 각 노드들에게, 해당 노드가 화면상에 나타내어져야할 좌표를 줍니다. 단, 더 나은 UX를 위해, 모든 HTML을 파싱할 때 까지 기다리지 않고 배치(Layout)와 그 다음 단계인 그리기(Paint)를 시작합니다. (네트워크로부터 나머지 내용이 전송되기를 기다리는 동시에 받은 내용의 일부를 먼저 화면에 표시하는 것을 말합니다.)
(5) 위 배치의 결과를 바탕으로 그리기(Paint)를 진행합니다. UI 백엔드에서 렌더러가 paint() 메소드를 통해 렌더 트리의 각 노드를 가로지르며 형상을 만들어냅니다.
4. 렌더링 엔진 동작 원리
4-1. HTML 파싱 및 DOM
HTML의 어휘와 문법은 W3C에 의해 명세로 정의되어 있습니다. 현재 버전은 HTML5입니다. HTML은 문맥 자유 문법이 아닙니다. 그렇기에 모든 전통적인 파서는 HTML에 적용할 수 없죠. 그런 전통적인 파싱은 CSS와 자바스크립트를 파싱하는 데 사용됩니다.
문서를 파싱한다는 것은 문서를 브라우저가 이해하고 사용할 수 있는 코드의 구조로 변환하는 것을 얘기합니다. 파싱된 결과는 보통 문서의 구조를 나타내는 노드들의 트리인데, 파싱 트리(parse tree) 또는 문법 트리(syntax tree)라고 부릅니다.
파싱은 문서에 작성된 언어 또는 형식의 규칙에 따르는데, 파싱할 수 있는 모든 형식은 정해진 용어와 구문 규칙에 따라야 합니다. 이것을 문맥 자유 문법(context free grammer)이라고 합니다. (= 완전히 BNF로 표현 가능한 문법) 인간의 언어는 이런 모습과는 다르기 때문에 기계적으로 파싱이 불가능합니다.
파싱은 어휘 분석과 구문 분석의 두 세부 단계로 나뉘어질 수 있습니다.
어휘 분석이란 입력 값을 토큰들로 잘라 나누는 것입니다. 토큰은 인간의 언어로 말하자면 사전에 등장하는 모든 단어에 해당합니다. Lexer는 Tokenizer로 불리기도 합니다. 또한 이 과정에서 공백이나 줄바꿈 같은 의미 없는 문자를 제거하기도 합니다. 어휘는 보통 정규식으로 표현됩니다. 한편 구문 분석은 언어의 구문 규칙(syntax rule)을 적용하는 과정입니다. 구문은 보통 BNF형식으로 표현됩니다.
대부분의 경우에서 parse tree는 최종 결과물이 아닙니다. 파싱은 보통 입력 값인 문서를 다른 형태로 변환하는데에 사용됩니다. 그 하나의 예가 컴파일입니다. 소스 코드를 기계 코드로 만드는 컴파일러는 가장 먼저 파싱 트리를 생성하고 이후 이를 기계 코드 문서로 변환합니다.
HTML의 경우 그 변환 결과는 DOM tree가 됩니다. DOM이란 Document Object Model의 약어로서, HTML 문서의 객체 표현이고, 외부를 향하는 자바스크립트와 같은 HTML 요소의 연결 지점이 됩니다. 또한 DOM은 마크업과 1:1의 관계를 맺으며 HTML과 마찬가지로 W3C에 의해 명세가 정해져있습니다.
HTML 파싱 알고리즘은 토큰화와 트리 구축, 이렇게 두 단계로 이루어집니다. 토큰화란, 어휘 분석으로서, 입력 값을 토큰으로 파싱합니다. HTML에서 토큰은 시작 태그, 종료 태그, 속성 이름과 속성 값이 됩니다. 토큰화는 토큰을 인지해서 트리 생성자로 넘기고 다음 토큰을 확인하기 위해 다음 문자를 확인합니다. 그리고 입력의 마지막까지 이 과정을 반복합니다.
파서가 이미지와 같은 non-blocking 자원을 만나면 그런 요청이 필요한 자원을 요청하고 파싱을 계속합니다. CSS파일을 만나도 파싱을 계속하기는 하나, asyn나 defer 속성이 없는 script 태그와 같이 렌더링을 막는 태그를 만나면 HTML파싱을 중단합니다.
HTML 파일 작성자가 Invalid Syntax와 같은 실수를 해도 특정 부분들에 대해선 브라우저의 파서가 그 실수를 교정하기도 합니다.
HTML 파싱에는 Preload Scanner 라는 것이 도움을 주기도 합니다. 브라우저가 DOM트리를 만들동안, 한 켠에서는 프리로드 스캐너가 문서를 훑어보며 CSS, JS, 웹폰트와 같이 높은 우선순위의 자원들을 찾아내고 파싱합니다. 덕분에 파서가 이것들을 따로 찾아내고 외부 리소스를 참조하기 위해 요청을 보낼때까지 기다리지 않아도 되죠. 메인 HTML 파서가 문서를 파싱해내려가다가, 요청된 자원들에 도달하는 동시에 그 자원들이 '네트워크를 통해 받아와지고 있거나 이미 다운로드 받은 상태'이기 위해 이 과정은 백그라운드에서 진행됩니다.
4-2. CSS 파싱
HTML과는 다르게 CSS는 문맥 자유 문법이고 파싱이 가능합니다. 어휘 문법(lexical grammer)은 위 표처럼 각 토큰을 위한 정규 표현식으로 정의되어 있습니다. 그리고 구문 문법(syntax grammar)은 아래 이미지처럼 BNF로 정의되어있습니다.
CSS 파일은 스타일 시트 객체로 파싱되고 각 객체는 CSS 규칙을 포함합니다.
그 결과로 CSSOM 트리를 생성하게 됩니다. CSSOM은 DOM과 비슷한 tree이긴 하지만 독립적인 자료구조입니다. CSS 선택자에 기초해서 계층적으로 CSS 규칙을 따라 tree를 생성하게 되는데, 이 과정은 한번의 DNS lookup보다 빠를 정도로 매우 빠르게 진행됩니다.
4-3. 스크립트와 스타일 시트의 프로세싱 순서
웹은 파싱과 실행이 동시에 존재합니다. 하지만 스크립트가 프로세싱 되는 동안은 파싱이 중단됩니다. 스크립트가 외부에 있어 네트워크로 받아와야 하는 경우도 마찬가지입니다. (코드 작성자가 스크립트 태그에 defer(지연)이라는 속성을 추가할 수 있는데, 이 때는 파싱이 중단되지 않고, 파싱이 완료된 이후에 스크립트가 실행됩니다.) HTML5에서는 스크립트를 비동기로 처리하는 속성을 추가했기 때문에 서로 다른 스레드에서 파싱과 프로세싱이 일어납니다.
웹킷과 파이어폭스는 예측 파싱과 같은 최적화를 지원합니다. 한 스레드가 스크립트를 실행하는 동안 다른 스레드는 네트워크로부터 다른 자원을 받고 문서의 나머지 부분을 파싱합니다. 단 예측 파서는 외부 소스로부터의 참조 자원(스크립트, 이미지, 스타일시트)을 파싱할 뿐 DOM 트리를 수정하진 않습니다. 그 작업은 오로지 메인 파서에게만 권한이 있습니다.
이론적으로 스타일 시트는 DOM 트리를 변경하지 않기 때문에 문서 파싱을 기다리거나 중단할 이유가 없어보이지만 스크립트가 문서를 파싱하는 동안 스타일 정보를 요구하는 경우엔 문제가 됩니다. 스타일이 파싱되지 않은 상태라면, 스크립트는 잘못된 결과를 내놓고 이로 인해 많은 문제를 야기할 수 있습니다. 이런 문제는 특이케이스처럼 보일 수 있어도 자주 발생합니다. 파이어폭스는 아직 로드 중이거나 파싱 중인 스타일 시트가 있는 경우 모든 스크립트의 실행을 중단합니다. 웹킷은 로드되지 않은 스타일 시트 가운데 문제가 될만한 속성이 있을 때에만 스크립트를 중단합니다.
4-4. 렌더 트리 구축
DOM 트리가 구축될 동안 브라우저는 또 다른 트리인 렌더 트리를 구축합니다. 렌더 트리는 각각의 요소가 어디에 어떤 순서로 배치될지에 대한 시각적 요소입니다. 즉, 이 트리의 목적은 요소들을 정확한 순서대로 배치하는데에 있습니다.
파이어폭스는 렌더 트리의 각 요소를 frame이라 하고 웹킷은 renderer 혹은 render 객체라고 부릅니다. 이후 이 렌더트리 요소는 웹킷의 예시로 renderer로 설명드리겠습니다.
renderer는 자기 자신과 그 하위 요소들을 어떻게 배치하고 그릴지 알고 있습니다. 각 renderer는 CSS 박스에 해당하는 사각형의 형태를 띄고, 너비, 높이, 위치 같은 기하학적 정보를 가지고 있습니다. 그 박스의 유형(위에서 말한 사각형의 형태)은 CSS의 display 속성에 영향을 받습니다. 나타내야할 요소가 form이나 table과 같이 특별한 형태라면 웹킷은 기존의 방식과는 다르게 비기하학 정보를 가지는 스타일 객체를 나타냅니다.
렌더 트리와 DOM 트리 사이 관계를 살펴보자면, 둘은 깊은 연관이 있지만 1:1대응은 아닙니다. 예를들어 head 태그 요소와 같은 비시각적인 DOM 요소는 렌더트리에는 포함되지 않습니다. 또한 display 속성을 none으로 지정하면 렌더트리에 나타나지 않습니다. (단, visibility 속성에 hidden 으로 설정된 요소는 렌더트리에 나타납니다!) 여러 개의 시각적인 객체에 대응하는 DOM요소도 존재합니다. 예를들어 select요소는 표시영역, 드롭다운 리스트, 버튼 표시를 위한 3개의 렌더 트리 요소를 가집니다. 어떤 렌더 트리 요소는 DOM노드에 1:1대응하지만 트리의 같은 위치에 있지는 않는 경우도 있습니다. float 처리된 요소나 position이 absolute로 처리된 요소는 기존 흐름에서 벗어나 트리의 다른 곳에 배치됩니다. 대신 'placeholder 프레임'이 트리 상에서 원래 있어야 할 자리에 배치됩니다.
웹킷을 기준으로 렌더 트리를 구축하는 과정을 살펴보자면, 웹킷에서 스타일을 결정하고 렌더러를 만드는 과정을 attachment라고 부르는데요, 모든 DOM노드는 각자 attach 메소드를 가집니다. 이 attachment는 동기적이며 DOM트리에 하나의 노드를 추가하면 다음 노드의 attach 메소드를 호출합니다. 이러한 방법을 통해 html 태그와 body 태그를 프로세싱함에 따라 렌더 트리의 루트를 구성한 후, 트리의 나머지 부분은 DOM 노드를 차례로 구성하며 구축합니다.
렌더 트리를 구축하려면 각 렌더 객체의 시각적 속성에 대한 계산이 필요한데 이는 각 요소의 스타일 속성을 계산함으로써 처리됩니다.
스타일 데이터는 구성이 매우 광범위할 수 있고 속성들이 많기 때문에 최적화가 되어있지 않다면 메모리 등에 성능 문제를 야기할 수 있습니다. 따라서 선택자에 따라 트리의 특정 줄기를 선택하는 과정 등 계층 구조를 파악해야하는 규칙이 필요합니다. 이 규칙에 대한 자세한 내용은 오늘 다루고자 하는 범위를 벗어나므로 추후에 따로 다뤄보도록 하겠습니다.
4-5. 배치(Layout) 혹은 리플로(Reflow)
렌더트리가 생성되고 나서는 배치(Layout) 과정을 시작합니다. 여기서 말하는 배치 과정이란 렌더러가 생성되어 트리에 추가될 때, 존재하지 않는 요소의 실제 크기와 위치 정보를 계산하는 것을 말합니다.
HTML은 흐름 기반의 배치 모델을 사용합니다. 이러한 배치의 흐름은 왼쪽에서 오른쪽으로 또는 위에서 아래로 흐릅니다(table은 예외). 뷰포트를 기준으로 진행되며 좌표계는 기준점으로부터 상대적으로 위치를 결정하는데 X축(left)과 Y축(top) 좌표를 사용합니다. 최상위 렌더러의 위치는 0,0 이고 브라우저 창의 보이는 영역에 해당하는 뷰포트 만큼의 면적을 갖습니다. 이미지와 같이 크기정보를 모르는 것에 대해선 placeholder의 자리로 계산합니다. 이러한 과정이 최초로 일어나면 배치(Layout), 최초 수행 이후에 이어지는 재계산은 Reflow라고 부릅니다. 예를들어서 크기정보를 모르는 이미지 파일을 네트워크를 통해 받기전엔 배치가 일어나고, 이미지 파일을 받고 나서의 재계산으로 리플로가 일어나겠죠.
소소한 변경 때문에 전체를 재배치하는 것을 피하고자 브라우저는 "Dirty Bit(더티비트)"라는 시스템을 사용합니다. 렌더러는 재배치할 필요가 있는 변경 요소나 추가된 것, 그리고 그 자식을 "더티"라고 표시합니다. 이런 표시를 위한 "더티"와 "자식이 더티" 이렇게 두 가지 플래그가 있습니다. "자식이 더티"하다는 것은 본인은 괜찮지만 자식 가운데 적어도 하나를 다시 배치할 필요가 있다는 의미입니다.
배치는 크게 전역 배치와 점진적 배치로 나눌 수 있습니다. 전역 배치는 "렌더 트리 전체에서 일어날 수 있는 배치"로, 글꼴 변경과 같은 렌더러 전체에 영향을 주는 전역 스타일 변경이나 화면 크기 변경에 의한 결과를 말합니다. 점진적 배치는 렌더러가 "더티"일 때 비동기적으로 발생합니다. 위에서 예시를 들은 이미지 파일의 예시처럼 네트워크로부터 추가 내용을 받아서 DOM 트리에 더해진 다음 새로운 렌더러가 렌더 트리에 붙을 때 특히 이러한 배치가 일어납니다.
점진적 배치는 보통 비동기, 전역 배치는 보통 동기적으로 실행되는데, "offsetHeight" 같은 스타일 정보를 요청하는 스크립트는 동기적으로 점증 배치를 실행하기도 합니다.
렌더링 엔진은 몇가지 최적화를 지원하는데, 배치가 크기 변경 또는 렌더러 위치 변화 때문에 실행되는 경우 렌더러의 크기는 다시 계산하지 않고 캐시로부터 가져옵니다. 또한 텍스트필드 입력과 같이 변화 범위가 한정적이어서 주변에 영향을 미치지 않을 때 하위 트리만 수정이 되고 최상위로부터 배치가 시작되지 않기도 합니다.
배치 과정은 순서대로 1. 부모 렌더러가 자신의 너비를 결정하고 2. 자식 렌더러를 배치하면서 (부모와 자식이 더티하거나 전역 배치 상태이거나 또는 다른 이유로) 필요하다면 자식 배치를 호출하여 자식의 높이를 계산합니다. 이후 3. 부모는 자식의 누적된 높이와 여백, 패딩을 사용하여 자신의 높이를 설정합니다. 이 값은 부모 렌더러의 부모가 사용하게 됩니다. 4. 마지막으로 더티 비트 플래그를 제거합니다.
4-6. 그리기(Paint)
화면에 내용을 표시하기 위한 렌더 트리가 탐색되고 렌더러의 "paint" 메서드가 호출됩니다. 브라우저에서 부드러운 스크롤과 애니메이션을 위해선 스타일계산-리플로-그리기가 16.67ms 안에 이루어져야합니다. 리페인팅을 최초페인팅보다 빠르게 하기 위해 화면을 그리는 것을 몇개의 layer로 나눠서 진행하기도 합니다. 이럴 경우엔 추후 compositing 과정이 필요합니다. 여기서 메인 스레드를 쓰는 CPU 대신 GPU가 활약하기도 합니다.
그리기는 배치와 마찬가지로 전역 또는 점증적 방식으로 수행됩니다. 그리는 순서는 블록 렌더러가 쌓이는 순서대로 그리며, 블록 안에선 1. 배경색 2. 배경 이미지 3. 테두리 4. 자식 5. 아웃라인 순으로 그립니다.
브라우저는 변경에 대해 가능한 한 최소한의 동작으로 반응하려고 노력합니다. 그렇기 때문에 요소의 색깔이 바뀌면 해당 요소의 리페인팅만 발생합니다. 요소의 위치가 바뀌면 요소와 자식 그리고 형제의 리페인팅과 재배치가 발생합니다. DOM 노드를 추가하는 경우는 노드의 리페인팅과 재배치가 발생합니다. "html" 요소의 글꼴 크기를 변경하는 것과 같은 큰 변경은 캐시를 무효화하고 트리 전체의 배치와 리페인팅이 발생합니다.
통신을 제외한 거의 모든 경우에 렌더링 엔진은 단일 스레드로 동작합니다. 통신은 몇 개의 병렬 스레드에 의해 진행될 수 있는데 병렬 연결의 수는 보통 2개에서 6개로 제한됩니다.
그리기는 박스모델을 기반으로 진행됩니다. 각 박스는 콘텐츠 영역(문자, 이미지 등)과 선택적인 패딩과 테두리, 여백이 있습니다. 모든 요소는 만들어질 박스의 유형을 결정하는 "display" 속성을 갖는데 이 속성의 유형은 Block, Inline, None(박스를 만들지 않음)으로 나뉩니다. Block은 다른 블록 아래 수직으로 배치되고 Inline은 수평으로 배치됩니다.
위치는 "position" 속성과 "float" 속성에 의해 결정되는데 static과 relative로 설정하면 일반적인 흐름에 따라 위치가 결정되고 absolute와 fixed로 설정하면 절대적인 위치가 됩니다. (absolute와 fixed의 차이 : fixed인 경우 뷰포트로부터 위치를 결정)
'Network' 카테고리의 다른 글
호스팅(Hosting)이란? (0) | 2022.09.21 |
---|---|
HTTP란 무엇인가? (2) | 2022.06.25 |
DNS란 무엇이고 어떤 원리로 동작하는가 (0) | 2022.06.22 |
Internet(인터넷)의 동작 원리 (0) | 2022.06.16 |
Internet(인터넷)은 무엇인가? (0) | 2022.05.12 |