기초정보

2. HTML 프론트엔드 개발자라면 알고 있어야 할 브라우저의 동작 과정-2

에프티에치코리아3 2021. 4. 1. 16:30

2. 렌더링 엔진의 동작 과정

우리가 어떠한 웹 페이지에 접속하게 되면, 네트워크를 통해 해당 웹 페이지의 HTML 문서를 얻어올 수 있습니다. 그러면 렌더링 엔진은 위의 다이어그램과 같은 과정을 거쳐 읽어들인 HTML 문서를 해석합니다. 브라우저 엔진마다 해석 방식이 조금씩 다를 순 있지만, 크게 다음과 같은 네 단계로 이루어져 있다고 봐도 무방합니다.

 

  • 파싱(Parsing)
  • 렌더 트리(Render Tree) 구축
  • 레이아웃(Layout) 또는 리플로우(Reflow)
  • 페인트(Paint)

위에서 이야기한 모든 과정들을 일컫어 우리는 중요 렌더링 경로(Critical Rendering Path)라고 부릅니다. 각 단계에서 리소스를 로드하는 순서나 작성한 스크립트의 내용에 따라 웹 페이지의 반응 속도가 달라질 수 있습니다. 이러한 과정의 최적화를 통해 프론트엔드 개발자는 렌더링에 걸리는 시간을 개선시키고, 사용자 경험을 방해하지 않을 수 있습니다.

지금부터는 중요 렌더링 경로의 각 단계에 대해 자세히 알아보도록 하겠습니다.

 

1)파싱

파싱(Parsing) 토큰화(tokenize)된 코드를 구조화하는 과정을 말합니다. 이러한 파싱 과정을 전문적으로 해주는 부분을 파서(Parser)라고 부릅니다.

정확하게는 어휘분석기(Lexical scanner, Lexer)를 통해 토큰화된 코드가 생성되고, 이를 파서가 해석하는 순서로 이루어집니다. 토큰화라는 것은 의미가 있는 최소 단위로 코드를 쪼개는 것을 의미합니다. 가령 <div></div> 라는 코드를 토큰화하면 ['<','div','>','</','div','>'] 처럼 나타낼 수 있죠.

파싱 과정은 입력받은 문자열이 정해진 문법(grammar)들을 모두 따르는지를 확인하는 과정입니다. 각 문법은 어휘(vocabulary) 문법 규칙(syntax rule)으로 구성이 되어 있는데요. 어휘는 알파벳이나 한글처럼 사용할 수 있는 단어와 그 조합들을 의미하며, 문법 규칙은 어휘 사이에 적용될 수 있는 규칙들을 의미합니다. 가령 숫자의 계산에서 곱하기는 앞뒤로 정수가 와야 하고, 더하기보다 더 높은 우선순위를 갖고 있는 것 같은 규칙들처럼요.

브라우저는 HTML, CSS, JavaScript 세 종류의 언어를 해석할 수 있습니다. 그 중에서 JavaScript는 렌더링 엔진 레이어가 아니라 JavaScript 해석기라는 별도의 레이어에서 언어를 해석합니다. 따라서 렌더링 엔진에서는 HTML과 CSS를 파싱합니다.

 

1-1)  HTML 파싱

파싱 흐름도

브라우저는 위에서 이야기한 토큰화된 HTML의 문자열들을 이용해 파스 트리(Parse Tree)를 생성합니다. 파스 트리는 브라우저가 읽어야 할 HTML 코드를 트리 모양으로 구조화하여 나타낸 것입니다.

이러한 파스 트리를 이용해서 렌더를 바로 할 수 있을까요? 그렇지 않습니다. 브라우저는 파스 트리를 이용해 DOM(Document Object Model) 트리를 새로 만들기 때문입니다. 그렇다면 두 트리의 차이점은 무엇일까요?

파스 트리는 토큰화된 문자열을 단순하게 구조화한 트리에 불과했지만, DOM 트리는 우리가 실제로 상호작용할 수 있는 HTML 엘리먼트로 이루어진 트리입니다. 따라서 우리가 실제로 JavaScript로 상호작용할 수 있는 부분은 DOM 트리죠.

한편, HTML 파서는 다른 파서와 비교했을 때 조금 독특한 특징을 갖고 있습니다. HTML 파서의 첫 번째 특징은 오류에 너그러운(forgiving nature) 속성입니다. 다시 말해, HTML을 파싱하는 도중 어떠한 에러가 발생한다면, 브라우저는 자체적으로 에러를 복구하려 합니다. 아래와 같은 HTML 코드를 생각해봅시다.

 

<body> <p class=highlight>Hello <div><span>World

위 예제는 제대로 작성되지 않은 HTML 코드를 나타냅니다. 최상단에 <html> 태그를 쓰지 않았고, <body>, <p>, <div>, <span> 태그 같은 경우에는 닫는 태그를 작성하지 않았습니다. 또한 클래스 어트리뷰트를 쌍따옴표로 묶어주지도 않았죠. 하지만 이 HTML 코드를 실제로 브라우저에서 실행시켜보면 다음과 같이 완성된 코드가 나옵니다.

 

<body> <p class="highlight">Hello</p> <div><span>World</span></div> </body>

이러한 규칙들은 HTML Document Type Definition (DTD)에 의해 정의되고 있습니다. HTML 파서는 명세된 규칙들을 따르는 예외 처리를 따로 해주어야 합니다. 그리고 이는 일반적인 파서의 규칙만으로는 적용하기가 어렵습니다.

정확하게는 대부분의 프로그래밍 언어가 촘스키 계층 문맥 자유 문법(Context-free grammar)에 속하는 것에 반해, HTML은 자체의 특징 때문에 위 계층에 속하지 않기 때문입니다.

파싱 과정 중단 가능

 

HTML 파서의 두 번째 특징는 파싱 과정이 중단될 수 있다는 것입니다. HTML은 파싱 도중 <script>, <link> 같은 외부 태그를 만나게 되면 HTML 파싱을 즉시 중단합니다. 그리고 해당 태그의 해석을 실행하죠. 만약 해당 태그가 외부 파일을 참조하고 있다면 다운로드를 한 후 해석을 시작합니다.

 

이는 네트워크를 통해 먼저 받아온 코드부터 해석을 실행할 수 있는 HTML과는 달리 외부 컨텐츠들은 증분적(Incrementally)으로 해석을 할 수 없기 때문입니다. 또 다른 이유는 <script>에 DOM을 직접 수정할 수 있는 내용이 있을 수도 있기 때문입니다. 가령 document.write() 같은 API를 사용하면 HTML을 파싱하고 있는 도중에도 DOM 엘리먼트를 동적으로 삽입할 수 있습니다. 이로 인해 외부 컨텐츠를 해석하고 실행하기까지 HTML의 파싱은 중단됩니다.

 

이러한 문제점을 해결하기 위해서 스크립트 같은 경우에는 별도의 옵션을 제공합니다.

여기에 대해서는 지난 번에 작성한 글인 스크립트의 실행 시점을 조절하는 Async와 Defer 속성가 있으니, 관심이 있으신 분은 참고해주세요.

일부 브라우저에서는 예측 파싱(Speculative parsing) 기법을 이용해 별도의 쓰레드에서 외부 스크립트, 링크, 스타일 등을 불러오기도 합니다.

HTML 파서의 세 번째 특징은 재시작(Reentrant)입니다.

위에서 말한 것처럼 HTML의 파싱 과정은 어떠한 외부의 요인으로 인해 방해받을 수 있습니다. 파싱 중간에 외부의 요인으로 인해 DOM이 추가, 변경, 삭제 될 수 있습니다.

이러한 경우에 HTML은 처음부터 다시 파싱 과정을 거칩니다. 즉, 바이트를 문자로 변환하고, 토큰을 식별한 후 노드로 변환하고 DOM 트리를 빌드합니다. 이 때문에 처리해야 할 HTML이 많을 때에는 파싱 시간이 오래 걸릴 수 있습니다.

 

 

1-2) CSS 파싱

CSSOM

한편 CSS 파싱은 공식적인 명세가 있기 때문에, 파싱 과정이 HTML에 비해 그렇게 복잡하지는 않습니다.

일반적으로 CSS을 링크하는 코드가 HTML 코드 내에 삽입되어 있기 때문에, HTML을 파싱하는 도중에 CSS 파싱이 시작됩니다. 네트워크를 통해 먼저 받아온 코드부터 해석을 실행할 수 있는 HTML 파서와는 달리, CSS 파서는 전체 파일을 모두 다운로드할 때까지 파싱을 시작할 수 없습니다.

전체 CSS 파일을 다운로드 한 후 CSS 파싱 과정이 끝나게 되면, 코드에서 명세한 내용과 순서를 바탕으로 DOM과 같은 트리를 구성하는데 이를 CSSOM(CSS Object Model) 트리라 부릅니다. 이 트리에는 스타일, 규칙, 선택자 등의 정보가 노드에 들어가게 됩니다.