🙂 이번 시간에는 브라우저 렌더링 과정 그리고 리플로우, 리페인트에 대해 알아보았다!
1. 브라우저 렌더링 과정
1) 브라우저는 어떻게 화면을 그릴까?
- 우리는 주소를 이용해 사이트에 접속한다.
- 사이트 주소는 대게 'http://www.xxxx.com' 형식이며, 이를 도메인 주소라고 부른다.
❓ 그런데 브라우저는 IP 주소를 사용한다고 들었는데, 도메인 주소를 어떻게 IP 주소로 바꾸는 걸까?
방법은 아래와 같다.
- 사용자가 www.google.com(도메인 주소)에 접속한다.
- 이때 브라우저는 도메인 주소로 네트워크 요청을 보낸다.
- 네트워크에서는 DNS서버를 사용해, 도메인 주소에 해당하는 IP주소를 찾는다.
- 찾은 IP주소로 요청을 전송하여 도메인 주소에 해당하는 리소스(html)를 응답으로 받는다.
❓ 아하! 도메인 주소를 주면 도메인 주소에 해당하는 IP주소를 찾아 리소스를 요청하는 방식이구나!
그런데 응답으로 온 리소스(ex. html)는 어떻게 화면에 그려주는 걸까?
- 브라우저는 html(응답값)파일을 해석해서 화면을 렌더링하는데, 과정은 아래 그림과 같다.
- 우선, 브라우저는 DOM, CSSOM트리를 생성한다.
- 그 후 DOM, CSSOM트리를 결합하여 렌더트리를 만든다.
이때 렌더트리는 화면에 보이지 않는 요소는 제외한다.
예를 들어 DOM 트리의 html, head, meta 등의 비시각적 요소나display : none
이 적용된 요소는 렌더 트리에서 제외된다. - 브라우저는 랜더트리를 기반으로 요소의 위치, 크기 등을 계산하는데, 이를
Layout
이라고 부른다. - 마지막으로 요소에 스타일을 적용하는
Paint
과정을 거친다.
- 이처럼 브라우저는 트리 생성 → Layout → paint 과정을 거쳐 화면을 렌더링한다.
2) 브라우저 렌더링과 reflow, repaint의 관계
❓ 그런데 브라우저 렌더링 과정과 reflow, repaint는 무슨 관계가 있는걸까?
- reflow, repaint를 알기위해,
Layout
,paint
부분이 중요하다. Layout
는 앞서 설명했듯이 요소의 위치, 크기를 계산하여 화면에 그리는 과정인데,reflow
와 관련있다.paint
는 배치된 요소에 스타일(색상, 굵기 등)을 적용하는 과정으로repaint
와 관련있다.
- 우선
reflow
는 말 그대로,flow
과정을 다시 하는 작업이다. flow
작업은 곧layout
이며,reflow
가 된다는 건 요소의 위치, 크기 계산을 다시 한다는 말이 된다.repaint
는paint
작업을 다시 하는 걸 말한다.paint
는 요소에 스타일을 적용하는 과정이므로,repaint
는 요소의 스타일을 재적용하는 걸 말한다.
- 결국 오늘 우리가 알고자하는
reflow
,repaint
는 이 브라우저 렌더링의 일부 과정을 다시하는 걸 말한다. - 이제
reflow
,repaint
에 대해 간단히 알아보았으니, 본격적으로 자세히 알아보자!
2. 리플로우(reflow) 와 리페인트(repaint)
- 리플로우와 리페인트는 요소가 시각적으로 변경되었을 때, 변화를 계산하여 화면에 그려주는 작업이다.
- 만약 DOM이 시각적으로 변경되면 리플로우가 발생하여 렌더트리를 재생성하고,
- 생성된 렌더트리를 기반으로 요소를 화면에 그리는 리페인트가 발생한다.
1) 리플로우? 리페인트?
(1) 리플로우 (reflow)
- 리플로우는 요소의 너비, 높이, 위치 등이 변경되어 렌더트리를 재생성하는 작업이다.
📌 리플로우 발생 시점
a. DOM 요소의 기하학적 속성이 변경될때 (width, height 등)
b. 브라우저 사이즈가 변할때
- 리플로우는 비용이 큰 작업인데, 그 이유는 특정 요소에서 리플로우가 발생하면 주변 요소(부모, 자식, 형제)에도 영향을 주기 때문이다.
🤔 특정 요소의 너비 변경이 주변 요소에 리플로우를 일으킨다고? 잘 이해가 안되는데, 예시를 보자
- 만약 세 개의 요소가 있는데, 이중 형제2의 너비를 변경했다.
- 당연히 형제2의 경우 너비를 변경하므로 리플로우가 발생한다.
- 그리나 형제2의 너비 변경으로 인해, 인접한 형제3의 위치도 변경되어 리플로우가 발생한다.
- 즉, 요소 하나의 변화가 주변 요소의 위치나 크기에 영향을 주어, DOM트리 계산 작업이 발생하고 렌더 트리가 재생성되는 것이다.
❓ 형제2의 위치를 크기를 변경했을 때, 리플로우, 리페인트가 발생하는지 개발자 도구로 확인해보았다.
- 방법은 간단한데, 우선 [개발자도구] > [성능] 탭을 연다.
- 기록 버튼을 클릭한다.
- 기록이 되는 동안, 형제2의 너비를 변경한다.
- 기록을 중단하고 결과를 보면, 리플로우, 리페인트가 발생한 걸 확인할 수 있다.
💡 그럼 요소의 너비, 높이와 같은 기하학적 속성 변경 시에만 리플로우가 발생하는가?
답은 아니다! 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
로 요소의 글자색만 변경했을 때 리페인트만 발생하게 된다.
😊 요소의 글자색을 변경했을 때 리페인트만 발생하는지 개발자 도구로 알아보자.
방법은 앞서 리플로우를 확인한 방법과 같다.
- 우선 [개발자도구] > [성능] 탭을 연다.
- 기록 버튼을 클릭한다.
- 기록이 되는 동안, 요소의 글자 색상을 변경한다.
- 기록을 중단하고 결과를 보면, 요소의 글자 색상을 변경했을 때 페인트만 발생한 걸 알 수 있다.
😊 오! 개발자 도구로 리플로우, 리페인트 발생을 볼 수 있다니 좋은데요?
그런데, 브라우저에서 클릭이나 스크롤 동작을 했을 때 리플로우, 리페인트가 발생하는지 알 수 없나요?
- [개발자도구] > [렌더링] 탭을 열면 페인트, 레이아웃이 발생할 때마다 하이라이트를 할 수 있다.
- 아래는 페인트 플래시를 키면, 페인트가 발생하는 영역에 녹색 하이라이트가 발생한다.
- 레이아웃 변경 지역을 클릭하면 변경 지역에 파란색 하이라이트가 발생한다.
2) 리플로우, 리페인트 줄이기
🤜 앞서 우리는 리플로우, 리페인트가 비용이 드는 작업임을 알게 되었다.
그럼 어떻게 해야 리플로우, 리페인트를 줄일 수 있을 지 여러 방법을 알아보자
(1) 애니메이션 요소의 위치를 absolute로 두기
- 애니메이션으로 요소의 위치를 변경할 때, 주변 요소(형제, 부모, 자식)의 위치도 변경되어 리플로우가 여러 번 발생한다.
- 리플로우가 여러 번 발생하지 않도록, 애니메이션이 적용된 요소의
position
을absolute
로 설정하여, 주변 요소에 영향을 주지 않게 하자.
.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 사용하기
방법이 있었다.
이 중 어떤 방법을 쓸 지는 개발자 본인의 상황에 따라 선택하면 좋을 듯 하다 🙂
'개발 기술 > 개발 이야기' 카테고리의 다른 글
[npm] package.json의 version 방식, tilde(~)와 caret(^) (0) | 2023.01.29 |
---|---|
Storybook, UI 컴포넌트를 테스트하게 해줘!(feat. vite) (0) | 2023.01.15 |
[CSS/JS] 웹 폰트를 최적화하는 5가지 방법 (0) | 2022.12.04 |
husky로 Conventional Commit 체크하기 (2) | 2022.11.19 |
[CSS] Media Query 비교연산자(<, >, =) (0) | 2022.11.09 |
댓글