본문 바로가기
Develop/CS

브라우저의 동작 원리

by 연로그 2021. 10. 7.
반응형

목차

1. Web browser

2. Render Engine

3. Parsing

    3-1. Parsing-general

    3-2. HTML Parser

    3-3. CSS parsing

4. Render tree construction

5. Layout

6. Painting

7. Dynamic changes

 

 


1. Web Browser

 

 웹 브라우저는 크롬, 익스플로러, 파이어폭스, 사파리, 오페라 등이 있다. 누구든지 인터넷만 있다면 언제 어디서나 이용할 수 있게 해준다. 내 데스크 톱이나 모바일 기기 등에 다른 웹으로부터 가져온 정보를 보여준다. HyperText Transfer Protocol (텍스트, 이미지, 비디오 등이 웹에서 어떤 방식으로 전송될지를 결정) 를 이용해 전송되는 이 정보는 전세계 어디서나 브라우저를 사용하는 사람들이 정보를 볼 수 있도록 일관된 형식으로 공유 및 표시되어야 한다.

 

 모든 브라우저 제조사가 같은 방식으로 해석되지는 않는다. 이 말은 즉, 사용자들마다 웹사이트가 다르게 보일 수 있다. 이를 위해 필요한 것이 '웹 표준'인데, 어떤 사용자나 사용하는 브라우저의 종류와는 상관없이 인터넷을 즐길 수 있도록 브라우저 간의 일관성을 만드는 것이다.

 

 웹 브라우저가 인터넷과 연결된 서버에서 데이터를 가져올 때, 소프트웨어의 일부는 데이터를 텍스트나 이미지로 전환시킬 렌더링 엔진을 호출한다. 가져온 데이터는 html으로 작성되어 있기 때문에 웹 브라우저가 이 데이터를 읽어 우리가 보고, 듣고, 경험할 수 있도록 만들어준다.

 

 하이퍼링크는 사용자들에게 웹에서 다른 페이지나 사이트의 경로로 이동할 수 있게 해준다. 웹페이지, 이미지, 비디오는 각각 그들만의 특별한 URL이 존재한다. (= Web Address) 데이터를 위해 브라우저가 서버를 방문할 때, Web Address는 브라우저에게 html에 설명된 각 항목(item)의 위치와 웹 페이지 위치를 알려준다.

 

 브라우저가 html을 해석하고 표시하는 방식은 html과 css 사양이 지정되어 있다. 이 사양은 웹 표준 기관인 W3C에서 유지 관리한다. 몇년간 브라우저는 사양의 일부만 준수하고 자체적으로 확장시켜 개발해왔으나 이로 인해 호환성 이슈가 발생했다. 이제는 대부분의 브라우저들이 어느 정도의 사양을 준수하게 되었다.

 

 브라우저 유저 인터페이스는 서로 많은 공통점들을 갖고 있다.

  • URI를 입력하는 주소창
  • 이전 / 다음버튼 (=뒤로가기, 앞으로가기)
  • 북마크 기능
  • 새로고침 또는 새로고침 중단 버튼
  • 사용자가 지정한 홈 페이지로 이동할 수 있는 홈 버튼

 

 브라우저의 사용자 인터페이스는 공식적인 사양으로 지정되어있지 않다. 이건 브라우저들이 서로를 모방하며 수년간의 경험으로 인해 만들어진 모범 답안으로 굳혀진 것이다. html5 사양은 브라우저가 반드시 가져야하는 UI 요소로 정의되지 않았지만 몇 가지 공통 요소들을 갖고 있다. (주소창, 상태창, 도구창 같은) 물론 파이어폭스의 다운로드 관리자같이 특정 브라우저에만 존재하는 특별한 기능이 존재하기도 한다.

 

이제 본격적으로 브라우저의 구조에 대해 살펴보자.

🔻 각 요소에 대한 설명

더보기
더보기

User Interface

  • 주소창, 뒤로가기, 앞으로가기, 북마크 등
  • 사용자가 요청한 창을 제외한 브라우저에 표시되는 모든 부분

 

Browser Engine

  • UI와 렌더링 엔진 사이의 동작 제어

 

Rendering Engine

  • 요청한 컨텐츠의 표시를 담당
    ex: 컨텐츠가 html이라면 렌더링 엔진은 html과 css를 분석하여 화면에 표시
  • 기본적으로 html, xml을 표시
  • 플러그인이나 확장을 통해 다른 종류의 데이터들 표시 가능
  • 브라우저마다 다양하게 사용
    ex: Firefox-> Gecko / Safari-> WebKit / Chrome, Opera -> Blink / ...

 

Networking

  • HTTP request와 같은 네트워크 호출의 경우, 플랫폼과 독립적인 인터페이스 뒤에서 플랫폼마다 다른 구현을 사용

 

JavaScript Interpreter

  • Javascript 코드를 분석 및 실행

 

UI Backend

  • 콤보 박스와 창 같은 기본적인 위젯을 그리는데 사용
  • 플랫폼에 독립적인 일반적인 인터페이스를 보임
  • 운영체제 UI 메소드 안에서 사용

 

Data Storage

  • 데이터 저장 계층, 영속 계층(Persistence Layer)
  • 브라우저는 로컬(ex: 쿠키)에 모든 종류의 데이터를 저장
  • localStorage, IndexedDB, WebSQL, FileSystem 같은 저장 메커니즘 제공

 

다양한 종류의 데이터를 표시할 수 있지만 설명을 위해 html과 css를 중점에 두고 서술하겠다.

 


2. Render Engine

 

Rendering Engine은 네트워크 계층에서 요청된 문서의 컨텐츠를 가져오며 시작하는데 문서의 내용은 보통 8KB 단위로 전송된다.

세부적인 동작은 브라우저마다 다를 수 있지만 기본적인 흐름은 다음과 같다.

 

 

  1. render tree 트리 생성
    html 문서를 파싱하고 content tree 내부에서 태그를 DOM 노드로 변환
    외부 css 파일과 스타일 요소를 포함한 스타일 데이터를 파싱
    -> 이러한 스타일 정보와 html 표시 규칙들로 render tree라는 또다른 트리를 생성
  2. render tree 구조 세우기
    render tree는 색상과 너비 같은 시각적인 요소를 포함한 사각형 포함 
    -> 이 사각형들이 순서대로 화면에 표시

  3. layout 프로세스
    화면에 표시될 노드들의 정확한 좌표를 각각 제공
  4. painting
    render tree를 순회하며 각 노드들은 UI backend를 이용해 그려짐

 

 위와 같은 작업이 '점진적인 프로세스'라는 것이 중요하다. 더 좋은 사용자 경험을 위해 Rendering Engine은 최대한 빨리 컨텐츠를 화면에 보여줘야 한다. 컨텐츠의 일부가 parsing되고 화면에 표시되는 동안 프로세스는 계속해서 네트워크로부터 남은 컨텐츠를 받아온다.

 


3. Parsing

 

3-1. Parsing-general

Rendering Engine에서 파싱이란 매우 중요한 프로세스이기 때문에 좀 더 깊게 배워보겠다. 

 

Parsing이란?

  • 코드를 사용 가능한 구조로 전환하는 것
  • 파싱 결과는 보통 문서 구조를 나타내는 노드 트리. (= Parse Treel = Syntax Tree)

  • ex: 2 + 3 - 1은 아래와 같은 tree를 반환
  • Context Free Grammar: 파싱할 수 있는 모든 형식에 존재하는 어휘나 구문 규칙으로 구성된 문법
    사람의 언어는 이런 언어가 아니라 전통적인 파싱 기술로 파싱이 불가능.

 

Parser-Lexer combination; 파서-어휘 분석기 조합

  • 파싱 프로세스: 어휘 분석 + 구문 분석
  • Lexcial Analysis; 어휘 분석: 입력을 토큰으로 나누는 과정
  • Syntax Analysis; 구문 분석: 언어의 구문 규칙을 적용

  • 파서는 주로 2개의 컴포넌트로 작업 분류.
    lexer: 입력을 유효한 토큰으로 변경
            공백이나 줄바꿈 같이 의미 없는 문자를 제거하는 방법에 대해 앎
    parser: 언어 구문 규칙에 따라 문서 구조를 분석해 parse tree 생성

 

 파싱 작업은 반복적이다. 토큰이 구문 규칙에 부합하는지 매치시켜보고, 부합하면 이에 상응하는 노드가 parse tree에 추가되고 파서는 또다른 토큰을 호출한다. 규칙에 부합하지 않는다면 파서는 내부적으로 토큰을 저장하고 규칙에 부합할때까지 내부적으로 저장된 모든 토큰들을 찾아낸다. 그런 뒤 아무런 규칙도 찾아내지 못하면 파서는 예외(exception)가 일어날 것이다. 이는 문서가 유효하지 않고 구문적으로 오류가 존재함을 의미한다.

 

 

Translation; 변환

  • parse tree는 최종 결과물이 아님
  • 파싱은 종종 입력 문서를 다른 형식으로 변환(translation)할때 사용
    ex: 컴파일 -> 소스 코드를 기계어로 컴파일하는 컴파일러는 parse tree 생성 후 이를 기계어 문서로 변환.

 

3-2. HTML Parser

  • HTML 마크업을 parse tree로 파싱하는 역할
  • html의 단어와 구문은 W3C이 만든 사양에 정의되어 있음

 

문맥 자유 문법(Context Free Grammar)가 아님

 모든 기존의 파서 주제가 html을 허용하지는 않는다. html은 파서가 필요한 문맥 자유 문법에 의해 쉽게 정의될 수 없다. DTD라는 html을 정의하기 위한 일반적인 형식이 있지만 문맥 자유 문법이 아니다.

 여기서 이상한 점이 느껴질 수 있다. html은 xml과 유사해보이고 사용 가능한 xml 파서들이 많다. html을 xml형태로 재구성한 xhtml도 있는데 차이점이 뭘까? -> 바로 html이 좀 더 '너그럽다'는 점이다. 사용자가 암묵적으로 태그를 생략하거나 가끔은 시작/마지막 태그를 생략할 수 있다.

 이처럼 작은 차이가 큰 차이를 만든다. 이는 왜 html이 인기가 많은가를 알 수 있다. 웹 개발자의 실수를 너그럽게 용서하고 편하게 해주기 때문이다. 다른 한편으로는 공식적인 문법을 작성하기 어렵다. 요약하자면  html는 문맥 자유 문법에 해당하지 않기 때문에 conventional parser로 쉽게 파싱할 수 없기에, html은 xml 파서로 파싱하기 쉽지 않다.

 

 

DOM; Document Object Model

 parse tree는 DOM의 요소와 속성 노드들의 트리로서 출력 트리이다. DOM은 html 문서의 객체 표현과 Javascript같은 외부를 향하는 html 요소의 연결 지점이다. 트리의 최상위 객체인 루트에는 Document가 존재한다.

 트리가 DOM 노드를 포함한다고 말하는 것은 DOM 접점의 하나를 실행하는 요소를 구성한다는 의미다. 브라우저들은 내부의 다른 속성들을 이용해 이를 구체적으로 사용한다.

 

🔻 마크업과 DOM 트리 예제

더보기
더보기
<html>
  <body>
    <p>
      Hello World
    </p>
    <div> <img src="example.png"/></div>
  </body>
</html>

 

파싱 알고리즘

 앞서 내용에서 html은 일반적인 상향식 / 하향식 파서를 이용해 파싱할 수 없다는 것을 보았다. 그 이유는 아래와 같다.

  • 너그러운 언어의 속성
  • 잘 알려진 html 오류에 대한 브라우저의 관용
  • 변경에 의한 재파싱.
    소스는 파싱 도중에 변할 수 없지만 html은 동적 코드를 추가할 수 있어 실제로 입력 과정에서 파싱 작업이 수정됨.

 

일반적인 파싱 기술을 사용할 수 없기에, 브라우저는 html 파싱을 위해 커스텀된 파서들을 생성한다. 파싱 알고리즘은 html5 명세에 자세히 설명되어 있다. 알고리즘은 'tokenization', 'tree construction' 2가지 작업이 포함된다.

 

 Tokenization; 토큰화

: 어휘 분석. 토큰을 입력해 파싱한다. html 토큰 중에는 시작 태그, 끝 태그, 속성 이름, 속성 값이 있다. tokenizer는 토큰을 인식해 트리 생성자에게 전달하고, 다음 토큰을 확인하기 위해 다음 문자를 확인하는 과정을 입력의 마지막까지 반복한다.

html 파싱 흐름

 

 

Tokenization Algorithm; 토큰화 알고리즘

 알고리즘의 결과물은 html 토큰이다. 알고리즘은 상태 기계(state machine)으로 표현된다. 각 상태는 하나 이상의 연속된 문자를 입력받아 이에 따라 다음 상태를 갱신한다. 이 결과는 현재의 토큰화 상태와 트리 구조 상태에 따라 영향을 받는다. 즉, 같은 문자를 읽어 들여도 현재 상태에 따라 다른 결과가 나온다는 의미이다. 알고리즘은 설명하기 굉장히 복잡하지만 간단한 예를 통해 원리만 이해해보도록 하겠다.

 

<html>
  <body>
    Hello world
  </body>
</html>
  1. Data state
  2. < 문자가 들어와 Tag open state로 변경
  3. a-z 문자가 들어와 Tag name state로 변경 (> 가 들어오기 전까지)
  4. > 문자가 들어와 Data state로 변경
  5. <body>가 들어올때도 '1 ~ 4' 과정 반복
  6. Hello World의 글자 하나하나가 입력되며 </body>의 <가 나타날때까지 문자 토큰을 생성하고 방출
  7. 2 과정 반복
  8. / 문자가 들어와 end tag token을 생성
  9. 3, 4 과정 반복
  10. </html>의 경우에도 '7 ~ 9' 과정 반복

 

Tree Construction Algorithm; 트리 구축 알고리즘

 파서가 생성되면 문서 객체가 함께 생성된다. 트리 생성 단계 동안, 문서 최상단에서는 DOM 트리가 수정되고 요소가 추가될 것이다. 토큰화에 의해 방출됐던 각 노드들은 트리 생성자에 의해 처리된다. 각 토큰을 위한 DOM 요소의 명세는 정의되어 있다. DOM 트리에 요소를 추가하는 것이 아니라면 열린 요소는 스택에 쌓인다. 이 스택은 부정확한 중첩이나 닫히지 않은 태그를 교정하기 위해 사용된다. 위에서 봤던 예제를 다시 살펴보자.

 

<html>
  <body>
    Hello world
  </body>
</html>
  1. initial mode
  2. html 토큰을 받아 before html 모드
    문서 객체의 최상단에 HTMLHtmlElement 요소 추가
  3. before head 상태
  4. head 토큰이 받아지지 않아 묵시적으로 생성된 HTMLHeadElement가 트리에 추가
    in head, after head를 거쳐 body 토큰을 받아 in body 상태가 됨
  5. "Hello World" 문자 토큰을 받아 첫 번째 토큰 생성.
    본문 노드가 추가되며 다른 문자들이 이 노드에 추가될 것.
  6. body 토큰을 받아 after body 모드
  7. html 토큰을 받아 after after body 모드
  8. 마지막 파일 토큰(EOF token) 을 받아 파싱 종료

🔻 트리 구축 알고리즘 예제 그림

 

파싱이 종료된 후에...

 브라우저는 상호작용되는 문서로서 표시하고 (문서의 파싱이 끝난 뒤 실행하는) '지연' 모드의 스크립트를 파싱하기 시작한다. 문서 상태는 'complete'가 될 것이고 'load' 이벤트가 발생하게 된다. 토큰화와 트리 구축의 전체 알고리즘은 html5 명세에서 확인할 수 있다.

 

 

3-3. CSS Parsing

 css는 문맥 자유 문법이기 때문에 인트로에서 설명했던 파서들로 파싱할 수 있다. 실제로 css 명세는 css 어휘 및 구문 문법에 대해 정희하고 있다. 이에 관해서는 나는 생략하지만 css parsing 에서 자세히 볼 수 있다.

 

 


4.  Render Tree Construction

 

 DOM 트리가 구축되는 동안 브라우저는 render tree를 구축한다. 이 트리는 표시해야 할 순서와 문서의 시각적인 구성 요소를 갖고 있다. 이 트리의 목적은 컨텐츠가 올바른 순서대로 보여지는 것이다.

 

 Firefox는 렌더 트리의 요소를 'frame', WebKit은 'renderer'이나 'render object'라고 부른다. 렌더러는 자기 자신과 자식 요소들을 어떻게 배치하고 그려내야하는지 알고 있다. WebKit의 RenderObject라는 클래스는 렌더러의 기본 클래스이다. 

class RenderObject{
  virtual void layout();
  virtual void paint(PaintInfo);
  virtual void rect repaintRect();
  Node* node;  //the DOM node
  RenderStyle* style;  // the computed style
  RenderLayer* containgLayer; //the containing z-index layer
}

 각 렌더러는 CSS2 명세에 따라 노드의 css 박스에 부합하는 사각형의 영역을 표시한다. 이는 너비, 높이, 위치 같은 기하학적 정보들을 포함한다. 박스 유형은 스타일 속성의 'display'에 영향을 받는다. 

 

Render 트리와 DOM 트리의 연관성

 렌더러들은 DOM 요소들과 1:1 관계가 아니다. 비시각적인 DOM 요소들은 render 트리에 추가되지 않는다. (ex: head 요소) 또한 'display: none'인 요소들도 트리에 나타나지 않는다. (visibility: hidden인 값은 트리에 추가됨)

 

 여러 시각 객체와 대응하는 DOM 요소도 있는데, 이들은 보통 단일 사각형으로 설명할 수 없는 복잡한 구조를 가졌다. 예를 들어 'select' 요소는 표시 영역, 드롭다운 목록, 버튼 표시를 위한 3개의 렌더러를 가진다. 한 줄로 표시할 수 없는 문자가 여러 줄로 바뀔 때 새 줄에 대해서는 별도의 랜도로로 추가된다.

 

 어떤 렌더 객체는 DOM 노드와 대응하지만 트리의 동일한 위치에 존재하지는 않는다. 'float'이나 'absolute' 처리된 요소는 흐름에서 벗어나 트리의 다른 부분에 놓여진다. 대신 placeholder가 원래 있어야 할 곳으로 배치된다.

DOM 트리 - 렌더 트리 대응

 


5. Layout

 

 렌더러가 생성되고 트리에 추가될 때, 위치나 크기에 대한 정보를 갖지 않는다. 이 값들을 계산하는 것은 layout이나 reflow라고 부른다. (배치) 

 

 html은 흐름 기반의 배치 모델을 사용한다. 즉, 대부분의 경우 단일 경로를 통해 크기나 위치 정보를 계산할 수 있다. 일반적으로 나중에 나타난 요소들은 이전에 나타난 요소들의 위치, 크기의 요소들에 영향받지 않는다. 그래서 배치는 왼쪽->오른쪽, 위->아래로 흐른다. (단, 테이블은 하나 이상의 경로들을 고려하기 때문에 예외임)

 

 좌표계는 루트 프레임을 기준으로 한다. 윗쪽과 왼쪽을 기준으로 좌표가 사용된다. (ex: 0부터 100까지의 수치로 위치를 표현한다면 왼쪽일수록 0에 가깝고, 오른쪽일수록 100에 가깝다.)

 

 배치는 재귀적인 작업이다. <html> 요소에 해당하는 루트 렌더러에서 시작하며 재귀적으로 배치에 필요한 각 렌더러에 대한 위치, 크기에 대한 정보를 계산한다. 루트 렌더러의 좌표는 0, 0이며 이 치수는 창의 영역에 해당하는 뷰포트 만큼의 면적을 갖는다.

 

 모든 렌더러들은 'layout'이나 'reflow' 메소드를 갖는다. 각 렌더러들은 배치해야 하는 자식들의 배치 메소드들을 호출한다.

 

배치 과정

  1. 부모 렌더러가 자신의 너비 결정
  2. 자식 렌더러의 x, y를 배치
    필요 시 자식 배치를 호출해 자식의 높이를 계산
  3. 부모가 자신의 높이를 설정    // 자식의 누적된 높이와 여백의 높이를 사용
  4. 더티 비트 플래그 제거

 

🔻 더티 비트란?

더보기
더보기

 작은 변화가 있을때마다 모든 배치를 바꾸지 않기 위해 브라우저는 '더티 비트'를 사용한다. 재배치가 필요한 수정이나 추가 요소가 있는 렌더러들은 그들 자신과 자식들을 'dirty'라고 표시한다.

 

 'dirty'와 '자식이 dirty' 2가지 플래그가 존재한다. 자식이 dirty하다는 것은 본인은 괜찮지만 자식 중 최소 하나 이상을 재배치 할 필요가 있다는 의미이다.

 


6. Painting

 

 그리기 단계에서는 렌더 트리가 탐색되고 화면에 컨텐츠를 보여주기 위해 렌더러의 paint() 메소드가 호출된다. 그리기는 UI 기반의 구성 요소를 사용한다.

 

 그리기는 배치(layout)와 같이 전역적이고 점진적이다. 점증 그리기에서, 일부 렌더러들은 전체 트리에 영향을 주지 않는 방식으로 변경된다. 이 변경된 렌더러들은 화면 위의 사각형들을 무효화하는데, 이는 OS가 무효화한 사각형을 '더티 영역'으로 보고 paint 이벤트를 발생시키기 하기 위함이다. OS는 여러 영역을 하나로 합쳐 효과적으로 처리한다. (크롬은 렌더러가 별도의 처리 과정이기 때문에 좀 더 복잡하다.) 프레젠테이션은 이런 이벤트에 주의하며 렌더의 루트로 메시지를 전달한다. 트리는 적절한 렌더러에 이를 때까지 탐색되고 (보통 자식과 함께) 스스로 다시 그려진다.

 

그리기 순서

 CSS 2가 정의한 그리기 과정의 순서이다. 실제로 요소가 stacking contexts에 쌓이는 순서이며, 스택은 '뒤 -> 앞'으로 그려지기 때문에 이 순서가 그리기에 영향을 미친다. 블록 렌더러가 쌓이는 순서는 아래와 같다.

  1. 배경 색; background color
  2. 배경 이미지; background image
  3. 테두리; border
  4. 자식; childern
  5. 아웃라인; outline

 


7. Dynamic Changes

 

 브라우저는 변경 사항이 발생하면 최소한의 동작만 하려고 노력한다. 그래서 요소의 색에 변화가 있으면 해당 요소에 대해서만 리페인팅 된다. 요소의 위치가 바뀌면 해당 요소와 그의 형제 자식들의 재배치 및 리페인팅이 발생한다. DOM 노드를 추가하면 노드의 배치와 리페인팅이 발생한다. 'html' 요소의 글꼴 크기 변경 같은 큰 변경은 캐시를 무효화하고 트리 전체의 배치와 리페인팅이 발생한다.

 

 


참고 문서

  1. Mozilla - what is a browser
  2. How Browsers Work - Behind the scenes of modern web browser
반응형

'Develop > CS' 카테고리의 다른 글

NoSQL이란?  (0) 2021.11.18
What is Hosting? 호스팅이란?  (0) 2021.10.28
페이스북이 멈췄다? (DNS와 BGP에 대해)  (0) 2021.10.05
[Oracle] SQL문으로 주말 구하기  (1) 2021.08.30
[Oracle] ORA-01476: 제수가 0 입니다.  (0) 2021.08.09