개발 기술/개발 이야기

리플로우, 리페인트와 브라우저 렌더링 알아보기

by GicoMomg 2023. 1. 2.

🙂 이번 시간에는 브라우저 렌더링 과정 그리고 리플로우, 리페인트에 대해 알아보았다!

 

1. 브라우저 렌더링 과정

1) 브라우저는 어떻게 화면을 그릴까?

  • 우리는 주소를 이용해 사이트에 접속한다.
  • 사이트 주소는 대게 'http://www.xxxx.com' 형식이며, 이를 도메인 주소라고 부른다.

❓ 그런데 브라우저는 IP 주소를 사용한다고 들었는데, 도메인 주소를 어떻게 IP 주소로 바꾸는 걸까?
방법은 아래와 같다.

  1. 사용자가 www.google.com(도메인 주소)에 접속한다.
  2. 이때 브라우저는 도메인 주소로 네트워크 요청을 보낸다.
  3. 네트워크에서는 DNS서버를 사용해, 도메인 주소에 해당하는 IP주소를 찾는다.
  4. 찾은 IP주소로 요청을 전송하여 도메인 주소에 해당하는 리소스(html)를 응답으로 받는다.

 

❓ 아하! 도메인 주소를 주면 도메인 주소에 해당하는 IP주소를 찾아 리소스를 요청하는 방식이구나!
그런데 응답으로 온 리소스(ex. html)는 어떻게 화면에 그려주는 걸까?

  • 브라우저는 html(응답값)파일을 해석해서 화면을 렌더링하는데, 과정은 아래 그림과 같다.

  1. 우선, 브라우저는 DOM, CSSOM트리를 생성한다.
  2. 그 후 DOM, CSSOM트리를 결합하여 렌더트리를 만든다.
    이때 렌더트리는 화면에 보이지 않는 요소는 제외한다.
    예를 들어 DOM 트리의 html, head, meta 등의 비시각적 요소나 display : none 이 적용된 요소는 렌더 트리에서 제외된다.
  3. 브라우저는 랜더트리를 기반으로 요소의 위치, 크기 등을 계산하는데, 이를 Layout이라고 부른다.
  4. 마지막으로 요소에 스타일을 적용하는 Paint 과정을 거친다.
  • 이처럼 브라우저는 트리 생성 → Layout → paint 과정을 거쳐 화면을 렌더링한다.

 

2) 브라우저 렌더링과 reflow, repaint의 관계

❓ 그런데 브라우저 렌더링 과정과 reflow, repaint는 무슨 관계가 있는걸까?

  • reflow, repaint를 알기위해, Layout, paint 부분이 중요하다.
  • Layout는 앞서 설명했듯이 요소의 위치, 크기를 계산하여 화면에 그리는 과정인데, reflow와 관련있다.
  • paint는 배치된 요소에 스타일(색상, 굵기 등)을 적용하는 과정으로 repaint와 관련있다.

 

  • 우선 reflow는 말 그대로, flow과정을 다시 하는 작업이다.
  • flow작업은 곧 layout이며, reflow가 된다는 건 요소의 위치, 크기 계산을 다시 한다는 말이 된다.
  • repaintpaint 작업을 다시 하는 걸 말한다.
  • paint는 요소에 스타일을 적용하는 과정이므로, repaint는 요소의 스타일을 재적용하는 걸 말한다.

 

  • 결국 오늘 우리가 알고자하는 reflow, repaint는 이 브라우저 렌더링의 일부 과정을 다시하는 걸 말한다.
  • 이제 reflow, repaint에 대해 간단히 알아보았으니, 본격적으로 자세히 알아보자!








2. 리플로우(reflow) 와 리페인트(repaint)

  • 리플로우와 리페인트는 요소가 시각적으로 변경되었을 때, 변화를 계산하여 화면에 그려주는 작업이다.
  • 만약 DOM이 시각적으로 변경되면 리플로우가 발생하여 렌더트리를 재생성하고,
  • 생성된 렌더트리를 기반으로 요소를 화면에 그리는 리페인트가 발생한다.

1) 리플로우? 리페인트?

(1) 리플로우 (reflow)

  • 리플로우는 요소의 너비, 높이, 위치 등이 변경되어 렌더트리를 재생성하는 작업이다.

📌 리플로우 발생 시점
a. DOM 요소의 기하학적 속성이 변경될때 (width, height 등)
b. 브라우저 사이즈가 변할때

  • 리플로우는 비용이 큰 작업인데, 그 이유는 특정 요소에서 리플로우가 발생하면 주변 요소(부모, 자식, 형제)에도 영향을 주기 때문이다.

🤔 특정 요소의 너비 변경이 주변 요소에 리플로우를 일으킨다고? 잘 이해가 안되는데, 예시를 보자

  • 만약 세 개의 요소가 있는데, 이중 형제2의 너비를 변경했다.
  • 당연히 형제2의 경우 너비를 변경하므로 리플로우가 발생한다.
  • 그리나 형제2의 너비 변경으로 인해, 인접한 형제3의 위치도 변경되어 리플로우가 발생한다.
  • 즉, 요소 하나의 변화가 주변 요소의 위치나 크기에 영향을 주어, DOM트리 계산 작업이 발생하고 렌더 트리가 재생성되는 것이다.

 

❓ 형제2의 위치를 크기를 변경했을 때, 리플로우, 리페인트가 발생하는지 개발자 도구로 확인해보았다.

  1. 방법은 간단한데, 우선 [개발자도구] > [성능] 탭을 연다.
  2. 기록 버튼을 클릭한다.
  3. 기록이 되는 동안, 형제2의 너비를 변경한다.
  4. 기록을 중단하고 결과를 보면, 리플로우, 리페인트가 발생한 걸 확인할 수 있다.

 

💡 그럼 요소의 너비, 높이와 같은 기하학적 속성 변경 시에만 리플로우가 발생하는가?
답은 아니다! JS에서 DOM 관련 메소드를 실행하거나 DOM의 속성에 접근할 때도 발생한다!

  • JS에서 아래와 같은 메소드나 속성을 접근할 때도 리플로우가 발생한다.
  • 아래 속성과 메소드는 요소의 계산된 스타일을 접근할 때 사용하는데, 요소의 최신값을 알기 위해 리플로우가 발생한다.
el.offsetLeft/offsetTop
el.offsetTop
el.offsetWidth/offsetHeight

el.getClientRects()
el.getBoundingClientRect()

window.scrollX/scrollY
window.innerWidth/innerHeight
window.getComputedStyle()
  • 더 자세히 알고 싶다면 이 링크를 참고하자.

 

(2) 리페인트 (repaint)

  • 변경된 요소를 화면에 그려주는 작업을 리페인트라고 한다.

📌 리페인트 발생 시점
a. 리플루우가 발생했을 때
b. 요소의 스타일(색상, 배경색 등)이 변경되었을 때
c. visibility: hidden ↔ visble로 변경될 때

  • 리페인트는 요소의 스타일이 변경되었을 때 발생하므로 리플로우보다 비용이 적다.

🤔 어? 그런데 visibility가 변경되는 게 리페인트라면 display: none도 리페인트인건가?

  • 물론 visibility, display: none 둘 다 요소를 화면에서 숨긴다는 면에서 유사하다.
  • 하지만 visibility는 화면에서 공간을 차지하면서 보이지 않는 반면, display: none 는 영역도 차지하지 않으면서 보이지 않게 한다.
  • 영역을 차지하지 않는다는 말을 곧 렌더트리에서 제외된다는 말이므로, display: none이 적용된 요소는 영역이 사라지면서 주변요소의 위치, 크기에도 영향을 주어 리플로우, 리페인트가 발생한다.
  • 하지만 visibility : hidden 이 적용된 요소는 단순히 보이지 않을 뿐, 크기나 위치가 변하는게 아니기 에 리플로우는 발생하지 않고 리페인트만 발생한다.
  • 마찬가지로 color 로 요소의 글자색만 변경했을 때 리페인트만 발생하게 된다.

 

😊 요소의 글자색을 변경했을 때 리페인트만 발생하는지 개발자 도구로 알아보자.
방법은 앞서 리플로우를 확인한 방법과 같다.

  1. 우선 [개발자도구] > [성능] 탭을 연다.
  2. 기록 버튼을 클릭한다.
  3. 기록이 되는 동안, 요소의 글자 색상을 변경한다.
  4. 기록을 중단하고 결과를 보면, 요소의 글자 색상을 변경했을 때 페인트만 발생한 걸 알 수 있다.

 

😊 오! 개발자 도구로 리플로우, 리페인트 발생을 볼 수 있다니 좋은데요?
그런데, 브라우저에서 클릭이나 스크롤 동작을 했을 때 리플로우, 리페인트가 발생하는지 알 수 없나요?

  • [개발자도구] > [렌더링] 탭을 열면 페인트, 레이아웃이 발생할 때마다 하이라이트를 할 수 있다.

 

  • 아래는 페인트 플래시를 키면, 페인트가 발생하는 영역에 녹색 하이라이트가 발생한다.

 

  • 레이아웃 변경 지역을 클릭하면 변경 지역에 파란색 하이라이트가 발생한다.



2) 리플로우, 리페인트 줄이기

🤜 앞서 우리는 리플로우, 리페인트가 비용이 드는 작업임을 알게 되었다.
그럼 어떻게 해야 리플로우, 리페인트를 줄일 수 있을 지 여러 방법을 알아보자

(1) 애니메이션 요소의 위치를 absolute로 두기

  • 애니메이션으로 요소의 위치를 변경할 때, 주변 요소(형제, 부모, 자식)의 위치도 변경되어 리플로우가 여러 번 발생한다.
  • 리플로우가 여러 번 발생하지 않도록, 애니메이션이 적용된 요소의 positionabsolute로 설정하여, 주변 요소에 영향을 주지 않게 하자.
.animation-target {
  position: absolute;
  animation: moveTo 2s;
}

 

(2) display: none 사용하기

  • display: none이 적용된 요소는 랜더트리에서 제외된다.
  • 만약 요소의 여러 스타일이 수정되어야하는 경우, 먼저 display: none을 설정하고 스타일을 변경한 뒤 display: block을 하는 방법이 있다.
div.style.display = "none";   // (A) 렌더트리에서 제외시키기

// 스타일 수정

div.style.display = "block";  // (B) 스타일 수정 후 렌더트리에 추가하기
  • 이 방법은 (A), (B)에서 각각 리플로우, 리페인트가 1번씩 발생하기에 많은 스타일이 변경되는 경우 비용을 절감할 수 있다.
  • 추가적으로 리플로우, 리페인트가 발생하는 CSS 속성이 궁금하다면 이 사이트를 참고하자.

 

(3) DOM 속성 변경 코드 모으기

  • JS로 여러 DOM의 여러 속성을 변경할 때 코드 순서에 따라 리플로우 횟수를 줄일 수 있다. 아래 예시를 보자.
  • 아래 코드는 el1, el2, el3의 너비를 10px로 변경하는 코드이다.
  • 별 문제가 없어보이는 이 코드는…사실 리플로우가 3번 발생한다!
// BAD
const el1 = document.querySelector('.target-first');
el1.style.width = '10px';

const el2 = document.querySelector('.target-second');
el2.style.width = '10px';

const el3 = document.querySelector('.target-third');
el3.style.width = '10px';

 

😖 엥? 아니 세 요소의 너비를 변경하니까 리플로우가 세 번 발생하는 게 당연한 거 아닌가요?

  • 아니다! 코드 순서만 변경해줘도 리플로우 횟수를 줄일 수 있다!
  • 아래와 같이 DOM의 스타일을 변경하는 코드를 모아두면, 리플로우 횟수를 1번으로 줄일 수 있다.
// GOOD

const el1 = document.querySelector('.target-first');
const el2 = document.querySelector('.target-second');
const el3 = document.querySelector('.target-third');

// dom의 스타일 변경 코드를 한 곳으로 모아둠
el1.style.width = '10px';
el2.style.width = '10px';
el3.style.width = '10px';

 

❓ 코드 순서가 왜 리플로우 횟수에 영향을 주는 걸까?

  • 그 이유의 브라우저가 리플로우 처리 방식 때문이다.
  • 브라우저는 변경할 요소가 있을 때 즉시 처리하지 않고 큐에 저장한다.
  • 그리고 일정 시간이 지나거나 큐에 변경 작업이 쌓였을 때 리플로우를 실행한다.
  • 결국, DOM의 수정을 모아두면 이 수정 코드가 큐에 쌓여있다가 한 번에 처리되기 때문에 리플로우 횟수를 줄 일 수 있는 것이다.

 

(4) 리플로우 유발 함수의 호출을 제한하기

  • 리플로우를 발생시키는 함수나 속성을 매번 호출하지 않고 변수에 저장하는 방법이다.
// BAD
for (let i = 0; i< 10; i++) {
  el.style.width = target.offsetWidth + 10; // 반복문이 돌 때 매번 호출
}

// GOOD
const { offsetWidth } = target;  // 한 번 호출해서 변수에 저장
for (let i = 0; i< 10; i++) {
  el.style.width = offsetWidth + 10;
}

 

(5) CSS로 스타일을 한 번에 변경하기

  • 만약 DOM의 여러 스타일을 변경해야한다면, 스타일을 CSS 클래스로 정의해두고 한번에 변경하자
// BAD
el.style.width = "10px";
el.style.height = "10px";
el.style.borderRadius = "5px";
el.style.backgroundColor = "red";
el.style.left = "20px";
// GOOD
<body>
  <script>
    el.className = 'small';
  </script>
</body>

<style>
.small {
  width: 10px;
  height: 10px;
  border-radius: 5px;
  background-color: red;
  left: 20px;
}
</style>

 

(6) 가상 DOM 사용하기

  • DOM의 스타일을 변경할 때마다, 재렌더링이 발생하는 건 너무 비효율적이다 😟
  • 만약 여러분이 리액트, 뷰을 사용 중이라면, Virtual DOM(가상 DOM)이 비효율을 해소해 줄 것이다.
  • 그 이유는, 가상 DOM은 생성한 DOM을 저장했다가 DOM에 변화가 있다면 메모리에 저장했던 DOM과 현재 변경된 DOM을 비교하여 변경된 부분만 실제 DOM에 반영하기 떄문이다.
  • 결국 모든 DOM에 영향을 주지 않고 변경이 필요한 DOM만 바꿔주기에 불필요한 재렌더링을 막을 수 있다. 🙂



🤜 이번 시간에는 브라우저 렌더링 방식, 리플로우, 리페인트
그리고 리플로우와 리페인트를 줄이는 방법을 알아보았다.
리플로우는 요소의 너비, 높이, 위치 등을 변경할 때 발생하며,
리페인트는 요소의 색상, 배경색 변경 혹은 리플로우가 일어난 경우 발생했다.

이러한 리플로우, 리페인트는 비용이 드는 작업이기에 발생 횟수를 줄이기 위해,

(1) 애니메이션 요소을 절대 위치로 변경하기
(2) display: none 사용하기
(3) DOM 속성 변경 코드 모으기
(4) 리플로우 유발 함수 호출 횟수 줄이기
(5) CSS 클래스로 스타일 변경하기
(6) 가상 DOM 사용하기
방법이 있었다.

이 중 어떤 방법을 쓸 지는 개발자 본인의 상황에 따라 선택하면 좋을 듯 하다 🙂



반응형

댓글