브라우저의 주요 기능은 사용자가 입력한 자원을 서버에 요청하고 사용자에게 보여주는 것입니다. 여기서 자원은 보톤 HTML 문서지만 PDF나 이미지 또는 다른 형태일 수도 있습니다. 자원의 주소는 URI(Uniform Resource Identifier)에 의해 정해집니다.
브라우저는 HTML과 CSS 명세에 따라 HTML 파일을 해석해서 표시하는데 이 명세는 웹 표준화 기구인 W3C(World Wide Web Consortium)에서 정합니다. 과거에는 브라우저들이 일부만 이 명세에 따라 구현하고 나머지는 독자적인 방법으로 확장해 웹 제작자가 심각한 호환성 문제를 겪었지만 최근에는 대부분의 브라우저가 표준 명세를 따릅니다.
일반적으로 브라우저의 사용자 인터페이스는 아래와 같습니다.
렌더링 엔진의 역할은 서버로 부터 받은 응답 내용 즉 HTML을 브라우저 화면에 표시하는 일입니다. 렌더링 엔진은 HTML 및 XML 문서와 이미지를 표시할 수 있습니다. 렌더링 엔진은 HTTP 통신을 통해 요청한 문서의 내용을 얻는 것으로 시작하는데 문서의 내용은 보통 8KB 단위로 전송됩니다.
모든 브라우저는 고유한 렌더링 엔진이 있습니다. 브라우저마다 렌더링 엔진이 다르기 때문에 사용자 화면에서 웹 페이지를 해석하는 방식이 다 다릅니다.(프론트 엔드 개발자가 고통 받는 이유…) 여기서 브라우저간 호환성 문제가 발생합니다. 때문에 우리 웹 개발자에게(특히 프론트 엔드) 여러 브라우저에서 기능 및 디자인 측면에서 웹 애플리케이션의 일관성을 확인하는 크로스 브라우저 테스트는 필수라고 할 수 있습니다. 대표 브라우저의 렌더링 엔진은 아래와 같습니다.
렌더링 엔진이 어떻게 동작하는지만 알면 브라우저의 동작 원리를 모두 알 수 있습니다. 렌더링 엔진 기본 동작 과정은 아래와 같습니다.
이제 각 단계에 대해 자세히 알아보겠습니다.
파싱은 렌더링 엔진에서 매우 중요한 과정이기 때문에 조금 더 자세히 살펴보겠습니다. 그렇다고 너무 자세히 파보면 끝도 없기 때문에 브라우저 렌더링 과정을 이해할 정도까지만 살펴보겠습니다.
언어학에서 파싱은 구문 분석이라고합니다. 문장을 이루고 있는 구성 성분으로 문장을 분해하고, 구성 성분들 사이의 위계 관계를 분석하여 문장의 구조를 결정하는 것을 말합니다. 컴퓨터 과학에서 파싱은 일련의 문자열을 의미있는 토큰(token, 어휘 분석 단위)으로 분해하고 이들로 이루어진 파스 트리(parse tree)를 만드는 과정을 말합니다. 쉽게말해 파싱은 개발자가 작성한 코드를 컴퓨터가 이해할 수 있도록, 코드를 토큰이라는 단위로 쪼개고 구조화하는 과정이라고 생각하시면 됩니다.
브라우저는 서버로부터 HTML을 응답받으면 먼저 문서 파싱을 통해 브라우저가 HTML 코드를 이해하고 사용할 수 있는 구조로 변환하는 작업을 진행합니다. 파싱 결과는 보통 문서 구조를 나타내는 노드 트리인데 파스 트리(parse tree) 또는 문법 트리(syntax tree)라고 부릅니다.
파싱을 행하는 프로그램을 파서라고하는데, 파서는 보통 두 가지 일을 합니다.
파싱은 어휘 분석기를 통해 토큰화 된 코드가 생성되고, 이를 파서가 해석하는 순서로 이루어집니다. 토큰화라는 것은 의미가 있는 최소 단위로 코드를 쪼개는 것을 의미합니다. 예를 들어 <div></div>
라는 코드를 토큰화하면 ['<', 'div', '>', '</', 'div', '>']
처럼 나타낼 수 있습니다.
우리가 외국어를 배울 때 그 언어의 단어와 문법을 배우고, 그에 따라 언어를 말하고 해석할 수 있습니다. 파싱도 이와 같습니다. 파싱은 개발자가 작성한 코드가 해당 프로그램 언어의 문법(grammer)을 모두 따르는지 확인하는 과정입니다. 대부분의 프로그래밍 언어는 문맥 자유 문법이라는 것에 속하며 파서는 이 문맥 자유 문법에 따라 문서를 파싱합니다.
문법은 어휘(vocabulary)와 문법 규칙(syntax rule)으로 구성이 되어 있습니다. 어휘는 알파벳이나 한글처럼 사용할 수 있는 단어와 그 조합들을 의미하며, 문법 규칙은 어휘 사이에 적용될 수 있는 규칙들을 의미합니다. 가령 숫자의 계산에서 곱하기는 앞뒤로 정수가 와야 하고, 더하기보다 더 높은 우선순위를 갖고 있는 것 같은 규칙들처럼요.
문법은 어휘와 문법 규칙으로 구성되어 있기 때문에 파싱도 이 두 가지로 구분할 수 있습니다.
파싱은 아래의 과정이 반복됩니다.
파싱 결과는 파스 트리라고 설명했습니다. 그러나 이 파스 트리는 최종 결과물이 아닙니다. 파싱은 보통 문서를 다른 문서로 변환합니다. 컴파일로 예를 들면 소스 코드를 기계 코드로 변환시키는 컴파일러는 파스 트리 생성 후 이를 기계 코드 문서로 변환합니다.
파서는 기본적으로 하향식 파서와 상향식 파서가 있습니다.
파서는 파서 생성기라는 도구를 통해 만들어집니다. 이 파서 생성기에 언어의 어휘나 구문 규칙 같은 문법을 부여하면, 파서 생성기에 해당 언어에 맞는 파서를 생성해줍니다. 파서를 생성하는 것은 파싱에 대한 깊은 이해를 필요로하고, 개발자가 수동으로 파서를 최적화하여 생성하는 것은 쉬운일이 아니기 때문에 파서 생성기는 매우 유용합니다.
웹킷은 어휘 생성을 위한 플렉스(Flex)와 파서 생성을 위한 바이슨(Bison), 이 두 가지를 파서 생성기로 사용합니다.
드디어 파싱의 기본에 대한 설명이 끝났습니다. 그렇다면 이제 브라우저의 렌더링 엔진이 어떻게 HTML을 파싱하는지 알아보겠습니다.
브라우저는 HTML, CSS, Javascript 이 세 엔어를 해석할 수 있습니다. 그 중 Javascript는 렌더링 엔진이 아니라 Javascript 해석기라는 별도의 레이어에서 해석됩니다.(브라우저 기본구조 참고) 렌더링 엔진에서는 HTML과 CSS만 파싱 되므로, 이 두 부분에 대해서만 살펴보겠습니다.
HTML 파서는 HTML 마크업을 파싱 트리로 변환합니다. 그런데 이 HTML 파서는 우리가 앞서 살펴본 전통적인 파서와 다릅니다. 모든 전통적인 파서는 HTML에 적용 할 수 없습니다. 엥? 그러면 우리는 파서에 대해 괜히 살펴 본걸까요? 아니요 그렇지 않습니다. 우리가 전통적인 파서는 HTML에는 적용할 수 없지만 CSS와 자바스크립트에는 적용할 수 있습니다. 또한 전통적인 파서는 같은 마크업 언어인 XML과 HTML을 XML형태로 재구성한 XHTML에도 적용할 수 있습니다. 그렇다면 대체 왜 HTML에만 적용할 수 없는 걸까요? 이걸 이해하기 위해서는 먼저 DOM에 대해서 알아야 합니다.
앞에서 브라우저는 HTML 문서를 토큰화해 파스 트리를 생성한다고 했습니다. 파스트리는 브라우저가 읽어야할 HTML 코드를 트리모양으로 구조화해 나타낸 것입니다. 그러나 이 파스 트리로는 화면을 그릴 수 없습니다. 왜냐하면 브라우저는 파스 트리를 이용해 DOM(Document Object Model) 트리를 새로 만들기 때문입니다. 그렇다면 파스 트리와 DOM 트리의 차이는 무엇일까요?
파스트리는 토큰화한 문자열을 단순하게 구조화한 트리입니다. 그러나 DOM은 HTML 문서의 객체 표현으로 마크업과 1:1의 관계를 갖고있습니다. DOM 트리는 우리가 실제로 상호작용할 수 있는 HTML 엘리먼트로 이루어진 트리이기 때문에 우리가 실제로 자바스크립트로 상호작용할 수 있는 부분입니다.
앞에서 파서는 문맥 자유 문법에 따라 파싱을 한다고 말씀드렸습니다. 그런데 HTML이 문맥 자유 문법이 아닙니다. 이 말도 조금 어렵죠? 이것도 조금 더 쉽게 표현해 보겠습니다. HTML은 암묵적으로 태그에 대한 생략이 가능합니다. 개발자가 실수로 시작 혹은 종료 태그 등을 생략해도 정상 작동합니다. 전반적으로 엄격한 XML에 비해 HTML은 너그럽고 유연한 문법을 갖고 있습니다. 이게 바로 HTML이 문맥 자유 문맥이 아니라는 뜻입니다. HTML은 이러한 너그러운 문법때문에 개발하기 편하고 인기가 많지만, 공식적인 문법으로 HTML을 작성하기 어렵다라는 문제가 있습니다. 정리하자면 HTML은 파싱하기 어렵고 전통적인 구문 분석이 불가능하기 때문에 문맥 자유 문법이 아니라는 것이며 이 때문에 XML 파서로도 파싱하기 쉽지 않습니다.
HTML을 작성하면서 유효하지 않은 구문(invalid syntax) 관련 에러를 본적이 없을 겁니다. 이는 브라우저가 요류를 교정하기 때문입니다. 아래의 오류가 가득한 HTML이 있다고 가정해보겠습니다.
<body>
<p class=highlight>Hello
<div><span>World
하지만 이 코드를 실제로 브라우저에서 실행하면 아래와 같은 결과가 나옵니다.
<body>
<p class="highlight">Hello</p>
<div><span>World</span></div>
</body>
이러한 규칙들은 HTML Document Type Definition (DTD)에 의해 정의어 있습니다. HTML 파서는 명세된 규칙들을 따르는 예외 처리를 따로 해주어야 하는데, 이는 일반적인 파서의 규칙만으로는 적용하기 어렵습니다.
브라우저는 HTML 파싱 도중 <script>
, <link>
같은 외부 태그를 만나게 되면 HTML 파싱을 즉시 중단합니다. 그리고 해당 태그의 해석을 실행합니다. 만약 해당 태그가 외부 파일을 참조하고 있다면 다운로드를 한 후 해석을 시작합니다. 이는 네트워크를 통해 먼저 받아온 코드부터 해석을 실행할 수 있는 HTML과는 달리 외부 컨텐츠들은 증분적(Incrementally)으로 해석을 할 수 없기 때문입니다. 또 다른 이유는 <script>
에 DOM을 직접 수정할 수 있는 내용이 있을 수도 있기 때문입니다. 가령 document.write()
같은 API를 사용하면 HTML을 파싱하고 있는 도중에도 DOM 엘리먼트를 동적으로 삽입할 수 있습니다. 이로 인해 외부 컨텐츠를 해석하고 실행하기까지 HTML의 파싱은 중단됩니다.
이 문제를 해결하기 위해 웹킷과 파이어폭스는 예측 파싱 기법을 이용해 별도의 쓰레드에서 외부 스크립트, 링크, 스타일 등을 불러오기도 합니다.
앞서 말씀드린 것처럼 HTML 파싱은 어떠한 외부 요인으로 인해 방해받을 수 있습니다. 파싱 중간에 외부 요인으로 인해 DOM이 추가, 변경, 삭제가 될 수 있습니다. 이 경우 HTML은 처음부터 다시 파싱을 거칩니다. 즉, 바이트를 문자로 변환하고, 토큰을 식별한 후 노드로 변환하고 DOM 트리를 빌드합니다. 이 때문에 처리해야 할 HTML이 많을 때에는 파싱 시간이 오래 걸릴 수 있습니다.
CSS는 HTML과 달리 문맥 자유 문법이고 공식적인 명세가 있기 때문에 일반적인 파서로 파싱이 가능합니다. 즉 HTML에 비해 파싱과정이 복잡하지 않습니다.
일반적으로 CSS을 링크하는 코드가 HTML 코드 내에 삽입되어 있기 때문에, HTML을 파싱하는 도중에 CSS 파싱이 시작됩니다. 네트워크를 통해 먼저 받아온 코드부터 해석을 실행할 수 있는 HTML 파서와는 달리, CSS 파서는 전체 파일을 모두 다운로드할 때까지 파싱을 시작할 수 없습니다. 전체 CSS 파일을 다운로드 한 후 CSS 파싱 과정이 끝나게 되면, 코드에서 명세한 내용과 순서를 바탕으로 DOM과 같은 트리를 구성하는데 이를 CSSOM(CSS Object Model) 트리라 부릅니다. 이 트리에는 스타일, 규칙, 선택자 등의 정보가 노드에 들어가게 됩니다.
앞서 한 설명을 토대로 브라우저 렌더링 엔진이 기본 동작 과정 중 파싱 단계 부분을 간단히 정리해보겠습니다.
파싱 설명이 너무 길어서 프론트 편은 여러 편으로 나눠 설명하겠습니다.
출처
https://d2.naver.com/helloworld/59361
https://bythem.net/2021/04/23/프론트엔드-개발자라면-알고-있어야-할-브라우저의-2
https://na27.tistory.com/230