avatar
Published on

HTML 문서에서 DOM으로의 여정

Author
  • avatar
    Name
    yceffort

Table of Contents

Introduction

이전 글에서는 브라우저에서 서버로 URL이 전송되었을 때 어떻게 처리하는지, 그리고 관련 리소스 전달을 위해 어떻게 최적화 되고 있는지 등에 대해 알아보았다. 이제 데이터가 왔으니, 브라우저 엔진이 이 리소스를 렌더링하여 웹 페이지로 만들어야 한다. 어떻게 하면 HTML을 화면에 만들 수 있는 페이지로 만드는지 살펴보자.

Parsing

네트워크를 통해서 서버에서 클라이언트로 리소스가 전달되면 이를 변환하는 작업이 필요하다. 가장 첫번째로 일어나는 일은 HTML 파서로, 여기에서 인코딩, pre-parsing, 토큰화 (tokenization), 트리 구조 변환 등을 처리한다.

1. Encoding

http 응답은 HTML 텍스트에서 이미지에 이르기 까지 모든 것이 될 수 있다. 파서가 첫번째로 해야 하는 일은 방금 응답으로 밭은 바이트를 해석하는 방법을 알아내는 것이다. HTML 문서를 처리한다고 가정해보자. HTML 문서를 처리하기전에, 디코더는 텍스트 문서가 어떻게 바이트로 변환되었는지 확인해야 한다.

텍스트도 사실 컴퓨터에서 바이너리로 변환을 해야 컴퓨터가 읽을 수 있다는 사실을 기억해야 한다.

텍스트를 어떻게 디코딩 해야 하는지 알아내는 것은 브라우저가 해야할 일이다. 서버는 Content-Type 헤더로 브라우저에 이 콘텐츠에 대한 힌트를 줄 수 있으며, BOM을 통해서 맨 앞 비트를 가지고 분석을 할 수도 있다. 그럼에도 브라우저가 인코딩 할 수 없을 경우에는, 브라우저는 휴리스틱을 활용하여 최선의 인코딩을 분석할 수 있다. 혹은 html 태그에서 <meta /> 태그로 인코딩된 콘텐츠에서 발견할 수도 있다. 최악의 경우, 브라우저가 일단 추측을 한다음, 파싱이 본격적으로 시작된 후로 이 인코딩에 대한 정보가 담겨있는 <meta /> 태그를 발견할 수도 있다. 이러한 경우에는 이전까지 디코딩한 콘텐츠를 다 버리고 다시 처음부터 시작해야 한다. 브라우저는 종종 오래된 웹 콘텐츠 (레거시 인코딩으로 된)를 처리할 때가 있는데, 이러한 페이지들이 이런 방식으로 처리되고 있다.

2. Pre-parsing 및 scanning

인코딩을 확인하게 되면, 추가 리소스에 대한 왕복 딜레이를 최소화 하기 위해, 콘텐츠를 스캔하기 위한 initial pre-parsing을 시작하게 된다. 이 pre-parser는 완전한 파서로 보기는 어렵다. 왜냐하면 HTML이 얼마나 중첩되어 있는지, 그리고 부모-자식 관계는 무엇인지 확인하지 못하기 때문이다. 하지만 특정 HTML 태그의 속성 등을 파악할 수는 있다. 예를 들어 HTML 콘텐츠 어딘가에

<img src="https://somewhere.example.com/images/dog.png" alt=">

가 있다고 가정하자.

이 pre-parser는 이 src의 값을 확인하고 이 리소스 값을 리소스 요청 대기열에 집어 넣어준다. 이렇게 함으로써 이미지를 최대한 빨리 요청할 수 있고, 이미지가 도착하는데 까지 걸리는 시간을 최소화 할 수 있다. 이외에도 preloadpre-fetch 지시자와 같은 것들을 확인하여 대기열에 집어 넣어줄 수 있다.

Tokenization

토큰화는 HTML 파싱 과정 중 하나로, 마크업을 begin tag end tag text run comment 등과 같은 개별 토큰으로 변환하여, 파서의 다음 상태로 만들어 준다. tokenizer는 상태 머신으로, HTML 언어의 서로다른 상태를 처리해준다. |를 이 상태 머신이 처리하는 과정이라고 간주해보자.

  • <|video controls>: 태그가 열려 있는 상태임
  • <video con|trols>: 태그의 controls이라고 하는 속성을 파악
  • <video controls|>: 태그가 닫혀있음.

이렇듯 tokenizer는 문자를 읽을 때 마다 반복적으로 태그의 상태를 파악하는 역할을 한다.

https://i0.wp.com/alistapart.com/wp-content/uploads/2018/10/fig2.png?w=960&ssl=1

HTML 스펙 문서를 살펴보면 tokenizer를 위해서 대략 80여개의 상태를 정의해둔다. 텍스트의 내용이 유효한 HTML 콘텐츠가 아니라도, 텍스트 컨텐츠를 처리하고 HTML 문서로 변환할 수도 있다. 이와 같은 탄력성은 개발자들이 쉽게 웹 개발을 할 수 있도록 해주는 특징이다. 그러나 이러한 탄력성이 예상치 못한 결과를 야기할 수도 있으며, 이로 인해 미묘한 버그가 발생할 수도 있다. HTML validator로 한번 검사하면 이러한 실수를 사전에 방지 할 수 있다.

마크업 언어 정확성에 엄격하게 대응하기 위해, 어떠한 실패라도 렌더링하지 못하게 막는 메커니즘이 있다. 이 parsing module은 HTML을 처리할 때 XML규칙을 사용하며, 문서를 application/xhtml+xml 유형으로 브라우저에 전송하면 된다.

브라우저는 이 pre-parser단계와 tokenization 단계를 최적화를 위해서 한꺼번에 수행할 수도 있다.

3. 파싱 및 트리 구조화

브라우저는 웹 페이지 내부(메모리)에 표현할 무언가가 필요한데, 이를 정의하는 것이 DOM 표준이며, 이 스펙에서 어떻게 어떤 형태로 표현해야하는지 정의한다. 여기서 파서가 할 일은, 이전에 tokenizer가 만든 토큰을 가져와서 적절한 방식으로 만든 다음, Document Object Model(DOM) 객체에 삽입하는 것이다. DOM은 우리가 알다시피 트리 데이터 구조로 생성되므로, 이 프로세스를 트리 구조화 라고도 한다.

놀랍게도 IE는 역사적으로 봤을 때 이를 트리구조로 사용한 적이 별로 없다. https://blogs.windows.com/msedgedev/2017/04/19/modernizing-dom-tree-microsoft-edge/

https://i1.wp.com/alistapart.com/wp-content/uploads/2018/10/fig3.png?w=960&ssl=1

HTML 파싱은 그 구조가 꽤 복잡하다. 그 이유는 앞서 언급한 것 처럼, 레거시 HTML 콘텐츠를 오늘날의 브라우저와 호환 가능하도록 구조를 유지하는 선에서 지원해야 하기 때문이다. 예를 들어, 대다수의 HTML 태그에는 끝 태그 문자 />가 존재한다. 따라서 브라우저는 자동으로 해당 태그와 일치하는 태그를 닫을 수 있다. 아래 예시를 살펴보자.

<p>sincerely<p>The authors</p>

파서는 위와 같은 개같은 구조를 암시적으로 종료 태그로 작성하는 규칙을 가지고 있다. 파서는 위 코드를 아래와 같이 변환한다.

<p>sincerely</p>
<p>The authors</p>

이러한 규칙 덕분에, 자동으로 두 <p/> 태그가 형제 형태로 자리잡을 수 있게 되었다. parser의 규칙 중에서, HTML 테이블은 아마도 적절한 표 구조를 가질 수 있도록 보장하기 위한 가장 복잡한 규칙일 것이다.

이러한 복잡한 파싱 규칙을 일단 거치고 나면, 일단 DOM 트리가 만들어지고 나면 더 이상 이 규칙은 실행되지 않는다. 예를 들어, 자바스크립트를 사용하여 DOM트리르 마구잡이로 이상한 형태로 만들어 낼수도 있다. (비디오 태그에 테이블 셀을 집어 넣는다던지...) 따라서 렌더링 시스템은 이와 같은 모순된 상황에 대처하는 방법을 알아야 한다.

HTML 파싱을 복잡하게 하는 또다른 요인 중 하나는, 파서가 작업을 수행하는 동안 자바스크립트가 또 파싱해야할 콘텐츠를 추가할 수 있다는 것이다. <script/> 태그 내부에는 파서가 수집하고 다시 스크립팅 엔진에 보내야 하는 text를 포함하고 있다. 스크립트 엔진이 스크립트 텍스트를 구분분석하고, 평가하는 동안 parser는 기다린다. 만약 여기에서document.write API를 호출하는 경우, 또다시 HTML 파서가 실행되어야 한다. 건축 과정에 비유하자면, <script/>document.write는 공사작업을 하던 도중에 갑자기 무언가 필요한게 생각 나서 모든 작업을 중단하고 필요한 것을 사러가는 행위다. 이렇게 무언가를 사러나는 동안, 모든 공사작업은 중단된다.

4. 이벤트

parser가 끝나면, DOMContentLoaded라는 이벤트가 실행된다. 이벤트는 자바스크립트가 듣고 응답할 수 있는, 브라우저에 내장된 일종의 브로드캐스팅 시스템이다. DOMContentLoaded와 마찬가지로, 웹 페이지에는 다양한 이벤트 - load (파싱이 수행되고, 이미지, CSS, 비디오 등 파서가 요청한 모든 리소스가 다운로드 됨), unload (웹 페이지가 닫힐 예정임을 의미) - 가 존재한다. 대다수의 이벤트는 사용자가 화면을 터치하는 행위, 마우스를 사용하는 행위, 키보드를 사용하는 행위 등 사용자 입력으로 이루어져있다.

브라우저는 DOM에 이벤트 객체를 만들고, 이와 관련된 유용한 상태 정보 (화면 터치 위치, 눌린 키보드 등)의 정보를 가지고 이벤트를 실행한다. 이벤트를 수신하는 모든 자바스크립트 코드가 이밴트 객체와 함께 실행된다.

DOM 트리 구조는 트리의 모든 레벨(위치)에서 이벤트를 리슨할 수 있도록 함으로서, 코드가 얼마나 자주 이벤트에 자주 반응할지를 필터링할 수 있게 해준다. 브라우저는 먼저 트리에서 이벤트를 실행할 위치 (<input/> 과 같은 DOM 객체)를 결정한 다음, 트리의 루트 부터 시작하여 각 분기 (<input/>에 도달할 때 까지)를 거쳐 루트로 돌아가는 이벤트를 위한 일종의 경로를 계산한다. 격로에 있는 각 객체는 이벤트 리스터를 트리거하여, 결국에는 트리의 루트에 있는 리스너가 많은 이벤트를 볼 수 있게 해준다.

https://i0.wp.com/alistapart.com/wp-content/uploads/2018/10/fig4.png?w=960&ssl=1

이 중 일부 이벤트는 취소 할 수도 있다. 예를 들어, form이 제대로 작성되지 않은 경우 form submit을 취소할 수도 있다.

5. DOM

HTML 은 파서가 처리할 수 있는 마크업의 범위를 훨씬 뛰어 넘는, 많은 기능을 제공한다. 파서는 어떤 element가 다른 element를 가지고 있는지, 어떤 attribute를 가지고 있는지 정도 수준의 구조를 구축한다. 이러한 구조와 상태를 조합하면, 기본적인 렌더링과 사용자가 인터랙션을 할 수 있는 환경을 제공하기에 충분하다. 그러나 CSS와 자바스크립트가 없다면 웹 페이지는 매우 단조로워 보일 것이다. DOM은 HTML의 엘리먼트와 HTML과 전혀 관련 없는 다른 객체들에 추가적인 기능을 제공한다.

element

파서는 트리에 넣을 객체를 만들때, 엘리먼트의 이름 (네임 스페이스)을 조회하고, 객체를 감싸기 위해 이와 일치하는 HTML 인터페이스를 찾는다.

인터페이스는 엘리먼트의 특징에 따라서 몇가지 기능을 추가한다. 이중 몇가지 기능을 사렾보자면

  • 특정 엘리먼트의 하위 항목 전체 또는 일부를 나타내는 HTML Collection에 접근할 수 있는 기능
  • 엘리먼트의 속성, 하위, 혹은 상위 엘리먼트를 검색하는 기능
  • 새로운 엘리먼트를 만들고 (파서 없이) 트리에 붙이거나 분리하는 기능

<table/>과 같은 특정 엘리먼트의 경우, 태이블 내의 모든 행, 열, 셀을 찾기 위한 테이블 고유 기능 뿐만 아니라, 테이블에서 행과 셀을 제거하고, 추가하기 위한 기능들도 제공한다. <canvas/>에는 선, 도형, 텍스트 및 이미지를 그릴 수 있는 기능이 제공된다. 이러한 Api를 사용하기 위해서는, HTML 마크업만으로는 불가능하며, 자바스크립트를 사용한다.

위에서 설명한 api로 트리에 DOM을 변경하면, 이에 대한 변경 및 업데이트를 분석하는 작업이 브라우저에서 시작된다. 화면에 보이는 것을 최대한 빨려 보여주기 위해 노력한다. 이 트리는 이러한 반복적인 업데이트를 빠르고 효율적으로 만들기 위한 다음과 같은 최적화 기능을 활용한다.

  • common 엘리먼트의 이름과 속성을 숫자로 접근 (빠른 식별을 위해 해시테이블 활용)
  • 엘리먼트에서 자주 사용되는 하위 항목을 캐시 (빠른 하위 항목 iteration을 위해)
  • 하위 트리의 변경이 전체트리의 변경으로 이어지지 않도록 변경을 최소화

다른 api

HTML 엘리먼트와 DOM 내부의 HTML 인터페이스는 화면에 콘텐츠를 표시하는 브라우저의 유일한 메커니즘이다. CSS는 레이아웃에 영향을 미칠 수 는 있지만, HTML 엘리먼트에 존재하는 컨텐츠에 대해서만 영향을 미칠 수 있다. 궁극적으로 화면에서 콘텐츠를 보기 위해서는, 트리의 일부인 HTML 인터페이스를 통해야 한다.

지금까지는 파서가 어떻게 서버에서 가져온 HTML을 DOM 트리로 가져왔는지를 살펴보았으며, 그리고 DOM 내부의 엘리먼트 인터페이스를 사용하여 트리에 추가, 제거, 수정을 할 수 있는 지 알아보았다. 그러나 브라우저의 프로그래밍 가능한 DOM은 상당히 방대하며, 이는 HTML 엘리먼트 인터페이스에만 국한되는 것은 아니다.

브라우저의 DOM의 스코프는 앱이 OS에서 사용할 수 있는 기능들과 유수한 수준의 기능들을 가지고 있다. 예를 들어

  • 스토리지 시스템에 접근 (데이터베이스, key/value 스토리지, 네트워크 캐시 스토리지)
  • 디바이스에 접근 (geolocation, 근접 및 방향 센서, USB, MIDI, 블루투스 등)
  • 네트워크 (http, 양방향 서버 소켓, 실시간 미디어 스트리밍)
  • 그래픽 (2d, 3d, 쉐이더, 가상 및 증강 현실)
  • 멀티스레딩 등등..

DOM에 의해 제공되는 기능은 주요 브라우저 엔진에 의해 새로운 웹 표준이 개발되고, 브라우저가 점차 이 기능을 구현해 나가면서 증가하고 있다.