웹을 공부하면서 빼놓을 수 없는 부분이 있다면 바로 브라우저일 것이다. 브라우저는 사용자가 선택한 HTML 문서, PDF, 이미지 파일 등의 리소스를 서버에 요청하고, 서버로부터 리소스를 전달 받아 우리에게 익숙한 웹 페이지의 형태로 보여주는 역할을 한다. 그렇다면 브라우저가 내부적으로는 어떤 원리로 웹 페이지를 해석하는 것일까? 지금부터 이에 대해 좀 더 자세히 알아보고자 한다.
브라우저의 동작 원리에 대해 이해하기 전에, 브라우저가 어떻게 구성되어 있는지 알 필요가 있을 것이다. 위의 그림처럼 브라우저는 크게 사용자 인터페이스, 브라우저 엔진, 렌더링 엔진, 자료 저장소, 통신, 자바스크립트 해석기, UI 백엔드로 구성되어 있다. 각각의 구성 요소를 자세히 살펴보면 다음과 같다.
사용자 인터페이스는 바로 위의 사진 그 자체다. 실제로 웹 페이지를 보여주는 창을 제외하고 주소 표시줄, 이전 버튼과 다음 버튼, 북마크 메뉴, 홈 버튼 등 사용자가 브라우저와 상호작용할 수 있는 UI를 일컫는다.
브라우저 엔진은 사용자 인터페이스와 렌더링 엔진 사이를 연결해주는 다리 역할을 하는 요소이다. 중간에 위치하여 사용자 인터페이스의 입력에 따라 렌더링 엔진의 동작을 제어한다. 또한 브라우저 엔진은 자료 저장소를 참조하여 로컬 저장소에 데이터를 쓰고 읽는 다양한 작업을 수행하기도 한다.
자료 저장소는 브라우저 자체의 메모리를 활용하여 서버가 아닌 클라이언트, 즉 브라우저에 데이터를 저장할 수 있도록 하는 기능을 담당한다. HTML5에서는 기존에 사용하던 쿠키의 한계점을 보완하기 위해 로컬 스토리지, 세션 스토리지와 같은 웹 스토리지가 등장했다.
지금 살펴볼 렌더링 엔진이 바로 브라우저 동작 원리의 핵심 요소라고 할 수 있다. 브라우저별로 Webkit, Blink, Gecko 등의 다양한 렌더링 엔진을 사용하고 있다. 웹 서버가 전달한 HTML, XML, 이미지 등을 브라우저에 구현하는 역할을 바로 이 렌더링 엔진이 수행한다. 구체적으로는 HTML 문서를 응답 받아서 사용자가 시각적으로 볼 수 있게끔 HTML과 CSS를 파싱하는 역할을 하는 것이다. 여기서 알고 가야 할 중요한 개념이 있다. 바로 파싱이다.
💡 파싱(Parsing)이란?
: 브라우저가 코드를 이해하고 사용할 수 있는 구조로 문서를 분석하고 변환하는 과정
: 여기서의 구조를 파싱 트리(Parsing Tree)라고 하는데, 문서 내용을 토큰으로 분석한 후 문법적인 의미와 구조를 반영한 것
: HTML과 CSS를 파싱하면 각각 DOM(Document Object Model), CSSOM(CSS Object Model)을 생성
: 구체적으로는 파서가 자료를 토큰(요소)로 분해하는 어휘 분석과 언어의 구문 규칙(문법)을 적용하는 구문 분석, 두 가지의 일을 반복함
ex) HTML 문서의 토큰은 시작 태그, 종료 태그, 속성과 값 등
: 토큰이 문법과 일치하면 토큰에 해당되는 노드를 파싱 트리에 추가하는 방식으로 작동함: 파싱 트리가 생성되고 나면 최종적으로 브라우저가 이해할 수 있도록 컴파일러는 파싱 트리를 기계 코드로 변환함
다음으로 살펴볼 요소는 통신이다. 통신은 HTTP를 요청하는 등 브라우저가 서버와 통신할 수 있도록 네트워크를 호출하는 역할을 한다. 한 가지 특징이 있다면 통신은 독립적인 인터페이스이기 때문에 각각의 플랫폼 하부에서 개별적으로 실행된다는 점이다.
말 그대로 자바스크립트 파일을 해석하는 역할을 한다. 구체적으로는 렌더링 엔진이 파싱 과정 중에 스크립트 파일을 만나면 문서 파싱을 중단하고 자바스크립트 해석기로 스크립트 파일을 넘긴다. 이때 스크립트 파일에 defer
속성을 추가하면 문서 파싱을 중단하지 않고 마무리한 뒤에 스크립트 파일을 실행할 수 있다.
UI 백엔드는 렌더링 엔진이 최종적으로 생성한 렌더 트리의 각 노드를 확인하여 브라우저 창에 그리는 작업을 수행한다. 플랫폼에서 명시하지 않은 일반 인터페이스로, OS 사용자 인터페이스 체계를 사용한다는 특징이 있다.
앞서 브라우저의 전체 구조를 살펴본 결과, 결국 사용자에게 HTML 문서를 보여주기 위한 핵심 과정은 렌더링 엔진에서 수행하고 있다는 사실을 알 수 있다. 그러므로 렌더링 엔진의 동작 원리를 분석하여 브라우저의 동작 원리를 렌더링 측면에서 심층적으로 이해해보고자 한다.
전체적인 렌더링 과정을 요약하면 위의 그림과 같다. 각각의 과정에 매긴 번호와 함께 렌더링 과정을 순차적으로 이해해보겠다.
(1) HTML 문서 요청
: 먼저 통신을 통해 브라우저가 서버에 HTML 문서를 요청하여 받아옴
(2) DOM 트리 생성
: 렌더링 엔진이 받아온 HTML 문서에 있는 토큰(태그, 속성, 값 등)을 파싱하여 위와 같은 DOM 노드로 변환하고 최종적으로 DOM 트리가 생성됨
(3) CSS 문서 요청
: (1)의 과정과 유사하게 서버로부터 HTML 문서에 사용된 CSS 문서를 요청하여 받아옴
(4) CSSOM 트리 생성
: 렌더링 엔진이 받아온 CSS 문서와 HTML 문서 내부에 포함된 스타일 요소를 파싱하여 최종적으로 위와 같은 CSSOM 트리가 생성됨
: 구체적으로 CSS 파일이 하나의 스타일 시트 객체로 파싱되고, 그 안에 선택자와 선언 객체 등을 하위 요소로 포함하는 CSS 규칙 객체가 존재하는 구조
(5) 렌더 트리 생성
: DOM 트리가 구축되는 동안 렌더링 엔진은 DOM과 CSSOM을 합쳐서 렌더 트리를 생성함
: 이때 생성되는 렌더 트리는 문서를 시각적인 구성 요소로 만들어주는 역할
: 위의 그림에서 우측이 바로 렌더 트리의 구조, 색상 또는 면적과 같은 시각적 속성이 있는 사각형을 포함💡 렌더 객체
: Webkit 엔진에서는 렌더 트리의 사각형을 렌더러 혹은 렌더 객체라고 표현하는데, 각각의 렌더 객체는 자신과 자식 요소를 어떻게 배치하고 구현해야 하는지 알고 있음
: 렌더 객체는 DOM 요소에 부합하지만, 그렇다고 DOM 요소와 1:1의 관계는 아님
: 예를 들어head
영역의 태그나display: none
속성을 지정한 DOM 요소는 시각적으로 볼 수 없기 때문에 애초에 렌더 트리에 추가되지 않기 때문
(6) 렌더 트리의 배치
: 렌더 트리의 생성 완료 후 렌더링 엔진이 렌더 트리를 배치하여 레이아웃을 구성함
: 구체적으로는 렌더 트리의 각 노드인 렌더 객체를 화면의 정확한 위치에 표시하는 과정
: 렌더 객체가 처음 생성되어 트리에 추가될 때 위치와 크기 정보가 존재하지 않기 때문에 각각의 값을 계산하는 과정 필요, 이를 배치 혹은 리플로우라고 부름
: 구체적인 배치 과정은 부모 렌더 객체가 자기 자신의 너비를 결정하고, 부모 객체가 자식 렌더 객체의 높이 등을 검토하여 자식 렌더러를 배치한 후padding
,margin
등의 정보까지 합쳐서 부모 자신의 높이를 설정
(7) 그리기
: 배치 과정이 완료된 렌더 트리는 UI 백엔드로 전달되어 탐색된 후 렌더 객체의
paint
메서드가 호출됨
: CSS2에 명시된 바에 따르면 구체적으로는 '배경 색 > 배경 이미지 > 테두리 > 자식 > 아웃라인'의 순서로 그리기 과정이 진행됨
평상시에 아무렇지 않게 접속했던 웹 사이트를 사용자에게 보여주기 위해 브라우저는 내부적으로 얼마나 복잡한 과정을 거치는가. 글을 쓰면서도 느꼈지만 브라우저 스스로 꽤나 체계적이고 복잡한 과정을 거쳐 웹 페이지를 구현하고 있다는 생각이 든다. 물론 브라우저가 불쌍하지는 않다. 불쌍함은 이를 공부하는 개발자들의 몫이리라.
아무튼 엄밀히 따지면 사실 브라우저의 동작 원리는 크게 두 가지 관점으로 구분하여 살펴볼 수 있다. 이번 글은 그 중에서도 웹 페이지의 렌더링 관점에서 브라우저 동작 원리를 살펴본 것이다. 또 다른 관점인 네트워크 중심으로 브라우저가 어떻게 동작하는지는 추후 다른 글에서 다뤄보도록 하겠다.
🙏 출처
https://d2.naver.com/helloworld/59361
https://bbangson.tistory.com/87