simoong·blog

Reflow와 Repaint 완벽 정리: 느린 웹의 진짜 원인

Reflow vs Repaint — 브라우저 렌더링 성능에 진짜 영향을 주는 요소들

image

최근에 렌더링 관련 성능을 점검하다가, Reflow와 Repaint의 차이를 다시 정리하게 됐어요.
이 둘은 웹 성능 이슈에서 정말 자주 등장하지만, 막상 정확히 구분해서 설명하는 글은 많지 않더라고요.
React, RealGrid, 또는 일반 DOM 조작을 하다 보면 무심코 발생하는 렌더링 비용이 바로 이 두 녀석입니다.


💡 Reflow와 Repaint란?

브라우저는 화면을 그릴 때 단순히 HTML만 해석하지 않아요.
DOM → CSSOM → Render Tree를 만들고, 그걸 기반으로 Layout(배치)과 Paint(그리기)를 수행합니다.

이 과정에서 변경이 생기면, 브라우저는 다시 계산을 해야 하죠.

용어 설명
Reflow (Layout) 요소의 크기나 위치가 바뀔 때 발생하는 레이아웃 재계산
Repaint 색상, 배경 등 시각적 스타일만 바뀔 때 발생하는 다시 그리기

한마디로, Reflow는 구조가 바뀔 때, Repaint는 색이 바뀔 때 생기는 작업이에요.


🧩 예를 들어보면

<div id="box" style="width:100px;height:100px;background:red;"></div>
<script>
  const box = document.getElementById('box')
 
  // 1️⃣ Reflow 발생
  box.style.width = '200px' // 크기나 위치 변경 → 레이아웃 재계산
 
  // 2️⃣ Repaint 발생
  box.style.backgroundColor = 'blue' // 색상 변경 → 다시 그리기만 수행
</script>

위 코드에서 width를 바꾸면 Reflow → Repaint가 모두 발생합니다.
하지만 backgroundColor만 바꾸면 Repaint만 일어나죠.

Reflow는 Paint보다 훨씬 비쌉니다.
브라우저는 바뀐 요소뿐 아니라, 그 영향을 받는 모든 하위·상위 요소의 배치까지 다시 계산해야 하거든요.


⚙️ 성능에 미치는 영향

Reflow는 생각보다 쉽게 발생합니다.
아래는 Reflow를 일으키는 대표적인 상황들이에요.

원인 설명
DOM 크기 변경 width, height, margin, padding, font-size
DOM 구조 변경 노드 추가/삭제 (appendChild, removeChild)
위치 변경 top, left, position, display
윈도우 크기 변경 resize 이벤트
스크롤 / overflow 변경 스크롤 발생 시 레이아웃 재배치
offset, client, scroll 속성 접근 브라우저가 최신 레이아웃을 강제로 계산

이런 변경이 많아지면, 브라우저는 계속해서 Layout → Paint → Composite 단계를 반복해야 합니다.


🔬 실무에서 자주 겪는 예시

1️⃣ 반복 DOM 조작

// ❌ 비효율적인 방식
for (let i = 0; i < 1000; i++) {
  const el = document.createElement('div')
  el.textContent = i
  document.body.appendChild(el) // 매번 Reflow 발생
}

이런 코드는 1000번의 Reflow를 유발합니다.
브라우저는 append될 때마다 레이아웃을 다시 계산하니까요.

해결 방법: DocumentFragment를 이용해서 한 번에 추가

const frag = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
  const el = document.createElement('div')
  el.textContent = i
  frag.appendChild(el)
}
document.body.appendChild(frag) // 단 한 번의 Reflow

2️⃣ 스타일 속성 연속 변경

// ❌ Reflow 여러 번 발생
box.style.width = '200px'
box.style.height = '200px'
box.style.margin = '20px'

해결 방법: classList 또는 CSS text로 묶기

box.classList.add('expanded')
// 혹은
box.style.cssText = 'width:200px;height:200px;margin:20px;'

이렇게 하면 브라우저가 한 번에 렌더링을 갱신할 수 있습니다.


3️⃣ 강제 Reflow (Forced Reflow)

이건 진짜 조심해야 할 부분이에요.
아래처럼 레이아웃 정보를 읽는 코드와 수정 코드가 섞여 있으면 브라우저가 강제로 Reflow를 실행합니다.

el.style.width = '200px'
console.log(el.offsetWidth) // 💥 강제 Reflow 발생

왜냐면 offsetWidth를 읽을 때 브라우저는 “최신 레이아웃”을 보장해야 하니까
모든 계산을 다시 수행하게 됩니다.

즉, “읽기-쓰기-읽기” 패턴은 성능의 적입니다.


🧠 실무에서의 최적화 팁

상황 권장 방식
여러 요소를 수정해야 할 때 DocumentFragment / display:none 처리 후 일괄 변경
스타일 변경이 많은 경우 CSS 클래스로 묶어서 교체
scroll, resize 이벤트 Debounce / Throttle로 제어
애니메이션 transform, opacity 속성 중심으로 (Reflow 없음)
Reflow 강제 발생 방지 offset, scroll 속성 접근 최소화

Reflow는 줄일 수는 있어도 완전히 없앨 수는 없어요.
하지만 “언제, 왜 발생하는지” 알고 있으면 대부분의 성능 문제를 예방할 수 있습니다.


⚡ Reflow를 피하고 싶은 CSS 팁

  • transformopacity는 Reflow 없이 애니메이션 가능
  • position: absolute 또는 fixed를 이용해 영향 범위 축소
  • will-change로 브라우저에게 미리 최적화 힌트 제공
  • contain 속성으로 하위 요소의 레이아웃 독립성 보장 (CSS Containment)

이런 속성들은 Chrome DevTools의 Rendering 탭 → “Paint flashing” 옵션으로 확인할 수 있어요.


📊 정리

구분 Reflow Repaint
발생 시점 레이아웃 구조 변경 시 시각적 속성 변경 시
비용 높음 중간
대표 트리거 width, height, position, font-size 등 color, background 등
최적화 포인트 레이아웃 변경 최소화 불필요한 페인트 줄이기

💬 마무리하며

Reflow와 Repaint는 눈에 보이지 않지만, 브라우저 성능에 직접적인 영향을 주는 요소예요.
특히 React나 Vue처럼 Virtual DOM을 쓰더라도, 결국 마지막 단계에서 Real DOM 업데이트가 일어나면 Reflow는 피할 수 없습니다.

즉, Virtual DOM은 “Reflow를 덜 일으키는 전략”일 뿐, 없애주는 기술이 아니에요.
이 차이를 이해하고 코드를 작성하면, 훨씬 부드럽고 효율적인 UI를 만들 수 있습니다.

브라우저는 생각보다 똑똑하지만, 작은 실수 하나에도 큰 비용을 치릅니다.
코드를 짤 때 항상 “지금 이 줄이 Reflow를 유발할까?”를 한번쯤 떠올려보세요.


#Reflow #Repaint #브라우저렌더링 #렌더링성능 #CSS성능 #JavaScript성능 #웹최적화 #프론트엔드성능 #Reflow최적화 #VirtualDOM