[React] Virtual DOM

DOM이란 무엇인가?

가상 DOM(Virtual DOM)을 이해하기 위해선, DOM이 무엇인지 먼저 확실하게 이해해야 한다.

DOM(Document Object Model)은 우리가 작성한 HTML 코드가 브라우저에 의해 파싱되어 구조화 및 계층화 된 노드 트리다. 글만 봐서는 이해하기 힘든 것이 당연하기에 아래 HTML 코드를 봐보자.

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<title>My first web page</title>
</head>
<body>
<h1>Hello, world!</h1>
<p>How are you?</p>
</body>
</html>

이처럼 우리가 작성한 HTML 코드가 DOM으로 표현되면 아래와 같을 것이다.각 노드들이 트리 형태로 연결되어 있는 모습이다. 브라우저는 이를 기반으로 페이지에 우리가 작성한 HTML코드를 표시해준다.

DOM Tree

DOM을 쓰면 되지 굳이 왜…?

DOM에 특별한 하자가 있는 것도 아닌데, 왜 우리는 가상 돔을 사용해야 하는 것일까?

DOM을 조작(manipulate)하는 것에 대한 시간복잡도가 비효율적이기 때문이다. 다시 말해, DOM의 변화를 브라우저에 적용시키는 것이 굉장히 느리다. 물론, 확실하게 짚고 넘어가야 할 것이 있는데, DOM 자체의 조작이 느린 것은 아니다. DOM이 새로운 변동사항을 적용하여 최신화 되는 과정은 굉장히 빠르다. javascript가 이를 처리하기 때문이다. 하지만, 이러한 최신화된 DOM이 렌더트리가 되고, 다시 새롭게 배치 및 페이지 상에 그려지는 데에 시간이 오래걸리는 것이 더 정확한 표현일 것이다.

출처: bitsofcode, “What, exactly, is the DOM?”

즉, 위 사진에서 노란색 렌더트리 ~ 브라우저 렌더링 까지의 시간이 오래 걸린다는 것이다.

원본 DOM만 사용할 경우 (전체가 다시 렌더링 되어야 함)

가상 DOM을 사용할 경우

따라서, 우리는 원본 DOM만을 사용해서는 DOM의 변동사항을 브라우저에 효율적이고 빠르게 렌더링시키지 못한다. 이 블로그를 예로 들어, 댓글 하나만 추가됐을 뿐인데 전체 페이지를 다시 재배치하고 페인팅해야 한다면 어떨까? 굉장히 비효율적일 것이다. 따라서 우리가 기대하는 효율적인 렌더링은 “변화가 일어난”부분만 리렌더링 되는 것이다. (위 사진과 같이)

이 때문에 리액트에서는 자체적인 비교 알고리즘을 활용하여 이전 DOM과 최신 DOM 간의 비교를 통해 “변화가 일어난 부분”만 리렌더링 될 수 있도록 해준다(재조정). 이는 우리가 리액트를 사용하는 주된 이유 중 하나일 것이다. 그럼 이제 Virtual DOM이 무엇인지 알아보도록 하자.


Virtual DOM이란?

가상 DOM은 원본 DOM을 복제하여 메모리에 담아두고, 원본 DOM과 동기화 한 하나의 DOM이자 객체다.

비유적으로 표현하면, 가상 DOM을 전자기기의 설계도라고 표현할 수도 있을 것 같다. 설계도가 변경된다고 해서 즉각적으로 원래 존재하던 전자기기의 설계가 변경되진 않는다. 설계도를 기반으로 전자기기를 재조립하거나 새로 만들어야 변경사항이 적용된다는 것이다. 이처럼 Virtual DOM 또한 즉각적이고 직접적으로 브라우저에 표시된 페이지를 변화시키진 못한다. 단지, 가상DOM과 가상DOM 간의 비교 알고리즘(diffing)을 통해 변동사항을 감지하고 원본 DOM에 변동사항을 적용시키는 것일 뿐이다.

여기서 또 중요한 내용이 나온다. 가상 DOM과 가상 DOM의 비교다.

ntitled

“엥? 원본 DOM과 가상 DOM을 비교하는 것 아니었어?”

부분적으로는 맞는 말일 수도 있다. 하지만 정확하게는 원본 DOM과 동일한 가상 DOM과 vs 변경사항이 적용된 가상 DOM 간의 비교가 이루어지는 것이다. (물론 원본 가상DOM과 원본 DOM은 같기 때문에, 원본 DOM과 가상 DOM(변경사항이 적용된)의 비교라고 해도 큰 무리는 없을 것이지만 말이다.)

그럼 Virtual DOM은 어떻게 만들어질까?

코드를 활용해 설명해보도록 하겠다.

1
2
3
4
5
## 우리가 작성한 HTML 코드
<ul class="”itemList”">
<li>item 1</li>
<li>item 2</li>
</ul>

위 HTML 코드를 javascript를 통해 객체화 한다면 아래의 모습과 같다.

1
2
3
4
5
6
7
8
// 위의 HTML 코드를 DOM의 형태로 객체화
{ type: 'ul', props: { 'class' : 'itemList' }, children: [
{ type: 'li', props: {}, children: ['item1'] },
{ type: 'li', props: {}, children: ['item2'] }
] }

// 즉, 하나의 node는 아래와 같은 형태를 갖는 것이다.
{ type: ‘…’, props: { … }, children: [ … ] }

이를 jsx로 다시 코드를 짠 뒤, 바벨을 활용해 오브젝트 형태로 트랜스파일링 해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** @jsx transpile */

// 위의 주석 코드를 통해 바벨은 jsx코드를 아래 작성한 transpile함수에 맞게 트랜스파일링 해준다.
function transpile(type, props, ...children) {
return { type, props, children };
}

// 우리가 작성한 jsx 코드.
const newHTML = (
<ul class='list'>
<li>item 1</li>
<li>item 2</li>
</ul>
);

트랜스파일 된 jsx

이제 트랜스파일 된 자바스크립트 오브젝트를 활용해 가상DOM으로써 활용해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/** @jsx transpile */

function transpile(type, props, ...children) {
return { type, props, children };
}

const newHTML = (
<ul class='list'>
<li>item 1</li>
<li>item 2</li>
</ul>
);

function createElement(node) {
console.log(node);
if (typeof node === 'string') {
return document.createTextNode(node);
}
// 최상단 node의 type은 'ul'이다.
const $el = document.createElement(node.type); // ul
// node.children = [...{type:'', props:{}, children:[...]}] => 즉, item1, item2의 node다.
node.children
.map((child) => createElement(child))
.forEach($el.appendChild.bind($el)); // 각 하위 노드들이 차례대로 $el에 Child로써 어펜드 된다.
return $el; // 최종 DOM
}

const $root = document.getElementById('root');
$root.appendChild(createElement(a));

여기까지가 순수 자바스크립트를 활용해 Virtual DOM을 생성한 과정이다. (물론 이는 단지 예시일 뿐, 리액트에서는 다른 방식으로 Virtual DOM을 생성하고 manipulating 할 것이다.)

여하튼, Virtual DOM을 통해 변경사항만 리렌더링 된다.

리액트는 DOM 엘리먼트의 타입, 속성(className, style 등)의 변동사항을 캐치하여 해당 타입 또는 속성만 갈아 끼워 넣어주기도 한다.

뿐만 아니라, 엘리먼트의 내용(텍스트)이 변경된 경우에도 변경된 엘리먼트만 업데이트 한다.

1
2
3
4
<ul>
<li>first</li>
<li>second</li>
</ul>
1
2
3
4
<ul>
<li>first</li>
<li>third</li> // 이 부분만 변경
</ul>

또한, 아래와 같이 새롭게 추가되거나 제거된 엘리먼트도 효율적으로 핸들링 한다.

이는 first 엘리먼트는 같네? → second 엘리먼트도 같네? → 어 third는 없었는데 추가됐네?와 같이 순차적이고 선형적인 방식으로 비교가 진행된다.

1
2
3
4
5
6
7
8
9
10
<ul>
<li>first</li>
<li>second</li>
</ul>

<ul>
<li>first</li>
<li>second</li>
<li>third</li> // 추가
</ul>

하지만 이런 경우는 어떨까?

1
2
3
4
5
6
7
8
9
10
11
12
// old
<ul>
<li>apple</li>
<li>orange</li>
</ul>

// new
<ul>
<li>grape</li>
<li>apple</li>
<li>orange</li>
</ul>

<li>apple</li> ≠ <li>grape</li>, <li>orange</li> ≠ <li>apple</li>… 와 같은 식으로 비교(diffing)가 진행되기 때문에 상단에 grape만 추가됐을 뿐인데 모든 엘리먼트들이 업데이트되게 될 것이다.

이러한 상황이 매우 크고 복잡한 엘리먼트에서 발생한다면 심각한 비효율성을 초래할 것이다. 따라서 이 때 활용되는 것이 Keys다.

Keys

리액트는 엘리먼트에 key 속성이 존재한다면, key를 기준으로 엘리먼트들을 비교한다.

따라서 아래의 코드는 이제 apple과 orange에는 변동사항이 없는 것으로 판별될 것이다. 따라서 grape 엘리먼트의 추가만 변동사항으로 인식하여 트리를 업데이트 할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
// old
<ul>
<li key='1'>apple</li>
<li key='2'>orange</li>
</ul>

// new
<ul>
<li key='0'>grape</li>
<li key='1'>apple</li>
<li key='2'>orange</li>
</ul>

정리 및 요약

가상 DOM은 원본 DOM의 조작 또는 변동사항이 일어날 경우, 전체 DOM이 다시 생성 및 재배치되는 비효율성을 방지하고자 활용되는 DOM이다. 가상DOM은 메모리에 저장되는 원본 DOM의 복제본이며, 리액트에서의 재조정 및 비교의 주체가 되어 변경사항만 선택적으로 리렌더링 될 수 있도록 한다. 즉, 가상 DOM은 변경사항이 생겼을 때, 전체 DOM이 리렌더링되는 것이 아닌, 변경사항만 적용되어 선택적 리렌더링이 될 수 있도록 하는 데 핵심적인 역할을 한다.

세 줄 요약

  1. 가상 DOM은 원본 DOM의 복제본이다.
  2. 가상 DOM과 비교알고리즘(diffing)을 통해 변경된 부분만 기존 DOM 트리에 업데이트 한다.
  3. 변경사항만 리렌더링 되기 때문에 훨씬 빠르고 효율적인 성능을 기대할 수 있다.

참조한 자료

How to write your own Virtual DOM

What, exactly, is the DOM?

What is Virtual DOM? How Virtual DOM works ? What is Reconciliation ? What is diffing algorithm? What makes React so fast ?

재조정 (Reconciliation) - React


Author

Hoonjoo

Posted on

2022-03-11

Updated on

2022-03-11

Licensed under

Comments