Written by Paul
리플로우(Reflow)와 리페인트(Repaint)
리플로우(reflow)와 리페인트(repaint)는 웹 브라우저가 DOM을 처리하고 화면에 그릴 때 발생하는 두 가지 주요 작업이다. 이 두 과정은 서로 다르며, 브라우저의 렌더링 성능에 큰 영향을 미칠 수 있다.
리플로우
- 정의: 리플로우는 웹 페이지의 레이아웃이 변경될 때 발생하는 과정. 요소의 크기, 위치 또는 형태가 바뀌면 브라우저는 DOM 트리를 다시 계산하여 요소의 배치를 결정합니다. 이 과정에서 레이아웃의 모든 요소를 재배치할 수 있다
- 원인: 다음과 같은 작업들이 리플로우를 발생시킨다
- DOM 요소의 추가, 삭제
- CSS 스타일 변경 (예:
width
,height
,margin
,padding
등) - 윈도우 크기 조절
- 요소의 위치 변경 (예:
position
속성 변화) - 폰트 크기 변경
- 성능 영향: 리플로우는 상대적으로 비싼 연산이다. 페이지의 많은 요소가 영향을 받을 수 있으므로, 성능에 영향을 줄 수 있다. 리플로우가 발생하면, 브라우저는 해당 요소의 모든 후손 요소의 레이아웃을 다시 계산해야 한다.
리페인트
- 정의: 리페인트는 요소의 외관(비주얼)만 변경될 때 발생하는 과정. 요소의 색상, 배경, 텍스트 색상과 같은 시각적 속성이 변경되면, 브라우저는 해당 요소를 다시 그린다. 하지만 레이아웃 자체는 영향을 받지 않는다.
- 원인: 리페인트는 다음과 같은 경우에 발생한다
- 요소의 배경색 변경
- 텍스트 색상 변경
- 이미지 또는 비디오의 src 변경
- 성능 영향: 리페인트는 리플로우보다 비용이 적지만, 여전히 많은 리페인트가 발생하면 전체 성능에 영향을 줄 수 있다. 리페인트는 레이아웃 계산이 필요 없기 때문에 비교적 빠르게 진행되지만, 빈번한 리페인트도 렌더링 성능을 저하시킬 수 있다.
최적화 방법
- 리플로우를 줄이기 위한 최적화
- DOM에 직접적으로 접근하지 않고, 한 번에 변경 사항을 적용한다
- CSS 클래스나 스타일을 한 번에 변경하고, 필요한 경우에만 리플로우를 발생시킨다
- 레이아웃을 변경할 필요가 없는 경우,
visibility
또는opacity
속성을 사용하는 것을 고려한다
- 리페인트를 줄이기 위한 최적화:
- 가능한 한 최소한의 CSS 속성을 변경한다
- CSS 속성 변경을 그룹화하여 한 번의 리페인트로 처리할 수 있도록 한다
- window scroll 이벤트에 addEventListener를 통하여 스크롤을 할때마다 레이아웃을 변경하거나 요소를 변경하게 되어 리플로우 혹은 리페인트를 유발한다면, IntersectionObserver를 활용하는 방식으로도 최적화가 가능하다
IntersectionObserver
IntersectionObserver
는 웹 브라우저에서 요소가 뷰포트(Viewport) 또는 특정한 부모 요소 안에 들어오거나 나가는지를 비동기적으로 관찰할 수 있게 해주는 API이다. 이 API를 사용하면 스크롤에 따라 요소가 화면에 보이는지 여부를 쉽게 감지할 수 있어, 스크롤 이벤트를 직접 사용하는 것보다 성능적으로 더 효율적이다.IntersectionObserver의 주요 개념
- 비동기 관찰:
IntersectionObserver
는 뷰포트 또는 특정 요소의 경계와 타겟 요소의 교차 상태를 비동기적으로 확인할 수 있다
- 콜백 함수: 요소가 관찰 중인 경계와 교차할 때마다 콜백 함수가 호출된다
- 옵션: 관찰할 때, 루트 요소(기본값은 뷰포트), 경계의 마진, 교차 비율 등을 설정할 수 있다
Reflow와 관련된 이벤트 리스너 대체
scroll
이벤트는 성능에 영향을 미치는 문제를 자주 일으키는데, 그 이유는 reflow가 빈번하게 발생하기 때문이다. reflow
는 요소의 위치나 크기 등이 변경될 때 브라우저가 레이아웃을 다시 계산하는 과정으로, 자주 발생하면 성능 저하를 유발한다. scroll
이벤트는 스크롤 위치에 따라 이 reflow
가 자주 발생하게 만들 수 있다.기존에는 스크롤 이벤트를 직접 사용하여 요소가 화면에 보이는지 감지하는 경우가 많았는데, 이는 성능상 문제가 있었다.
scroll 이벤트를 사용하는 방식 (비효율적)
window.addEventListener('scroll', () => { let target = document.querySelector('.target-element'); let bounding = target.getBoundingClientRect(); if ( bounding.top >= 0 && bounding.left >= 0 && bounding.right <= (window.innerWidth || document.documentElement.clientWidth) && bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) ) { console.log('Element is in view!'); } else { console.log('Element is out of view.'); } });
이 코드는
scroll
이벤트가 발생할 때마다 getBoundingClientRect()
를 호출해 요소의 위치를 계산하기 때문에 성능에 영향을 줄 수 있다.IntersectionObserver로 대체하는 방식 (효율적)
let target = document.querySelector('.target-element'); let observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { console.log('Element is in view!'); } else { console.log('Element is out of view.'); } }); }); // 요소 관찰 시작 observer.observe(target);
이 코드는
IntersectionObserver
를 사용해 스크롤 이벤트를 직접 다루지 않고도 요소가 화면에 나타나는 시점을 감지할 수 있다. 브라우저가 뷰포트와 요소 간의 교차 상태를 효율적으로 계산하므로 불필요한 reflow나 layout recalculation을 줄일 수 있다. 이렇게 scroll
이벤트 대신 IntersectionObserver
를 사용하면 브라우저의 최적화된 메커니즘을 활용하여 성능을 개선할 수 있다.브라우저가 렌더링 되는 순서
1. HTML 파싱과 DOM 생성 (Document Object Model)
- 브라우저는 서버에서 HTML 파일을 받아오면, 이를 한 줄씩 읽으면서 DOM 트리를 생성한다
- DOM 트리는 HTML 문서의 논리적 구조를 트리 형태로 표현한 것이다
- 각 HTML 태그는 DOM의 노드가 되고, 이 DOM 트리는 이후 렌더링 과정의 기본이 된다
<html> <body> <h1>Hello World</h1> <p>This is a paragraph.</p> </body> </html>
이 HTML은 다음과 같은 DOM 트리로 변환된다.
Document └── html └── body ├── h1 └── p
2. CSS 파싱과 CSSOM 생성 (CSS Object Model)
- HTML 파일에 포함된 CSS도 파싱된다. CSS 파서가 이를 읽어들이고, 스타일 규칙들을 구조화하여 CSSOM (CSS Object Model)을 만든다
- CSSOM은 각 요소에 적용할 스타일을 정의하는 구조이다. CSSOM 트리와 DOM 트리가 결합되어 화면에 어떻게 그려질지 결정된다
body { font-size: 16px; } h1 { color: blue; }
이는 다음과 같은 CSSOM 트리로 변환된다.
CSSOM ├── body { font-size: 16px; } └── h1 { color: blue; }
3. 렌더 트리 생성
- DOM 트리와 CSSOM 트리가 결합하여 렌더 트리(Render Tree)가 생성된다
- 렌더 트리는 실제 화면에 표시될 요소들만 포함하며,
display: none
같은 속성으로 숨겨진 요소는 렌더 트리에 포함되지 않는다
- 이 렌더 트리는 각 요소가 화면에서 어떻게 그려질지를 결정한다
Render Tree ├── body └── h1 (color: blue, font-size: 16px)
4. 레이아웃 계산 (Layout/Reflow)
- 렌더 트리를 기반으로 각 요소의 위치와 크기를 계산하는 단계이다. 이 과정을 레이아웃 또는 reflow라고 부른다
- 브라우저는 화면의 크기와 CSS 속성에 따라 각 요소가 페이지에서 어디에 배치될지 계산한다
- 이때, 픽셀 단위로 각 요소의 좌표와 크기가 결정된다
5. 페인팅 (Painting)
- 레이아웃이 완료되면, 브라우저는 실제로 화면에 그려질 내용을 준비한다. 이 과정을 페인팅이라고 부르며, 색상, 그림자, 배경 이미지 등 요소의 시각적 속성들이 처리된다
- 브라우저는 각 요소의 스타일 속성에 따라 화면에 그릴 이미지, 텍스트, 경계선 등을 결정한다
6. 합성 (Compositing)
- 복잡한 페이지에서는 여러 레이어를 동시에 처리해야 한다. 예를 들어, 애니메이션이 적용된 요소나,
z-index
를 통해 다른 요소 위에 쌓인 요소들이 있을 수 있다
- 브라우저는 이러한 요소들을 레이어(layer)로 나누어 각각의 레이어를 개별적으로 그린 후, 이를 한 화면에 합성하여 최종 결과를 만든다
- 이 단계는 GPU 가속이 필요한 경우에 GPU가 활용되기도 한다
브라우저 렌더링 과정 요약
- HTML 파싱 → DOM 트리 생성
- CSS 파싱 → CSSOM 트리 생성
- DOM + CSSOM → 렌더 트리 생성
- 레이아웃 계산 (Layout/Reflow)
- 페인팅 (Painting)
- 합성 (Compositing)
JavaScript 처리와 렌더링 중단
- JavaScript는 DOM 파싱을 중단시킬 수 있다. 만약 스크립트가
<script>
태그에 위치해 있으면, 브라우저는 DOM 트리를 완성하기 전에 JavaScript를 실행해야 하므로 렌더링이 잠시 멈춘다
- JavaScript 파일이 DOM 파싱과 렌더링을 차단하지 않게 하려면
defer
나async
속성을 사용한다 defer
: DOM 파싱이 끝난 후 스크립트를 실행함async
: 스크립트 파일을 비동기로 불러와 파싱이 완료되는 즉시 실행함.
쌓임 맥락
쌓임 맥락 (Stacking Context)은 HTML 요소가 화면에서 z축(z-index)을 기준으로 쌓이는 방식을 결정하는 규칙이다. 이 규칙에 따라 요소들이 화면 위에 어떻게 겹쳐지고, 어떤 요소가 다른 요소 위나 아래에 놓이는지 정의된다.
쉽게 말하면, 쌓임 맥락은 브라우저가 2D 평면인 웹페이지에서 각 요소를 z축(깊이) 기준으로 어떻게 배치할지 결정하는 "레이어 시스템"이라고 볼 수 있다.
1. 쌓임 순서 (Stacking Order)
각 요소는 기본적으로 z-index 값에 따라 쌓인다. z-index는 요소의 쌓임 순서를 결정하는 속성으로, z-index 값이 큰 요소일수록 화면에서 앞쪽(위쪽)에 배치된다. 기본적인 쌓임 순서는 다음과 같다.
- 배경 및 테두리(background/border) – 가장 아래에 위치
- 비포함 블록 요소들 – 보통의 텍스트, 이미지 등이 여기에 해당한다
- z-index가
auto
또는 0인 요소
- z-index가 명시된 요소들 – z-index 값이 클수록 위에 쌓임
2. 쌓임 맥락을 생성하는 방법
쌓임 맥락은 특정한 조건이 만족될 때 요소에 의해 생성된다. 새로운 쌓임 맥락이 생성되면, 해당 맥락 내의 모든 자식 요소는 부모 쌓임 맥락 내에서 독립적인 레이어로 처리된다. 즉, 자식 요소들의 z-index는 부모 쌓임 맥락의 외부 요소들과는 독립적으로 계산된다.
쌓임 맥락을 생성하는 주요 조건은 다음과 같다.
- z-index 값이
auto
가 아닌 포지셔닝 요소 (position: relative
,absolute
,fixed
)
opacity
가 1보다 작은 요소 (ex:opacity: 0.5
)
transform
속성이 적용된 요소 (transform: scale(1)
,rotate()
등)
filter
,clip-path
,perspective
,mask
속성 적용 시
will-change
속성이 설정된 요소
<iframe>
,<video>
등 일부 HTML 요소는 자체적으로 쌓임 맥락을 형성
3. 쌓임 맥락의 중요성
쌓임 맥락은 여러 이유로 중요한데, 특히 z-index 충돌을 방지하거나 복잡한 레이아웃에서 요소 간의 깊이 관계를 제어하는 데 도움을 준다. 예를 들어, 한 요소 내에서 z-index 값을 다르게 설정해도 해당 요소가 속한 쌓임 맥락에서 더 높은 z-index 값을 가진 요소보다 위로 올라갈 수 없다.
4. 쌓임 맥락의 예시
<style> .parent { position: relative; z-index: 1; } .child { position: absolute; z-index: 999; width: 100px; height: 100px; background-color: red; } .sibling { position: relative; z-index: 2; width: 100px; height: 100px; background-color: blue; } </style> <div class="parent"> Parent <div class="child"></div> </div> <div class="sibling">Sibling</div>
parent
요소는 z-index가 1인 쌓임 맥락을 생성한다
child
요소는 z-index가 999이지만, 부모 요소가 생성한 쌓임 맥락 내에 속하기 때문에, 부모 요소보다 위로 올라갈 수 없다
sibling
요소는 z-index가 2로parent
보다 크므로, 화면에서는 sibling 요소가 child 요소보다 위에 그려진다
이 예시에서
child
는 z-index 999를 가졌지만, 부모의 쌓임 맥락 내에 있어 그보다 z-index가 작은 sibling
보다 아래에 그려진다.5. 중첩된 쌓임 맥락
쌓임 맥락은 중첩될 수 있다. 상위 요소가 쌓임 맥락을 생성하면, 그 안에 있는 하위 요소는 자신만의 쌓임 맥락을 생성할 수 있다. 하지만, 하위 쌓임 맥락은 부모 쌓임 맥락에 종속적이다. 이 때문에 복잡한 레이아웃을 다룰 때 쌓임 맥락을 올바르게 관리하는 것이 중요하다.