1. "화면이 깜빡거려요"
프론트엔드 개발자라면 누구나 한 번쯤 겪는 문제입니다. 버튼을 눌렀는데 화면이 버벅거리거나(Jank), 애니메이션이 뚝뚝 끊기는 현상. 이걸 해결하려면 리액트(React)나 뷰(Vue)를 잘하는 것도 중요하지만, 더 근본적으로 브라우저가 어떻게 화면을 그리는지를 알아야 합니다.
우리가 크롬 주소창에 google.com을 치고 엔터를 누르는 순간부터, 눈앞에 검색창이 뜨기까지 브라우저 내부에서는 엄청난 일이 벌어집니다. 이 일련의 과정을 CRP (Critical Rendering Path, 중요 렌더링 경로)라고 부릅니다.
이 경로를 최적화하는 것이 웹 성능 최적화의 핵심이며, 구글이 웹사이트의 성능을 측정하는 지표(Core Web Vitals)의 근간이 됩니다.
오늘은 브라우저의 렌더링 엔진(Blink, WebKit, Gecko 등)이 어떻게 단순한 HTML 텍스트를 아름다운 픽셀로 바꾸는지, 그 6단계 과정을 아주 상세하게 뜯어봤습니다.
2. 1단계 - 파싱(Parsing) - 설계도 읽기
서버로부터 HTML 파일을 받으면(Response), 이는 단순한 0과 1로 된 바이트(Bytes) 스트림일 뿐입니다. 브라우저는 이를 해석 과정을 거쳐 이해 가능한 구조로 바꿉니다.
DOM (Document Object Model) 트리 생성
- 변환(Conversion): 바이트를 지정된 인코딩(UTF-8 등)에 따라 문자(Characters)로 변환합니다. (
<,h,t,m,l,>) - 토큰화(Tokenizing): 문자를 W3C 표준에 따라 토큰(Tokens)으로 분해합니다. (
StartTag: html,StartTag: body) - 렉싱(Lexing): 토큰을 객체(Objects/Nodes)로 변환합니다.
- DOM 트리 구축: 객체 간의 부모-자식 관계를 정의하여 트리 구조를 만듭니다. (
html->body->div)
DOM은 자바스크립트가 HTML 구조에 접근하고 조작할 수 있도록 만든 API 인터페이스입니다.
프리로드 스캐너 (Preload Scanner)
원칙적으로 DOM 파싱은 동기적(Synchronous)으로 일어납니다. 즉, HTML을 읽다가 <script> 태그를 만나면, 스크립트 실행이 끝날 때까지 파싱을 멈춥니다.
하지만 브라우저는 생각보다 똑똑합니다. 메인 파서가 멈춰있는 동안, 백그라운드에서 프리로드 스캐너라는 녀석이 HTML을 미리 훑어봅니다.
"어, 저 뒤에 <img>, <link>, <script>가 있네?" 하고 미리 네트워크 요청을 보냅니다.
덕분에 파싱이 끝나기도 전에 리소스 다운로드가 병렬로 진행되어 전체 로딩 속도가 빨라집니다.
CSSOM (CSS Object Model) 트리 생성
HTML을 읽다가 <link rel="stylesheet">나 <style> 태그를 만나면 CSS 파싱을 시작합니다.
DOM과 비슷하게 CSS도 CSSOM 트리로 변환됩니다.
여기에는 "상속(Cascading)" 규칙이 적용됩니다. body { font-size: 16px }라고 지정하면, 자식인 div도 16px를 상속받는 계산이 이 단계에서 모두 끝납니다.
중요: CSSOM이 완성될 때까지 렌더링은 차단됩니다(Render Blocking Resource). CSS가 없으면 화면이 깨져 보이기(FOUC) 때문입니다.
3. 2단계 - 렌더 트리 (Render Tree) - 실제로 보일 것만 추리기
DOM과 CSSOM이 합쳐져서 렌더 트리(Render Tree)가 됩니다. 여기서 중요한 점은 "화면에 실제로 보이는(Visible) 요소만 포함한다"는 것입니다.
opacity: 0-> 눈에는 투명해서 안 보이지만, 자리는 차지하므로 렌더 트리에 포함됩니다. (클릭 이벤트도 받음)visibility: hidden-> 역시 자리는 차지하므로 렌더 트리에 포함됩니다.display: none-> 아예 공간조차 차지하지 않으므로 렌더 트리에 포함되지 않습니다. (트리에서 제외됨)
그래서 성능 최적화를 할 때, 요소를 잠깐 숨기려면 display: none을 쓰는 것이 렌더링 비용을 아끼는 방법이고, 애니메이션(페이드 아웃)을 주려면 opacity를 써야 합니다.
4. 3단계 - 레이아웃 (Layout) - 위치 잡기
렌더 트리가 만들어지면, 이제 각 요소가 화면의 어느 위치에, 어떤 크기로 배치될지 계산합니다. 이 과정을 레이아웃(Layout) 또는 리플로우(Reflow)라고 합니다.
"Header는 가로 100%, 높이 50px. 그 밑에 Content는 가로 80%..."
브라우저는 뷰포트(Viewport) 크기에 맞춰 기하학적 계산을 수행합니다. % 나 vh 같은 상대 단위가 절대 단위인 px로 변환되는 시점이 바로 이때입니다.
리플로우(Reflow)가 발생하는 경우
레이아웃 단계는 계산 비용이 매우 비쌉니다. 다음과 같은 경우에 다시 발생합니다.
- 창 크기 조절 (Resizing)
- 노드 추가/삭제
- 요소의 크기, 위치 변경 (
width,height,margin,padding,top,left) - 폰트 변경
레이아웃 스래싱 (Layout Thrashing)
자바스크립트로 스타일을 읽고 쓸 때 주의해야 합니다.
// 나쁜 예: 읽고(Read) -> 쓰고(Write) -> 읽고 -> 쓰고
const list = document.getElementById('list');
const width = list.offsetWidth; // 브라우저는 정확한 값을 주기 위해 강제로 레이아웃을 다시 계산함!
list.style.width = (width + 10) + 'px';
브라우저는 offsetWidth를 정확히 계산하기 위해 큐에 쌓인 변경 사항을 즉시 처리하고 레이아웃을 다시 돌립니다. 이걸 반복문 안에서 하면 성능이 바닥을 칩니다.
해결책은 값을 읽는 것과 쓰는 것을 분리하거나, requestAnimationFrame을 사용하는 것입니다.
5. 4단계 - 페인트 (Paint) - 색칠하기
위치가 잡혔으면 이제 픽셀을 채울 차례입니다. 텍스트의 색상, 이미지, 그림자, 테두리, 배경색 등을 그립니다. 이 과정을 페인트(Paint) 또는 시스템 래스터화(Rasterizing)라고 합니다.
페인트도 그냥 한 도화지에 그리는 게 아니라, 포토샵처럼 여러 레이어(Layer)로 나눠서 그립니다.
position: fixed, transform: translate3d, video, canvas 태그 등을 사용하면 브라우저는 해당 요소를 별도의 레이어로 분리(Promotion)합니다. 레이어가 분리되면 나중에 그 부분만 다시 그리면 되기 때문에 성능상 이점이 있습니다.
6. 5단계 - 합성 (Composite) - 합치기
마지막으로, 나뉘어진 레이어들을 합쳐서 최적으로 화면에 표시합니다. 이 과정은 주로 GPU가 담당합니다.
하드웨어 가속 (Hardware Acceleration)
우리가 애니메이션을 만들 때 left, top 대신 transform: translate()를 쓰라고 하는 이유가 여기에 있습니다.
left변경 -> Layout부터 다시 수행 (CPU 부하 큼, 주변 요소도 다 다시 계산). 이를 리플로우라고 합니다.transform변경 -> Layout과 Paint를 건너뛰고 Composite 단계만 수행 (GPU 사용, 매우 빠름).
GPU는 텍스처(이미지)를 이동시키거나 회전시키는 데 최적화되어 있습니다. 레이아웃 변화 없이 레이어만 움직이는 것이기 때문에 60fps(초당 60프레임)를 유지하기 훨씬 쉽습니다.
7. CSS-in-JS와 가상 DOM 한 걸음 더
React의 가상 DOM (Virtual DOM)
리액트가 빠른 이유는 돔 조작을 최소화하기 때문입니다. 데이터가 바뀌면 가상 DOM을 먼저 수정하고, 실제 DOM과 비교(Diffing)해서 바뀐 부분만 실제 DOM에 적용(Reconciliation)합니다. 이는 불필요한 레이아웃(Reflow) 횟수를 획기적으로 줄여줍니다.
CSS-in-JS의 런타임 비용
styled-components나 Emotion 같은 CSS-in-JS 라이브러리는 편리하지만 "런타임 비용"이 있습니다.
자바스크립트가 실행되면서 스타일을 문자열로 생성하고, 해시 클래스 이름을 만들고, <style> 태그를 동적으로 헤더에 삽입합니다.
이 과정에서 JS 실행 시간과 CSSOM 트리 재구성 시간이 소요됩니다.
이 때문에 최근에는 Zero-runtime CSS (Tailwind CSS, Vanilla Extract)가 각광받고 있습니다. 빌드 타임에 이미 CSS 파일이 완성되어 나오기 때문에, 브라우저 입장에서는 렌더링 부하가 훨씬 적습니다.
8. 최적화 마무리
웹 성능 최적화의 핵심은 "가능한 뒤쪽 단계(Composite)만 실행되도록 만드는 것"입니다. 앞 단계(Layout)로 돌아갈수록 브라우저는 괴로워하고 배터리 소모는 심해집니다.
요약 및 팁
- 애니메이션은
transform,opacity만 사용: CPU 대신 GPU를 일하게 하세요. display: none적극 활용: 안 보이는 건 렌더 트리에서 빼버리세요.- 레이아웃 스래싱 주의: DOM 속성 읽기(
offsetWidth등)는 최소화하세요. requestAnimationFrame사용:setTimeout대신 이걸 써야 프레임 드랍 없이 부드럽게 움직입니다.
브라우저가 어떻게 픽셀을 그리는지 이해하는 것, 그것이 "코더"를 넘어 "엔지니어"로 가는 첫걸음입니다.