부트캠프에서 이런 질문을 받았다. "HTML, CSS, JavaScript 코드가 브라우저에서 어떻게 해석되어 DOM · CSSOM · 렌더 트리로 변환되는지 설명하고, 무거운 JavaScript 실행이 모델 추론 UI의 반응성에 어떤 영향을 미칠 수 있는지 서술하시오." 이 질문에 답하려면 브라우저가 코드를 픽셀로 바꾸는 전체 파이프라인을 따라가 보아야 한다. 이 글은 그 경로인 Critical Rendering Path를 단계별로 짚은 뒤, 단일 스레드라는 구조적 한계가 왜 무거운 연산 앞에서 UI를 멈추게 하는지 설명한다.
Critical Rendering Path (CRP)
브라우저가 HTML, CSS, JavaScript 코드를 해석하기 위해서 거쳐야 하는 단계를 CRP(Critical Rendering Path, 중요 렌더링 경로)라고 부른다. 전체적인 단계는 다음과 같다.
그림 1. Critical Rendering Path의 전체 단계.
아래 순서에서 1번과 2번은 동시에 이루어진다고 생각하면 된다.
- HTML 파싱과 DOM 구축
- CSS 파싱과 CSSOM 구축
- 렌더링 트리 구성
- 레이아웃/리플로우
- 페인트와 합성
HTML 파싱과 DOM 구축
첫번째 단계는 HTML을 파싱(parsing)하여 DOM(Document Object Model, 문서 객체 모델)을 구축하는 것이다. 전체적인 단계는 크게 토큰화tokenization와 트리 구축tree construction으로 나뉘어 있다.
그림 2. WHATWG HTML 표준에 실린 HTML 파서의 순서도.
브라우저의 렌더링 엔진1은 네트워크를 통해 전송받은 0과 1로 이루어진 바이트 스트림(byte stream)을 가장 먼저 마주한다. 엔진은 HTTP 헤더에 명시된 문자 인코딩을 기반으로 이 바이트들을 개별 문자로 변환하고, 변환된 문자 스트림을 토크나이저(tokenizer / lexer)로 넘긴다.
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>아무 말 대잔치 - 게시글 작성</title>
<link rel="stylesheet" href="/css/style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Noto+Sans+KR:[email protected]&display=swap"
rel="stylesheet">
</head>
토크나이저로 넘기기 전에 변환되는 데이터에는 네트워크를 통해 전송받은 바이트 스트림도 있지만, document.write()2와 같은 JavaScript API를 통해 넘겨받는 데이터도 존재한다. 입력 스트림 전처리기input stream preprocessor에서 두 가지 데이터를 정해진 규칙에 맞게 변환하면 토크나이저가 소화할 수 있는 형태의 입력 스트림이 만들어지고, 토크나이저는 입력 스트림의 문자를 하나씩 사용하며 HTML 표준에 정의된 규칙에 따라 의미 있는 단위인 토큰(token)을 만든다.
HTML의 토크나이저는 토큰화 알고리즘을 통해 HTML 토큰을 생성하는데, 이 알고리즘은 결과물이 현재 상태의 영향을 받는 상태 기계state machine3라고 볼 수 있다. 예제를 통해 어떻게 HTML 토큰이 생성되는지 알아보자.
<html>
<body>
Hello world
</body>
</html>
이 코드를 토큰화하는 과정을 단순화하면 아래 그림과 같다.
그림 3. HTML 토크나이저가 토큰을 생성하는 과정. 같은 문자를 읽어도 현재 상태에 따라 다음 상태가 다르게 나타나기 때문에 토크나이저는 상태 기계에 속한다.
초기 상태는 "자료 상태"이다. < 문자를 만나면 상태는 "태그 열림 상태"로 변한다. a부터 z까지의 문자를 만나면 "시작 태그 토큰"을 생성하고 상태는 "태그 이름 상태"로 변하는데, 이 상태는 > 문자를 만날 때까지 유지된다. 각 문자는 새로운 토큰 이름에 덧붙는데, 이 경우 생성된 토큰은 html 토큰이다.
> 문자에 도달하면 현재 토큰이 발행되고 상태는 다시 "자료 상태"로 바뀐다. 태그는 동일한 절차에 따라 처리된다. 지금까지 html 태그와 body 태그를 발행했고 다시 "자료 상태"로 돌아왔다. Hello world의 H 문자를 만나면 문자 토큰이 생성되고 발행되며, 이것은 종료 태그의 < 문자를 만날 때까지 진행된다. 즉 Hello world의 각 문자마다 문자 토큰이 발행된다.
종료 태그의 < 문자를 만나면 다시 "태그 열림 상태"가 된다. / 문자는 종료 태그 토큰을 생성하고 상태를 "태그 이름 상태"로 바꾸며, 이 상태는 > 문자를 만날 때까지 유지된다. 그리고 새로운 태그 토큰이 발행되고 다시 "자료 상태"가 된다.
토크나이저는 토큰을 인지해서 트리 생성자로 넘기고, 다음 토큰을 확인하기 위해 다음 문자를 확인한다. 그리고 입력이 끝날 때까지 이 과정을 반복한다.
HTML 파서가 생성될 때 Document 객체가 같이 생성된다. DOM 트리의 최상단에는 바로 이 Document 객체가 있으며 트리를 구성하는 과정에서 끊임없이 수정된다. 파서는 토큰을 순차적으로 처리하면서 노드를 생성하고, HTML의 중첩 구조를 반영하여 부모-자식 관계를 형성한다. 이 과정은 점진적으로 수행된다. 즉, 브라우저는 HTML 문서 전체를 다운로드할 때까지 기다리지 않고, 네트워크로부터 패킷이 도착하는 대로 파싱하여 DOM 트리를 구축하기 시작한다.
HTML 토큰이 DOM이 되는 과정을 자세히 알아보자. DOM을 만드는 트리 생성기 혹은 파서(parser) 역시 상태 기계에 해당한다.
그림 4. HTML 트리를 구축하는 과정.
- 트리 생성 단계의 입력은 토큰화 단계에서 만들어진 토큰들이다. 트리 생성기는 "초기 모드"라는 상태에서 시작하는데, html 토큰을 수신하면 "HTML 이전" 모드로 바뀌어 해당 모드에서 토큰을 다시 처리한다. 이로 인해
HTMLHtmlElement요소가 생성되고 DOM 트리의 최상단에 있는Document객체에 추가된다. - 이제 상태는 "head 이전"으로 변경되고, body 토큰이 들어온다. head 토큰이 없지만 그래도
HTMLHeadElement가 암시적으로 생성되고 트리에 추가된다. - 이제 "head 내부" 상태로 전환한 다음 "head 다음"으로 바뀐다. body 토큰이 다시 처리되고
HTMLBodyElement가 만들어진 다음 DOM 트리에 삽입되며 상태는 "body 내부"로 바뀐다. - 이제
Hello world문자열의 문자 토큰이 들어온다. 첫 번째 토큰으로 인해 "텍스트" 노드가 생성되고 삽입되며, 다른 문자들은 해당 노드에 추가된다. - body의 종료를 의미하는 토큰을 수신하면 "body 다음" 상태로 전환된다. 이제 html 종료 태그가 들어와 "body 다음의 다음" 상태로 전환되고, EOF(End-of-File, 파일 종료) 토큰을 수신하면 파싱이 종료된다.
이렇게 파싱이 끝나면 브라우저는 HTML 문서와 상호작용을 할 수 있게 되고, HTML 문서가 파싱된 이후 실행되어야 하는 "지연"(defer) 상태의 스크립트를 실행하기 시작한다.
CSS 파싱과 CSSOM 구축
DOM이 문서의 내용content과 구조를 담고 있다면, CSSOM(CSS Object Model, CSS 객체 모델)은 문서의 스타일과 레이아웃 규칙을 정의한다. 브라우저가 HTML을 파싱하다가 <link rel="stylesheet"> 태그나 <style> 태그를 만나면 CSS 파싱을 시작하는데, DOM과 달리 CSSOM은 점진적으로 구축되어 사용될 수 없다. CSS에는 상속inheritance과 계단식cascade 규칙이 존재하기 때문에, 스타일시트의 뒷부분에 정의된 규칙이 앞부분의 규칙을 덮어쓸 수 있다. 만약 CSSOM이 완성되지 않은 상태에서 렌더링을 진행하면, 스타일이 적용되지 않은 콘텐츠가 잠깐 보였다가 스타일이 적용되면서 화면이 번쩍이는 FOUC(Flash of Unstyled Content, 스타일 없는 콘텐츠 깜빡임) 현상이 발생하거나 잘못된 레이아웃이 그려질 수 있다. 따라서 브라우저는 CSSOM이 완전히 구축될 때까지 렌더링을 차단한다.
그림 5. CSS가 CSSOM으로 변환되어 렌더링에 쓰이기까지의 흐름.
CSS도 파서를 통해 CSSOM으로 변환된다. CSS 파서는 바이트를 문자로, 문자를 토큰으로, 토큰을 노드로 변환하여 CSSOM 트리를 만든다. 이 과정에서 브라우저는 각 노드에 어떤 스타일 규칙이 적용될지 결정해야 하는데, 이를 스타일 재계산style recalculation이라고 한다.
브라우저는 선택자selector를 효율적으로 매칭하기 위해 오른쪽에서 왼쪽으로 읽는 전략을 취한다. 예를 들어 .menu li span이라는 선택자가 있다면, 브라우저는 먼저 모든 span 태그를 찾고, 그 부모가 li인지 확인하며, 그 조상이 .menu 클래스를 가졌는지 확인한다. 이 방식은 매칭되지 않는 대다수의 요소를 빠르게 걸러낼 수 있어 성능상 유리하다.
그림 6. CSS 코드를 CSSOM 트리로 변환한 결과.
최종적으로 각 DOM 노드에는 명시적으로 선언된 스타일뿐만 아니라 브라우저 기본 스타일(user agent stylesheet), 상속된 스타일 등이 결합되어 계산된 스타일computed style이 할당된다. CSSOM은 이러한 스타일 규칙들의 집합체로서, 이후 렌더 트리(render tree) 구축의 핵심 재료가 된다.
렌더링 트리 구성
DOM과 CSSOM이 구축되면, 브라우저는 이 두 트리를 결합하여 화면에 실제로 그려질 요소들만을 포함하는 트리 구조인 렌더 트리를 생성한다.
그림 7. 렌더 트리와 그에 상응하는 DOM 트리. Gecko는 "Viewport"를, WebKit은 "RenderView" 객체를 가장 기본적인 블록으로 사용한다.1
렌더 트리를 구축하기 위해 브라우저는 DOM 트리의 루트(root)부터 시작하여 모든 노드를 순회한다. 이때 <head>, <meta>, <script>, <title>, 혹은 display: none과 같이 화면에 표시되지 않는 노드들은 렌더 트리에서 제외된다. 이 단계에서 각 렌더 트리 노드(또는 렌더 객체)는 자신에게 적용될 최종 스타일 정보를 CSSOM으로부터 받아 결합한다.
레이아웃/리플로우
렌더 트리가 생성되었지만, 아직 각 요소가 화면의 정확히 어느 위치에 어떤 크기로 그려져야 하는지는 결정되지 않았다. 이 계산 과정을 레이아웃(layout) 또는 리플로우(reflow)라고 한다. 레이아웃 단계에서 브라우저는 렌더 트리의 루트에서부터 순회하며 각 노드의 정확한 위치와 크기를 픽셀 단위로 계산하는데, 이때 CSS의 박스 모델(box model)이 적용되어 마진(margin), 테두리border, 패딩(padding), 콘텐츠 영역의 크기가 결정된다.
페인트와 합성
페인트(paint) 단계에서는 렌더 트리의 각 노드를 실제 픽셀로 변환한다. 텍스트, 색상, 이미지, 경계선, 그림자 등 시각적 요소들이 그려진다. 이 작업은 여러 개의 레이어(layer)로 나뉘어 수행될 수 있다. 또한 현대 브라우저는 성능 최적화를 위해 페이지를 여러 레이어로 나누어 페인팅한 뒤 이를 GPU를 이용해 합치는 합성composite 과정을 거친다. transform, opacity와 같은 속성은 레이아웃이나 페인트 단계를 거치지 않고 합성 단계에서만 처리될 수 있어, 애니메이션 성능을 크게 향상시킨다.
자바스크립트 실행 모델과 각종 병목 현상
이렇게 정교하게 설계된 브라우저의 렌더링 파이프라인에는 단일 스레드라는 근본적인 한계가 존재한다. 이로 인해 브라우저에서 발생하는 무거운 연산 작업이 UI 반응성에 치명적인 영향을 미칠 수 있다. 이쯤에서 자바스크립트의 실행 모델을 따라가며 단일 스레드라는 특성이 어떤 문제를 낳는지 알아보자.
메인 스레드와 이벤트 루프
브라우저 탭 하나는 일반적으로 하나의 메인 스레드(main thread)를 가지는데, 이 스레드는 자바스크립트 실행뿐만 아니라 HTML 파싱, DOM 구축, CSS 스타일 계산, 레이아웃, 페인팅까지 모든 작업을 도맡아 처리한다. 그리고 자바스크립트의 동시성 모델은 이벤트 루프(event loop)에 기반한다. 이벤트 루프에는 다음과 같은 자료구조가 있다.
- 콜 스택(call stack): 현재 실행 중인 함수들이 쌓이는 곳이다. 동기적 코드는 즉시 이곳에서 실행된다.
- 태스크 큐(task queue, macrotasks):
setTimeout,setInterval, 사용자 입력 이벤트, 네트워크 요청 콜백 등이 대기하는 큐이다. - 마이크로태스크 큐(microtask queue):
Promise.then,MutationObserver등의 콜백이 대기한다. 마이크로태스크는 일반 태스크보다 높은 우선순위를 가지며, 콜 스택이 비는 즉시 실행된다.
렌더링 차단
이 이벤트 루프의 가장 중요한 특징은 한 번에 하나만 처리한다는 것이다. 그래서 콜 스택에서 자바스크립트 코드가 실행 중인 동안에는 브라우저가 화면을 갱신(렌더링)할 수 없다. 일반적으로 브라우저는 초당 60프레임(60fps)의 부드러운 화면을 제공하기 위해 약 16.6ms마다 화면을 다시 그려야 하지만4, AI 모델 추론과 같이 연산량이 많은 자바스크립트 함수가 50ms 이상 실행되면 그 시간 동안 렌더링 업데이트가 차단된다. 사용자는 이를 화면 멈춤, 버벅거림, 또는 입력 지연으로 경험하게 된다.
파싱 차단(Parser Blocking)
HTML 파서가 <script> 태그를 만나면, 해당 스크립트가 DOM을 조작(예: document.write)할 가능성이 있기 때문에 DOM 구축을 일시 중지하고 자바스크립트 엔진에 제어권을 넘긴다. 앞서 본 그림 2의 파서 순서도에서 스크립트 실행 지점에 파서가 멈춰 서는 흐름을 확인할 수 있다. 이를 파싱 차단parser blocking이라고 하며, 초기 렌더링 속도를 저하시키는 주요 원인 중 하나이다. 이를 방지하기 위해 async나 defer 속성을 사용하여 스크립트 실행을 비동기화하거나 DOM 구축 완료 후로 미루는 최적화가 필수적이다.
레이아웃 스래싱(Layout Thrashing)
레이아웃은 계산 비용이 매우 높은 작업이다. 폰트 크기 변경, 창 크기 조절, DOM 요소 추가/삭제 등 기하학적 속성에 변화가 생기면 브라우저는 영향을 받는 모든 요소의 위치를 다시 계산해야 한다. 특히 자바스크립트 루프 내에서 스타일을 읽고(offsetWidth 등) 쓰는 작업을 반복하면, 브라우저가 강제로 레이아웃을 반복 수행하는 레이아웃 스래싱(layout thrashing)이 발생하여 심각한 성능 저하를 초래할 수 있다.
결론
브라우저가 HTML, CSS, JavaScript 코드를 해석하는 과정에 JavaScript가 직접적으로 관여하지는 않지만, 브라우저 탭 하나가 일반적으로 하나의 메인 스레드를 가지기 때문에 무거운 JavaScript 코드가 렌더링과 같은 스레드에서 실행되어 UI 반응성에 치명적인 영향을 미칠 수 있다. 모델 추론처럼 오래 걸리는 작업일수록 메인 스레드 밖으로 밀어내거나 응답을 잘게 나눠 스트리밍하는 설계가 필요한 이유다. 그렇다면 애초에 이 렌더링 부담을 서버와 클라이언트 중 어디에 둘 것인가? 이 선택의 문제는 렌더링 방식의 차이에서 다룬다.
출처
- Irish, P. and Garsiel, T. (2011) How browsers work. Available at: https://web.dev/articles/howbrowserswork (Accessed: 26 August 2026).
- Liccini, J. (2022) An Introduction to the Browser Rendering Pipeline. Available at: https://webperf.tips/tip/browser-rendering-pipeline/ (Accessed: 26 August 2026).
- MDN Web Docs (2025) Critical rendering path. Available at: https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Critical_rendering_path (Accessed: 26 August 2026).
- MDN Web Docs (2025) Populating the page: how browsers work. Available at: https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/How_browsers_work (Accessed: 26 August 2026).
- WHATWG (2026) HTML Standard, 13.2 Parsing HTML documents. Available at: https://html.spec.whatwg.org/multipage/parsing.html (Accessed: 26 August 2026).
- 포로의 테크로그 (2023) 오토마타 이론과 Parser의 원리 - HTML Parser 만들기 (Java). Available at: https://porolog.tistory.com/20 (Accessed: 26 August 2026).
*[DOM]: Document Object Model *[CSSOM]: CSS Object Model
Footnotes
-
렌더링 엔진(rendering engine)은 브라우저에서 HTML·CSS를 해석해 화면에 그리는 핵심 컴포넌트다. Gecko는 Firefox, WebKit은 Safari의 렌더링 엔진이며, Chrome과 Edge가 쓰는 Blink는 WebKit에서 갈라져 나왔다. ↩ ↩2
-
document.write()는 파싱 중인 문서 스트림에 문자열을 직접 삽입하는 오래된 DOM API다. 파서가 그 실행 결과를 기다려야 하므로 파싱을 차단하며, 현대 웹 개발에서는 사용이 권장되지 않는다. ↩ -
상태 기계(state machine)는 유한한 상태의 집합과 상태 전이 규칙으로 동작을 기술하는 계산 모델로, 같은 입력이라도 현재 상태에 따라 다른 결과를 낸다. HTML 토큰화 알고리즘은 WHATWG HTML 표준에 상태 기계 형태로 정의되어 있다. ↩
-
대부분의 디스플레이 주사율인 60Hz에 맞춰 1초에 60번 화면을 갱신하려면 한 프레임을 1000ms ÷ 60 ≈ 16.6ms 안에 그려내야 한다. 이 예산을 넘기는 작업이 메인 스레드를 점유하면 프레임이 떨어진다. ↩