frontend

리플로우(Reflow)와 리페인트(Repaint) 완벽 비교 가이드: 브라우저 렌더링 최적화

프론트엔드 개발자로서 성능 최적화를 논할 때 가장 많이 언급되는 단어 중 하나가 바로 **리플로우(Reflow)**와 **리페인트(Repaint)**입니다. 특히 React와 같은 라이브러리를 사용하다 보면 프레임 드랍이나 미세한 렉을 마주하게 되는데, 대개 그 원인은 브라우저의 렌더링 파이프라인을 비효율적으로 사용했기 때문입니다. 오늘은 이 두 개념의 차이점과 실무에서 어떻게 최적화해야 하는지 상세히 정리해 보겠습니다.

목차

  1. 브라우저 렌더링 파이프라인의 이해
  2. 리플로우(Reflow)란 무엇인가?
  3. 리페인트(Repaint)란 무엇인가?
  4. 성능 최적화를 위한 실무 사례 및 팁
  5. 개발자의 팁: 하드웨어 가속 활용하기

브라우저 렌더링 파이프라인의 이해

브라우저가 HTML 문서를 받아 화면에 그리기까지는 여러 단계를 거칩니다. 이를 Critical Rendering Path (CRP)라고 부릅니다.

  1. DOM + CSSOM: HTML과 CSS를 파싱하여 트리 구조를 만듭니다.
  2. Render Tree: 위 두 트리를 결합하여 화면에 실제로 보일 노드들만 모은 트리를 생성합니다.
  3. Layout (Reflow): 각 노드가 화면 어디에, 어떤 크기로 위치할지 계산합니다.
  4. Paint (Repaint): 위치가 결정된 노드들에 색상을 입히고 그림자, 텍스트 등을 그립니다.
  5. Composite: 여러 레이어를 하나로 합쳐 최종 화면을 구성합니다.

리플로우(Reflow)란 무엇인가?

**리플로우(Reflow)**는 렌더링 파이프라인의 Layout 단계를 다시 수행하는 것을 의미합니다. 즉, 요소의 기하학적인 속성(너비, 높이, 위치, 폰트 크기 등)이 변경되어 브라우저가 전체 혹은 일부의 위치를 다시 계산해야 할 때 발생합니다.

  • 사례: 브라우저 창 크기 조절(Resize), 요소 추가/삭제, width, height, margin, padding, display, font-size 변경 등.
  • 영향력: 리플로우는 주변 요소나 부모/자식 요소까지 연쇄적으로 계산을 불러일으키기 때문에 비용이 매우 높습니다.

리페인트(Repaint)란 무엇인가?

**리페인트(Repaint)**는 요소의 위치나 크기에는 변화가 없고, 오직 **시각적 외형(색상 등)**만 바뀔 때 발생합니다.

  • 사례: background-color, visibility, outline, color, box-shadow 변경 등.
  • 영향력: 레이아웃 수치를 다시 계산하지 않으므로 리플로우보다는 가볍지만, 여전히 브라우저가 화면을 다시 그려야 하므로 빈번한 발생은 지양해야 합니다.
Comparison diagram showing Reflow triggering the entire pipeline from Layout, while Repaint skips Layout and starts from the Paint stage

성능 최적화를 위한 실무 사례 및 팁

1. 인라인 스타일 변경 최소화

여러 개의 스타일을 하나씩 변경하면 그때마다 리플로우가 발생할 수 있습니다. 대신 클래스를 한 번에 교체하거나 cssText를 활용하세요.

// 비효율적: 리플로우 3번 발생 가능
const el = document.getElementById('box') as HTMLElement;
el.style.width = '100px';
el.style.height = '100px';
el.style.margin = '10px';

// 효율적: 리플로우 1번으로 제한
el.classList.add('is-active'); 

2. 레이아웃 스래싱(Layout Thrashing) 방지

스타일을 쓰기(Write) 전에 읽기(Read)를 반복하면 브라우저가 최신 값을 제공하기 위해 강제로 리플로우를 발생시킵니다.

// 나쁜 예: 반복문 안에서 레이아웃 정보를 읽고 쓰는 행위
items.forEach((item) => {
  const width = container.offsetWidth; // Read (강제 리플로우 유발 가능)
  item.style.width = `${width}px`;    // Write
});

// 좋은 예: 읽기를 밖으로 빼서 한 번만 수행
const width = container.offsetWidth;
items.forEach((item) => {
  item.style.width = `${width}px`;
});

개발자의 팁: 하드웨어 가속 활용하기

리플로우와 리페인트를 모두 피하고 싶다면 Composite 단계만 활용하는 것이 베스트입니다. CSS 속성 중 transformopacity는 브라우저의 GPU(하드웨어 가속)를 사용하여 레이아웃과 페인트 단계를 건너뛸 수 있게 해줍니다.

Tip: 애니메이션을 구현할 때 top/left를 조절하는 대신 transform: translate()를 사용하세요. 전자는 리플로우를 일으키지만, 후자는 컴포지트 단계에서만 처리되어 60fps의 부드러운 움직임을 보장합니다.


자주 묻는 질문 (FAQ)

Q1. visibility: hidden과 display: none의 차이는 무엇인가요?

display: none은 레이아웃에서 아예 제거되므로 리플로우리페인트를 모두 발생시킵니다. 반면 visibility: hidden은 공간은 차지하되 보이지만 않게 하므로 리페인트만 발생시킵니다.

Q2. 리액트의 Virtual DOM은 리플로우를 어떻게 해결하나요?

Virtual DOM은 메모리상에서 변경 사항을 먼저 계산하고, 최종적으로 바뀐 부분만 실제 DOM에 배치 처리(Batching)하여 적용합니다. 이 과정에서 수많은 리플로우 발생을 최소한의 횟수로 묶어주는 역할을 합니다.

Q3. GPU 가속을 위해 모든 요소에 translateZ(0)를 넣어도 될까요?

아니요. 과도한 레이어 생성은 메모리 사용량을 급증시켜 오히려 성능 저하를 초래할 수 있습니다. 꼭 필요한 애니메이션 요소에만 선택적으로 사용해야 합니다.

렌더링 파이프라인의 원리를 이해하면 코드 한 줄이 브라우저에 주는 부담을 가늠할 수 있습니다. 다음 프로젝트에서는 transformwill-change를 적절히 활용하여 더 부드러운 사용자 경험을 만들어 보세요!

이 글이 마음에 드셨나요?

로딩 중...