본문 바로가기
Study

[Vue.js] Vue에서의 Virtual DOM

by 안자두 2023. 5. 24.

DOM

DOM이란 Document Object Model이라는 뜻으로 웹 페이지, 즉 HTML 문서를 계층적 구조와 정보로 표현하며, 이를 제어할 수 있는 프로퍼티와 메서드를 제공하는 트리 자료구조이다.

브라우저 렌더링 과정

HTML 파일을 받으면 문자열을 파싱해서 DOM 트리로 만든다. 이 DOM 트리와 CSS로 만든 CSSOM 트리를 결합하여 Render 트리를 만들고 레이아웃과 페인팅 작업을 하면 비로소 우리의 화면이 우리의 눈에 보이게 된다.

 


 

Virtual DOM

JavaScript의 경우, DOM을 조작할 때마다 트리를 수정하여 레이아웃을 다시 그리고 리페인팅을 해야 한다. 이 과정이 반복되게 되면 비용이 커지게 되고 충분히 무거워질 수 있는 작업이 된다.

이때 등장한 것이 바로 Virtual DOM이다. Virtual DOM은 state가 변경될 때마다 실제 DOM이 그려지기 전에, 새로운 내용이 담긴 Virtual DOM을 생성한다.
이전의 Virtual DOM과 현재 새로 그려진 Virtual DOM을 비교하여 어떤 element가 변했는지를 내부적인 diff 알고리즘을 통해 비교한다. 

Virtual DOM이 효율적인 이유는, 한 리스트 안에서 몇번의 변화가 있든, 매번 DOM을 새로 그렸던 이전과는 달리, 실제 DOM에는 한 번만 적용한다는 점이다.

Virtual DOM 기본 로직

Virtual DOM은 그림과 같이 변경된 부분을 Virtual DOM에 반영하고, 기존의 Virtual DOM과 변경사항이 추가된 Virtual DOM을 diff 알고리즘을 통해 비교하여 변경된 부분만 실제 DOM에 업데이트한다.

DOM 조작에 비용이 가장 많이 발생하는 지점은 레이아웃 작업이기 때문에 굉장히 효율적이다.

 


 

Vue에서의 Virtual DOM

Vue에서도 React처럼 Virtual DOM이 있다. 조금 다른 점은 React의 경우에는 런타임에만 Virtual DOM을 구현하는 반면, Vue의 경우에는 컴파일러와 런타임 모두에 Virtual DOM을 제어해 런타임 성능을 향상시킨다.

이게 무슨 의미냐면,
React의 경우에는 diff 알고리즘은 들어오는 Virtual DOM에 대해 유추를 할 수 없으므로 트리를 완전 순회하고 모든 vnode의 props를 비교해야 한다. 만약 변경되지 않는 부분이어도 비교를 하게 되어 불필요한 메모리를 소모하게 된다는 것이다. 이것은 Virtual DOM의 가장 비판적인 부분 중 하나인데, 선언성과 정확성의 대가로 효율성을 희생하게 한다.

반면 Vue의 경우에는 컴파일러가 템플릿을 정적으로 분석하고 생성된 코드에 힌트를 남겨 런타임 때, 해당 부분들만 비교를 할 수 있도록 돕는다. 이 접근 방식을 Compiler-Informed Virtual DOM라고 한다.

Vue 템플릿 컴파일러가 수행한 최적화들에는 Static Hoisting, Patch Flags, Tree Flattening 등이 있다.

 

Static Hoisting

<div>
  <div>foo</div> <!-- hoisted -->
  <div>bar</div> <!-- hoisted -->
  <div>{{ dynamic }}</div>
</div>

foo와 bar가 있는 div의 경우 정적인 요소이다. 때문에 변할 일이 없어 굳이 vnode를 다시 만들고 diffing 할 필요가 없다. Vue 컴파일러는 이런 요소들을 호이스팅하여 모든 렌더링에서 동일한 vnode를 재사용한다. 때문에 렌더러에서는 호이스팅된 부분으로 diffing 할 필요가 없다는 것을 알게 된다.

또한, 위의 경우처럼 연속적으로 존재한다면 압축하여 하나로 축약할 수 있다. (예시)

 

Patch Flags

<!-- class binding only -->
<div :class="{ active }"></div>

<!-- id and value bindings only -->
<input :id="id" :value="value">

<!-- text children only -->
<div>{{ dynamic }}</div>

동적 바인딩이 있는 단일 요소의 경우, 컴파일 때 많은 정보를 추론할 수 있다. 이런 요소에 대한 렌더링 함수 코드를 생성할 때, Vue는 vnode 생성 호출에서 직접 필요한 각각의 업데이트 유형을 인코딩한다.

_createElementVNode("div", {
    class: _normalizeClass({ active: _ctx.active })
}, null, 2 /* CLASS */),
_createElementVNode("input", {
    id: _ctx.id,
    value: _ctx.value
}, null, 8 /* PROPS */, ["id", "value"]),
    _createElementVNode("div", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
  ], 64 /* STABLE_FRAGMENT */))

각 vnode의 마지막 인수(2, 8, 64)는 패치 플래그이다. 런타임 렌더러는 특정 작업을 수행해야 하는지 여부를 결정하기 위해 비트 연산을 사용하여 플래그를 확인할 수 있다.

비트 연산은 매우 빠르기 때문에 이 패치 플래그를 사용하면 Vue는 동적 바인딩으로 요소를 업데이트할 때, 필요한 최소한의 작업을 수행할 수 있다.

 

Tree Flattening

구조적 지시문이 포함되어 있지 않은 단일 블록의 경우를 제외한 블록만을 순회한다. 이를 트리 평탄화라고 한다.

<div> <!-- root block -->
  <div>...</div>         <!-- not tracked -->
  <div :id="id"></div>   <!-- tracked -->
  <div>                  <!-- not tracked -->
    <div>{{ bar }}</div> <!-- tracked -->
  </div>
</div>

위와 같은 코드일 때, 

div (block root)
- div with :id binding
- div with {{ bar }} binding

아래처럼 단일 블록을 제외한 블록들만 도출된다. 이 과정으로 인해 virtual DOM을 비교할 노드 수를 크게 줄일 수 있다.

 

 

위와 같은 최적화를 통해 Vue에서의 virtual DOM은 React 등 다른 virtual DOM에서보다 조금 더 효율적인 결과를 도출할 수 있다.


 

REF

https://developer.mozilla.org/ko/docs/Web/API/Document_Object_Model/Introduction

https://vuejs.org/guide/extras/rendering-mechanism.html

https://callmedevmomo.medium.com/virtual-dom-react-%ED%95%B5%EC%8B%AC%EC%A0%95%EB%A6%AC-bfbfcecc4fbb

 

728x90