본문 바로가기

FrontEnd

Vue3의 렌더링 메커니즘 알아보기

반응형
mount, patch, diffing, reconciliation, update...

vue3

Vue3은 렌더링을 어떻게 수행할까요?
공식 문서의 번역입니다. https://vuejs.org/guide/extras/rendering-mechanism.html

 

Rendering Mechanism | Vue.js

Less than 48 hours to get 45% off at Vue School Access 800+ lessons including the Vue.js 3 Masterclass

vuejs.org

Vue는 템플릿을 어떻게 가져와 실제 DOM 노드로 변환할까요?
Vue는 이러한 DOM 노드를 어떻게 효율적으로 업데이트할까요?
여기에서 Vue의 내부 렌더링 메커니즘을 살펴봄으로써 이러한 질문에 대한 설명을 시도합니다.


Virtual DOM

Vue의 렌더링 시스템이 기반으로 하는 가상 DOM이라는 용어에 대해 들어본 적이 있을 것입니다.
가상 DOM(VDOM)은 이상적인 또는 "가상" UI 표현이 메모리에 유지되고 "실제" DOM과 동기화되는 프로그래밍 개념입니다.
이 개념은 React에 의해 개척되었으며 Vue를 비롯한 다양한 구현을 통해 다른 많은 프레임워크에 적용되었습니다.

가상 DOM은 특정 기술보다 패턴에 가깝기 때문에 하나의 정식 구현이 없습니다.
간단한 예를 사용하여 아이디어를 설명할 수 있습니다.

const vnode = {
  type: 'div',
  props: {
    id: 'hello'
  },
  children: [
    /* more vnodes */
  ]
}
여기서 vnode는 <div> 요소를 나타내는 일반 JavaScript 객체("가상 노드")입니다.
여기에는 실제 요소를 만드는 데 필요한 모든 정보가 포함되어 있습니다.
또한 더 많은 children vnode를 포함하므로 가상 DOM 트리의 루트가 됩니다.

런타임 렌더러는 가상 DOM 트리를 탐색하고 이 트리를 기반으로 실제 DOM 트리를 구성할 수 있습니다.
이 프로세스를 마운트(mount)라고 합니다.

가상 DOM 트리의 복사본이 두 개 있는 경우 렌더러는 두 트리를 살펴보고 비교하여 차이점을 파악하고
이러한 변경 사항을 실제 DOM에 적용할 수도 있습니다.
이 프로세스를 "patch"라고 하며 "diffing" 또는 "reconciliation"이라고도 합니다.

가상 DOM의 주요 이점은 개발자가 DOM 조작을 렌더러에 맡기면서,
선언적 방식으로 원하는 UI 구조를 프로그래밍 방식으로 생성, 검사 및 구성(합성)할 수 있는 기능을 제공한다는 것입니다.
주 : 만드는 방법을 설명하지 않음. 구조와 데이터로만 UI를 표현

Render Pipeline

높은 수준에서 이것은 Vue 컴포넌트가 마운트될 때 발생합니다.
  1. Compile : Vue 템플릿은 가상 DOM 트리를 반환하는 함수인 렌더  함수로 컴파일됩니다. 이 단계는 빌드 단계를 통해 미리 수행하거나 런타임 컴파일러를 사용하여 즉석에서 수행할 수 있습니다.
  2. Mount : 런타임 렌더러는 렌더 함수를 호출하고 반환된 가상 DOM 트리를 살펴보고 이를 기반으로 실제 DOM 노드를 생성합니다. 이 단계는 반응 효과(reactive effect)를 통해 수행되므로 사용된 모든 반응 종속성을 추적합니다.
  3. Patch : 마운트 중에 사용된 종속성이 변경되면 효과가 다시 실행됩니다. 이번에는 업데이트된 새로운 가상 DOM 트리가 생성됩니다. 런타임 렌더러는 새 트리를 탐색하고 이전 트리와 비교하고 필요한 업데이트를 실제 DOM에 적용합니다.
효과 > 리렌더링 > 렌더 함수 호출 > 가상돔 변경 > 마운트 / (패치/디핑/조화)


Templates vs. Render Functions

Vue 템플릿은 가상 DOM 렌더 함수(render function)로 컴파일됩니다.
Vue는 또한 템플릿 컴파일 단계를 건너뛰고 렌더 함수를 직접 작성할 수 있는 API를 제공합니다.
렌더 함수는 JavaScript의 모든 기능을 사용하여 vnode로 작업할 수 있기 때문에
매우 동적인 로직을 다룰 때 템플릿보다 더 유연합니다.
그렇다면 Vue가 기본적으로 템플릿을 권장하는 이유는 무엇입니까? 여러 가지 이유가 있습니다.
  1. 템플릿은 실제 HTML에 더 가깝습니다. 이를 통해 기존 HTML 스니펫을 재사용하고, 접근성 모범 사례를 적용하고, CSS로 스타일을 지정하고, 디자이너가 이해하고 수정할 수 있습니다.
  2. 템플릿은 보다 선언적인 구문으로 인해 정적으로 분석하기가 더 쉽습니다. 이를 통해 Vue의 템플릿 컴파일러는 가상 DOM의 성능을 향상시키기 위해 많은 컴파일 시간 최적화를 적용할 수 있습니다(아래에서 논의함).
실제로 템플릿은 애플리케이션의 대부분의 사용 사례에 충분합니다.
렌더 함수는 일반적으로 매우 동적인 렌더링 논리를 처리해야 하는 재사용 가능한 컴포넌트에서만 사용됩니다.
렌더 함수 사용법은 Render Functions & JSX에서 더 자세히 설명합니다.

Compiler-Informed Virtual DOM

컴파일 타임 정보를 사용하는 가상돔
React 및 대부분의 다른 가상 DOM 구현은 순수하게 런타임 전용입니다.
조정 알고리즘은 들어오는 가상 DOM 트리에 대해 가정할 수 없으므로 트리를 완전히 탐색하고 모든 vnode의 props를 비교해야 합니다. 또한 트리의 일부가 변경되지 않더라도 매번 다시 렌더링할 때마다 새로운 vnode가 생성되어 불필요한 메모리 낭비가 발생합니다.
이는 가상 DOM이 가장 비판받는 측면 중 하나입니다.
다소 무차별적인 조정 프로세스는 선언성과 정확성에 대한 대가로 효율성을 희생합니다.
Vue에서 프레임워크는 컴파일러와 런타임을 모두 제어합니다.
이를 통해 컴파일러와 밀접하게 결합된 렌더러만 활용할 수 있는 많은 컴파일 시간 최적화를 구현할 수 있습니다.
컴파일러는 템플릿을 정적으로 분석하고 생성된 코드에 힌트를 남겨 런타임이 가능한 한 바로 가기를 사용할 수 있도록 합니다.
동시에 엣지 케이스의 직접적인 제어를 허용하기 위해 렌더 함수 레이어를 직접 구현할 수 있는 기능도 유지합니다.
우리는 이 하이브리드 접근 방식을 Compiler-Informed Virtual DOM이라고 부릅니다.
아래에서는 가상 DOM의 런타임 성능을 향상시키기 위해 Vue 템플릿 컴파일러가 수행하는 몇 가지 주요 최적화에 대해 설명합니다.

정적 호이스팅

템플릿에는 동적 바인딩이 포함되지 않은 부분이 있는 경우가 많습니다.
<div>
  <div>foo</div> <!-- hoisted -->
  <div>bar</div> <!-- hoisted -->
  <div>{{ dynamic }}</div>
</div>

Inspect in Template Explorer

foo 및 bar div는 정적입니다. vnode를 다시 생성하고 각 리렌더링에서 이를 비교하는 것은 불필요합니다.
Vue 컴파일러는 자동으로 렌더 함수에서 vnode 생성 호출을 끌어올리고 모든 렌더에서 동일한 vnode를 재사용합니다.
또한 렌더러는 이전 vnode와 새 vnode가 동일한 것을 발견하면 차이 비교를 완전히 건너뛸 수 있습니다.

패치 플래그

동적 바인딩이 있는 단일 요소의 경우 컴파일 시간에 많은 정보를 추론할 수도 있습니다.
<!-- class binding only -->
<div :class="{ active }"></div>

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

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

Inspect in Template Explorer
이러한 요소에 대한 렌더 함수 코드를 생성할 때
Vue는 vnode 생성 호출에서 직접 각 요소에 필요한 업데이트 타입을 인코딩합니다.

createElementVNode("div", {
  class: _normalizeClass({ active: _ctx.active })
}, null, 2 /* CLASS */)
마지막 인수 2는 patch flag입니다.
요소에는 단일 숫자로 병합되는 여러 패치 플래그가 있을 수 있습니다.
그런 다음 런타임 렌더러는 bitwise operations을 사용해 플래그를 확인하여
특정 작업을 수행해야 하는지 여부를 결정할 수 있습니다.
if (vnode.patchFlag & PatchFlags.CLASS /* 2 */) {
  // update the element's class
}

비트 단위 검사는 매우 빠릅니다.
패치 플래그를 사용하면 Vue는 동적 바인딩으로 요소를 업데이트할 때 필요한 최소한의 작업을 수행할 수 있습니다.

Vue는 또한 vnode가 가진 자식 타입을 인코딩합니다.
예를 들어 여러 루트 노드가 있는 템플릿은 fragment로 표시됩니다.
대부분의 경우 이러한 루트 노드의 순서가 절대 변경되지 않는다는 것을 알고 있으므로
이 정보를 런타임에 패치 플래그로 제공할 수도 있습니다.
export function render() {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    /* children */
  ], 64 /* STABLE_FRAGMENT */))
}
따라서 런타임은 루트 프래그먼트에 대한 하위 순서 조정을 완전히 건너뛸 수 있습니다.

트리 평탄화(Tree Flattening)

이전 예제에서 생성된 코드를 다시 살펴보면
반환된 가상 DOM 트리의 루트가 특별한 createElementBlock() 호출을 사용하여 생성되었음을 알 수 있습니다.

export function render() {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    /* children */
  ], 64 /* STABLE_FRAGMENT */))
}

개념적으로 "블록"은 내부 구조가 안정적인 템플릿의 일부입니다.
이 경우 전체 템플릿에는 v-if 및 v-for와 같은 구조적 지시문이 포함되어 있지 않기 때문에 단일 블록이 있습니다.

각 블록은 패치 플래그가 있는 모든 하위 노드(직계 자식뿐만 아니라)를 추적합니다.
동적 바인딩 여부!
<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
이 컴포넌트를 다시 렌더링해야 하는 경우 전체 트리 대신 평평한 트리만 탐색하면 됩니다.
이를 Tree Flattening이라고 하며 가상 DOM 조정 중에 통과해야 하는 노드의 수를 크게 줄입니다.
템플릿의 모든 정적 부분은 효과적으로 건너뜁니다.
v-ifv-for directive는 새 블록 노드를 생성합니다.
<div> <!-- root block -->
  <div>
    <div v-if> <!-- if block -->
      ...
    <div>
  </div>
</div>

자식 블록은 부모 블록의 동적 children 배열 내에서 추적됩니다.
이것은 상위 블록에 대한 안정적인 구조를 유지합니다.


Impact on SSR Hydration

패치 플래그와 트리 병합은 Vue의 SSR 수화 성능(SSR Hydration)도 크게 향상시킵니다.
  • 단일 요소 수화는 해당 vnode의 패치 플래그를 기반으로 빠른 경로를 취할 수 있습니다.
  • 블록 노드와 동적인 children만 수화 도중에 방문하면 템플릿 수준 부분 수화를 효과적으로 달성할 수 있습니다.

 

반응형