[Frontend] 렌더링 파이프라인 이해와 Reflow/Repaint 최소화 전략
왜 렌더링 과정을 알아야 할까?
프론트엔드 개발을 하다 보면 자연스럽게 ‘성능 최적화’라는 벽에 부딪히게 됩니다. React나 Vue 같은 훌륭한 프레임워크들이 많은 것을 대신 처리해주지만, 결국 우리가 짠 코드가 실행되고 화면에 그려지는 곳은 ‘브라우저’입니다.
화면을 스크롤할 때마다 버벅거리는 현상(Jank)이 발생하거나, 초기 로딩 속도가 느려 사용자가 이탈한다면 어떻게 해결해야 할까요? 이때 브라우저가 어떤 과정을 거쳐 코드를 화면의 픽셀로 변환하는지, 즉 브라우저 렌더링 파이프라인(Critical Rendering Path)를 이해하고 있다면 문제의 원인을 정확히 짚어낼 수 있습니다.
단순히 기능이 동작하는 코드를 넘어, 사용자에게 빠르고 부드러운 UI를 제공하는 ‘성능 좋은 웹’을 만들기 위해 브라우저의 화면 그리기 과정을 파헤쳐 보겠습니다.
브라우저 렌더링 파이프라인 5단계 핵심 요약
우리가 에디터에 작성한 HTML, CSS, JavaScript 파일이 브라우저를 만나 화면에 그려지기까지는 크게 5가지의 단계를 거치게 됩니다. 이 흐름을 머릿속에 그려두면 이후에 다룰 성능 최적화의 원리를 쉽게 이해할 수 있습니다.
① DOM 트리 생성(HTML 파싱)
브라우저가 서버로부터 HTML 문서를 전달받으면, 가장 먼저 이 문서를 브라우저가 이해할 수 있는 형태로 변환합니다. 브라우저의 렌더링 엔진은 HTML 문서를 위에서부터 아래로 읽어 내려가며(파싱), HTML 태그들을 각각의 객체(Node)로 만들고 이들을 트리 구조로 엮어냅니다. 이것이 바로 우리가 잘 알고 있는 DOM(Document Object Model) 트리입니다.
② CSSOM 트리 생성(CSS 파싱)
HTML을 파싱하다가 <link> 태그로 연결된 CSS 파일이나 <style> 태그를 만나면, 브라우저는 HTML 파싱을 잠시 멈추고 CSS를 해석하기 시작합니다. CSS 파일 역시 HTML과 마찬가지로 브라우저가 이해할 수 있는 트리 구조로 변환되는데, 이를 CSSOM(CSS Object Model) 트리라고 부릅니다. CSSOM에는 요소들의 크기, 색상, 위치 등 화면을 꾸미는 데 필요한 모든 스타일 정보가 담겨 있습니다.
③ 렌더 트리(Render Tree) 구축
이제 브라우저에는 문서의 뼈대를 담은 ‘DOM 트리’와 스타일 정보를 담은 ‘CSSOM 트리’가 준비되었습니다. 브라우저는 이 두 트리를 결합하여 화면에 실제로 보여질 요소들만 골라내어 렌더 트리(Render Tree)를 만듭니다.
- 💡 핵심 포인트: 화면에 보이지 않는 요소(예:
<head>,display: none이 적용된 요소)는 렌더 트리에 포함되지 않습니다.(단,visibility: hidden은 공간을 차지하므로 렌더 트리에 포함됩니다.)
④ 레이아웃(Layout/Reflow)
렌더 트리가 완성되면, 브라우저는 뷰포트(Viewport, 브라우저 화면) 내에서 각 요소들이 정확히 어디에 위치하고 얼마나 크게 그려져야 하는지 수학적으로 계산합니다. 이 과정을 레이아웃(Layout) 또는 리플로우(Reflow)라고 합니다. 상대적인 단위(예: %, vw, vh)로 지정된 크기들이 이 단계에서 실제 화면의 픽셀 단위(px)로 변환됩니다.
⑤ 페인트(Paint/Repaint) 및 컴포지팅(Composite)
레이아웃 계산이 끝났다면, 이제 실제로 화면에 픽셀을 채워 넣을 차례입니다.
- 페인트(Paint): 브라우저는 렌더 트리의 각 노드를 화면의 실제 픽셀로 그립니다.(색상, 이미지, 그림자 등)
- 컴포지팅(Composite): 성능 향상을 위해 브라우저는 화면의 요소들을 여러 개의 레이어(Layer)로 나누어 페인트하는데, 마지막으로 이 분리된 레이어들을 순서대로 차곡차곡 합성하여 최종적인 화면을 완성합니다.
Reflow와 Repaint
앞서 살펴본 5단계 중, 브라우저 성능에 가장 치명적인 영향을 미치는 단계는 어디일까요? 바로 4단계(레이아웃)와 5단계(페인트)입니다. 자바스크립트를 통해 DOM이나 CSS를 변경하면 브라우저는 화면을 다시 그려야 하는데, 변경되는 속성에 따라 거치는 단계와 비용이 크게 달라집니다.
- Reflow(Layout) - 뼈대가 흔들릴 때:
요소의 너비(width), 높이(height), 여백(margin,padding) 등 기하학적인 크기나 위치가 변경될 때 발생합니다. 한 요소의 크기가 바뀌면 주변에 있는 다른 부모/자식 요소들의 위치까지 연쇄적으로 다시 계산해야 하므로 가장 큰 비용(성능 저하)이 발생합니다. - Repaint(Paint) - 겉모습만 바뀔 때:
레이아웃에는 영향을 주지 않고, 글자 색상(color)이나 배경색(background-color) 같은 시각적인 요소만 변경될 때 발생합니다. 레이아웃 단계를 건너뛰기 때문에 Reflow보다는 덜 무겁지만, 여전히 픽셀을 다시 그리는 작업이므로 잦은 Repaint는 피해야 합니다. - Reflow, Repaint를 모두 피하는 방법(Composite):
애니메이션을 구현할 때 가장 권장되는 방식입니다.transform이나opacity속성을 사용하면 브라우저는 Layout과 Paint 단계를 모두 생략하고 화면의 레이어를 합성(Composite)하는 작업만 수행합니다. 이는 GPU(그래픽 카드)가 연산을 넘겨받아 처리하므로 매우 부드러운 화면 전환을 보장합니다.
렌더링 성능 최적화 전략
렌더링 파이프라인의 원리를 알았으니, 이제 코드에 바로 적용할 수 잇는 최적화 액션 아이템을 알아봅시다.
- 렌더링 차단 리소스(Render-Blocking Resources) 제어하기:
브라우저는 HTML 파싱 중<script>를 만나면 화면 그리는 것을 맘추고 스크립트를 다운로드 및 실행합ㄴ디ㅏ. 화면이 멈춰있는 시간을 줄이려면 무거운 JS 파일은<body>최하단에 배치하거나,<head>에 넣을 경우defer나async속성을 활용해 파싱을 막지 않도록 처리해야 합니다. 반면, CSS는 화면의 뼈대를 빠르게 잡기 위해 반드시<head>태그 안에서 먼저 로드해야 합니다. - 애니메이션은
top/left대신transform으로:
요소를 이동시킬 때 CSS의top,left속성을 변경하면 매 프레임마다 Reflow가 발생해 화면이 뚝뚝 끊기는 원인이 됩니다. 위치 이동이나 크기 조절 애니메이션을 적용할 때는 반드시transform: translate()나scale()을 사용하세요. - DOM 조작은 한 번에 묶어서 처리하기:
JavaScript로 DOM을 여러 번 수정하면 그 횟수만큼 화면을 다시 그리게 됩니다. 여러 요소를 반복문으로 추가할 때는DocumentFragment를 활용해 메모리 상에서 노드를 먼저 조립한 뒤, 마지막에 실제 DOM에 한 번만 추가하는 것이 좋습니다. (React나 Vue 같은 모던 프레임워크가 사용하는 Virtual DOM이 바로 이 렌더링 횟수를 줄여주는 역할을 합니다.)
Chrome DevTools로 코드 측정해보기
성능 최적화는 감이 아니라 ‘측정’에서 시작됩니다. 내가 짠 코드가 얼마나 많은 Reflow와 Repaint를 유발하는지 궁금하다면, 지금 바로 크롬 개발자 도구(F12)의 Performance(성능) 탭을 열어보세요.
녹화 버튼을 누르고 화면을 스크롤하거나 애니메이션을 동작시킨 뒤 중지해 보면, 타임라인에서 보라색(Layout)과 초록색(Paint) 작업이 얼마나 자주, 길게 일어난지 직접 확인할 수 있습니다. 특정 동작에서 보라색 렌더링 블록이 너무 길게 잡힌다면 코드를 점검해 보아야 할 시점입니다.