리액트가 빠르다는 말을 들은 적이 있을 것이다.
Virtual DOM을 통해 기존의 DOM보다 빠르게 렌더링된다는 말이 있다.
과연 이 렌더링이 어떻게 되길래 빠르다고 하는 것일까?
먼저 Javascript에서 어떻게 렌더링을 하는지를 알아볼 필요가 있다.
우리는 html
을 통해 문서의 구조를 만든다. 이것을 DOM
이라고 배웠다.
html
태그들을 트리 구조로 이룬 것을 말한다.
그런데 이것만 있어서는 되겠는가?
css
로 스타일링을 해줘야한다.
font-size
, background-color
등 각종 css
를 꾸미기 위해, 부트스트랩, Tailwind CSS같은 것도 지원하고, Styled-components나 emotion 같은 css in js 도 많이 발전해왔다.
이런 css
도 트리 구조인 cssom
으로 이루어져 있다.
DOM
+CSSOM
=Render Tree
두 트리가 합쳐서 실제 사용자의 눈에 보이는 렌더 트리로 합쳐진다.
display = None
같은 속성이 들어가있으면 렌더링할 때 아예 제외되는 등, 최종적으로 눈에 보이는 것을 다룰 때 사용된다.
렌더링도 사실 2가지로 더 자세하게 쪼개진다.
Reflow(레이아웃), Repaint(스타일링)으로 나뉜다.
Reflow는 레이아웃과 관련되어 있는 것으로, width
, height
같은 픽셀크기와 관련된 전체적인 틀을 의미한다.
html
이 시각화 될때, 프레임이 변화하면 reflow가 일어난다고 생각하면 이해가 편할 것이다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Reflow 예시</title>
<style>
.box {
width: 100px;
height: 100px;
border: 1px solid black;
padding: 10px;
margin: 20px;
}
</style>
</head>
<body>
<div class="box">Box</div>
<button onclick="changeSize()">크기 변경</button>
<script>
function changeSize() {
const box = document.querySelector('.box');
box.style.width = '150px'; // 너비 변경
box.style.height = '150px'; // 높이 변경
}
</script>
</body>
</html>
.box
클래스 태그의 width
, height
가 변화하면 Reflow가 발생한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Repaint 예시</title>
<style>
.box {
width: 100px;
height: 100px;
background-color: red; /* repaint을 유발하는 속성 */
}
.hover-effect {
background-color: blue; /* hover 시 적용될 색상 */
}
</style>
</head>
<body>
<div class="box"></div>
<script>
const box = document.querySelector('.box');
box.addEventListener('mouseover', function() {
box.classList.add('hover-effect'); // hover-effect 클래스 추가
});
box.addEventListener('mouseout', function() {
box.classList.remove('hover-effect'); // hover-effect 클래스 제거
});
</script>
</body>
</html>
여기서는 .hover-effect
를 클래스에 추가시켜줘서 호버링 스타일링을 변경한다.
이때 Repaint가 일어나는 것이다.
실제로 자바스크립트는 속성이 변경될 때 마다 리렌더링을 진행한다.
이 렌더링은 자원이 매우 크다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>렌더링 최적화하지 않은 예시</title>
<style>
.box {
width: 100px;
height: 100px;
background-color: red;
margin: 10px;
}
</style>
</head>
<body>
<div class="container">
<div class="box" id="box1"></div>
<div class="box" id="box2"></div>
<div class="box" id="box3"></div>
</div>
<script>
function updateBoxes() {
document.getElementById('box1').style.backgroundColor = 'blue';
document.getElementById('box2').style.backgroundColor = 'green';
document.getElementById('box3').style.backgroundColor = 'yellow';
}
// 예시: 1초마다 updateBoxes 함수 호출
setInterval(updateBoxes, 1000);
</script>
</body>
</html>
3개의 요소를 Repaint 시키는 것을 1초마다 반복한다.
이렇게 되면 1초에 3번의 Repaint가 일어난다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>렌더링 최적화된 예시</title>
<style>
.box {
width: 100px;
height: 100px;
margin: 10px;
}
.blue { background-color: blue; }
.green { background-color: green; }
.yellow { background-color: yellow; }
</style>
</head>
<body>
<div class="container">
<div class="box blue" id="box1"></div>
<div class="box green" id="box2"></div>
<div class="box yellow" id="box3"></div>
</div>
<script>
const boxes = document.querySelectorAll('.box');
let currentColorIndex = 0;
const colors = ['blue', 'green', 'yellow'];
function updateBoxes() {
boxes.forEach((box, index) => {
box.className = 'box ' + colors[(currentColorIndex + index) % colors.length];
});
currentColorIndex++;
}
// 예시: 1초마다 updateBoxes 함수 호출
setInterval(updateBoxes, 1000);
</script>
</body>
</html>
3개의 요소를 한꺼번에 updateBoxes()
함수에서 변경을 한 후 반영한다.
이렇게 되면 1초에 1번 Repaint가 된다.
리액트에선 Virtual DOM으로 실제 Javascript의 DOM을 모방한 트리구조를 2개 가지고 있다.
하나는 렌더링 이전의 상태를 저장한 vDOM, 다른 하나는 상태가 변경된 vDOM이다.
이 두 vDOM을 비교해 어떤 노드가 변경되었는지 확인한다.
변경된게 있다면 그때서야 vDOM의 내역을 Real DOM에 반영한다.
변경되지 않았다면 그냥 그대로 있는다.
리액트에서는 이것을 해주기에 DOM보다 빠르다고 하는 것이다. (렌더링 속도가)
vue.js
에서도 이 방법을 채택한다. (링크 참조)
하지만 단점이 있다.
vDOM을 따로 만들기에 메모리를 더 차지하고, vDOM을 최초에 구축하는데 있어서 오히려 더 시간이 걸릴 수 있다.
그리고, 아까 최적화 된 코드에서 updateBoxes()
처럼 본인이 잘만 최적화를 할 수 있다면, vDOM 없이도 빠르게 렌더링 할 수 있다.
그래서 나온 방식이 있다.
구글에서 운용중인 Angular.js
에서는 이 방식을 새로 만들었다.
Incremental DOM은 컴파일 타임에 코드를 분석하고 최적화한다.
컴포넌트를 렌더링 하는 js
코드를 컴파일하면 DOM을 조작하는 코드로 변환된다.
// 컴파일되기 전 angular.js 코드
angular.module('myApp', [])
.component('myComponent', {
bindings: {
name: '<'
},
template: `
<div>
Hello, <span>{{ $ctrl.name }}</span>!
</div>
`
});
// 컴파일된 후 pseudo code 비스무리한 렌더링 코드
function render(name) {
return elementOpen('div', null, null);
text('Hello, ');
elementOpen('span');
text(name);
elementClose('span');
text('!');
return elementClose('div');
}
이런 컴포넌트 코드가 있다고 하자.
name
이 바뀔것 처럼 생겼다.
실제로도 name이 변경되면 text(name)
이 name
과 바인딩 되어있기에 저부분만 변경된 것을 감지해서 DOM에 반영한다.
두 DOM의 방식은 속도가 특정상황에서 서로 빠를 수도 느릴 수도 있다.
하지만 이 말은 맞다고 전하고 싶다.
Incremental DOM은 Virtual DOM의 메모리를 차지하는 부분을 최적화하기 위해 만들었다.
그리고 Real DOM보다 Virtual DOM이 무조건 빠르다! 이말은 틀렸다.
Virtual DOM이 Incremental DOM보다 빠르다! 이것도 틀렸다.
탄생배경이나 의도를 이해하는 것이 중요한 것이다.