1990년대 초 World Wide Web을 시작으로 IE, Mosaic, Netscape 등 여러 브라우저가 나오면서 브라우저 시장은 춘추전국시대를 맞았습니다. *WordSideStory 데이터에 따르면 2002년에 IE는 약 95%의 사용점유율을 보였는데요. IE가 시장을 독점하다시피 했을 때는 브라우저가 어떻게 돌아가는지에 대해 알 수 없었습니다. 하지만 Mozila의 Firefox 이후로 오픈소스 브라우저들이 나오면서 우리는 브라우저의 내부 동작에 대해 알 수 있게 되었습니다.
그럼 우분투 정신에 감사하며 저번 포스팅에서 알아보았던 브라우저의 구조에 이어
브라우저가 어떻게 화면을 렌더링하는지 알아봅시다 :)
이번 포스팅에서는 HTML과 CSS 파싱을 다룹니다.
"렌더링 엔진 Rendering Engine"
렌더링 엔진이 하는 일이 무엇이냐, 바로 '화면에 표시하는 것'입니다. 무엇을 표시할까요? 사용자가 요청한 데이터입니다. 네트워크 레이어에서 사용자가 요청한 데이터를 가져오면 렌더링 엔진은 이를 파싱해서 브라우저에 보여줍니다.
렌더링 엔진의 동장 방식은 '파싱 - 빌드 - 배치 - 페인트' 순으로 이루어집니다. 요청한 데이터를 코드가 사용할 수 있는 구조로 해석하고 돔 트리를 만들며, 그와 동시에 화면에 보여줄 시각적 요소인 렌더 트리를 만듭니다. 렌더 트리를 만들면 정확한 위치에 배치시킨 뒤, 렌더 트리를 그립니다.
"HTML Parsing"
브라우저는 도큐먼트를 코드가 이해하고 사용할 수 있는 구조로 변환합니다. 이 과정을 파싱이라고 합니다. 파싱된 결과물은 도큐먼트 구조를 나타내는 트리 형태가 됩니다. 이를 Document Object Model Tree, 줄여서 DOM tree라고 부릅니다.
1. 너그러운 HTML
사람끼리 대화할 때는 주어나 동사 등 문장의 필수 요소가 빠져도 문맥을 통해 파악할 수 있습니다. 하지만 컴퓨터는 그렇지 않습니다. 무언가를 해석하기 위해서는 대상이 특정한 어휘와 문법 규칙으로 구성되어있어야 합니다. 어디서부터 시작되고 무엇이 어떻게 표현되는지와 같이, 데이터를 의미있게 만드는 기호들이 존재합니다. 바꿔말해 이 기호들만 있으면 문맥이 없어도 의미를 파악할 수 있습니다. 그래서 이를 '문맥 자유 언어(Context Free Language)'라고 합니다.
HTML도 컴퓨터에서 쓰이기 때문에 문맥 자유 언어일 것 같지만 그렇지 않습니다. HTML은 문맥 자유 언어가 아닙니다. HTML 문법의 '관용적인' 특성 때문입니다. HTML은 보통 <> </> 이렇게 생긴 '태그'를 열고 닫음으로써 엘리먼트를 표시하는데, 가끔은 시작이나 종료 태그를 생략한다든지 코드를 유연하게 표현할 수 있습니다. 개발자가 사용하기는 쉽지만 파싱하는 데는 경우의 수가 많아지고 복잡해집니다. 그래서 문맥 자유 언어와 같은 방식으로 파싱할 수는 없습니다.
2. HTML 파싱
네트워크 레이어에서 데이터가 들어오면 '토큰화'와 '트리 구축' 과정을 거쳐 파싱합니다. 그 결과물이 document object입니다.
3. 토큰화
토큰화 알고리즘을 먼저 살펴봅시다. 알고리즘은 state(상태)에 따라 진행됩니다.
<html>
<body>
Hello world
</body>
</html>
- 처음에는 'data' 상태로 시작합니다.
- 그러나 '<' 를 만나면 '태그 열림' 상태가 됩니다.
- 그 다음 a-z 문자를 만나면 '시작 태그 토큰'을 생성합니다. 이때 상태는 '태그 이름' 상태로 바뀝니다. (각 문자에는 html, body라는 토큰 이름이 추가됩니다.)
- 이 상태는 '>'를 만날 때까지 유지됩니다. 만약 '>'를 만나면 다시 'data' 상태가 됩니다.
- 위와 같은 과정을 반복하다가 '/>'를 만나면 '종료 태그 토큰'을 생성합니다.
토큰들 중 열린 태그들은 스택에 저장됩니다. 열린 태그가 쌓이고 종료 태그와 만나면서 들어오고 나오는 과정을 반복합니다. 종료 태그를 만나지 못하면 알아서 교정하거나 알 수 없는 경우 에러가 발생합니다.
4. 트리 구축
트리 구축 알고리즘의 input 값은 토큰화 단계의 결과물인 '토큰'입니다. html 토큰을 받으면 Document에 html 태그를 생성합니다. 그 다음에는 body 태그를 생성합니다. 문서의 마지막에는 EOF (End Of File) 이라는 토큰이 있는데 이 토큰을 받으면 파싱을 종료합니다.
파싱이 끝나면 이제 Document의 상태는 '완료'가 되고 '로드'로 바뀝니다. 그리고 문서 파싱 이후에 실행되어야 하는 'defer(지연)' 스크립트를 파싱합니다.
5. 스크립트 태그
스크립트는 파싱과 실행이 동기적으로 이루어집니다. 즉, 파싱한 후 바로 실행하는 것입니다. html 파서가 script 태그를 만나면 실행이 중단됩니다. 그래서 script 태그를 body 태그 안의 맨 밑에 넣기도 합니다. 스크립트가 외부에 있는 경우(부트스트랩, jquery 등 외부 라이브러리 사용)에도 네트워크에서 데이터를 가져올 때까지 실행은 중단됩니다.
이때, *defer(지연)를 사용하여 document 파싱이 끝날 때까지 스크립트 실행을 지연시킬 수 있습니다. 'defer'는 도큐먼트 파싱이 끝나면 스크립트를 실행시킵니다. 모듈 스크립트는 기본적으로 defer 속성이 있어서 추가해도 변화가 없습니다. 단, src 속성이 없으면 아무런 지연 효과가 없습니다. *async 속성도 있는데, async를 이용하면 도큐먼트 파싱과 별도로 파싱되고 실행되어 defer와 비슷한 효과를 가질 수 있습니다. 모듈 스크립트가 많다면 파싱 속도를 높이기 위해 두 속성을 이용할 수 있습니다.
*defer, async https://developer.mozilla.org/ko/docs/Web/HTML/Element/script
6. 에러 핸들링
파서가 문서를 파싱하다가 오류가 생기는 경우에는 어떻게 할까요? 앞에서 말한 것처럼 HTML은 너그러운 언어입니다. 오류가 생기면 웬만해선 스스로 고칩니다.
- 태그 안쪽에 추가하려는 태그가 금지된 것이라면 금지된 태그까지 모두 닫고 나중에 그것을 추가합니다. 예를 들어 p 태그 안쪽에 또 다른 열린 p 태그를 넣으면 일단 두 태그를 모두 닫습니다. 그런 뒤 먼저 p 태그 다음에 p 태그를 추가합니다. 이렇게요. 개발자 도구 - elements - edit in HTML로 document를 편집해보세요.
// 오류
<p> <p> is this work? </p> </p>
// 파싱 결과
<p></p>
<p>is this work?</p>
- 개발자에 의해 추가된 게 아니면 파서가 직접 태그를 추가하지 않습니다.
- *인라인 엘리먼트 안에 블록 엘리먼트가 있으면 부모 블록을 만날 때까지 모든 인라인을 닫습니다. 예를 들어 span 태그 안에 p 태그가 있으면 span의 부모 블록을 만날 때까지 span을 닫습니다.
* 인라인 element: 필요한 너비만 차지하는 태그 ex) span developer.mozilla.org/ko/docs/Web/HTML/Inline_elements
* 블록 element: 새로운 줄에서 시작하며 가능한 많은 너비를 차지하는 태그 ex) p developer.mozilla.org/ko/docs/Web/HTML/Block-level_elements
- 이래도 해결이 안되면 태그를 추가하거나 무시할 수 있는 상태가 될 때까지 태그를 닫습니다.
"CSS Parsing"
css는 html과 달리 *어휘와 문법을 갖고 있는 문맥 자유 언어입니다. 문맥 자유 문법은 두 가지 파서가 필요합니다. lexer와 syntax입니다.
* www.w3.org/TR/CSS2/grammar.html
1. 문맥 자유 언어 파싱
lexer는 입력된 데이터를 토큰으로 쪼갭니다. 토큰은 마치 단어같은 것입니다. 필요없는 조사나 공백을 없애 문장을 의미있는 단어들로 쪼개듯이, lexer는 필요없는 공백이나 줄바꿈 등을 없애고 syntax 파서가 해석하기 쉽게 토큰으로 나눕니다.
lexer가 쪼갠 토큰을 syntax 파서가 받아 해석합니다. 토큰이 문법 규칙에 맞는지 확인하고 규칙에 맞으면 parse tree에 추가합니다. 그리고 그 다음 토큰을 요청해서 반복합니다. 만약 규칙이 맞지 않으면 해당 토큰을 내부에 저장하고 일치하는 규칙이 발견될 때까지 토큰을 요청합니다. 맞는 규칙이 없으면 syntax error를 발생시킵니다.
이 과정은 반복됩니다. 파싱된 결과는 트리 형태로 구성됩니다. 이를 Parse Tree라고 합니다. parse tree를 만들면 기계가 이해할 수 있는 코드로 번역하는 compile 과정을 거칩니다.
파서의 종류에는 상향식 파서, 하향식 파서가 있는데 하향식 파서는 문법에 맞는 큰 덩어리들부터 파싱합니다. 예를 들어 2 + 3 - 1이 있으면 2 + 3부터 찾아봅니다. 반대로 상향식 파서는 처음 입력 받은 토큰과 문법 규칙에 맞는 토큰을 찾는 과정을 통해 파싱합니다. (왼쪽에서 오른쪽으로 포인터를 이동시켜가며 찾는데, 이동시킬 때마다 파싱할 것이 감소해서 '이동-감소' 파서라고도 합니다.)
2. css 파싱
css 어휘는 토큰화를 위해 정규표현식으로 정의되어있습니다. 구문 문법은 일반적으로 문맥자유언어를 표현하는 방법인 BNF로 구성되어 있습니다.
웹킷은 flex, bison 파서 생성기를 사용합니다. 이들은 css 문법 파일을 통해 자동으로 파서를 생성합니다. *flex와 *bison은 각각 lexer, syntax 파서입니다. 바이슨은 상향식 이동 감소 파서입니다. 파이어폭스는 직접 만든 하향식 파서를 사용합니다. 웹킷과 파이어폭스 둘 다 css 파일을 스타일 시트 오브젝트로 파싱하고, 각 오브젝트는 css 규칙으로 나뉩니다. css 규칙 오브젝트는 selector와 declaration 오브젝트, css 문법과 일치하는 다른 오브젝트로 나뉩니다.
* 플렉스 en.wikipedia.org/wiki/Flex_(lexical_analyser_generator)
* 바이슨 www.gnu.org/software/bison/
"예측 파싱"
웹킷, 게코는 예측 파싱과 같은 최적화 프로세스를 지원합니다. 스크립트를 실행하는 동안 다른 스레드를 병렬로 실행하여 외부 스크립트, 스타일시트, 이미지 등 외부 데이터를 파싱합니다.
"스타일시트"
스타일시트는 DOM트리를 변경하지 않으므로 문서 파싱을 신경쓰지 않아도 되지만, 스크립트가 실행될 때 스타일 정보를 요청하면 잘못된 결과가 반환됩니다. 때문에 파이어폭스에서는 이런 스타일 시트가 있으면 스크립트 실행을 중단시키고, 웹킷에서는 아직 로드되지 않은 시트에서 영향을 줄만 한 속성에 접근하려 할때에만 중단합니다.
이제 파싱 단계를 마쳤습니다. 파싱까지만 했는데 속이 울렁거리는 건 왜일까요?
아무튼 목구멍까지 올라오는 파싱은 다시 집어삼키고 다음 포스팅에서는 '렌더 트리 빌드'에 대해서 알아보겠습니다.
빠잉. 👋
"references"
브라우저 동작 방식
- www.html5rocks.com/en/tutorials/internals/howbrowserswork/#Layout
'개발 이야기 > front-end' 카테고리의 다른 글
css rendering - 기본 레이아웃 | CSS 속성은 사실 OO이다? (0) | 2021.02.21 |
---|---|
자바스크립트 이벤트 루프와 비동기 콜백 | how does javascript work? (0) | 2021.02.19 |
[Browser] 브라우저가 화면을 그리는 법 Render Tree, layout, paint | Browser Rendering (0) | 2021.02.11 |
[Browser] 브라우저의 구조 | Browser Structure (0) | 2021.02.09 |
나의 웹 개발자 로드맵 - 프론트엔드 체크리스트 (0) | 2021.02.04 |