Vue Document : Render Mechanism

2023. 10. 16. 01:03Today I Learned

Rendering Mechanism | Vue.js 을 읽고 번역한 글입니다 

 


  • Vue는 어떻게 template를 가져와 실제 DOM node로 변환할까?
  • Vue는 어떻게 해당 Dom node들을 효율적으로 업데이트할까?

⇒ Vue의 내부 렌더링 메커니즘을 살펴봄으로써 이에 대한 답을 찾아보자

 

Virtual DOM

  • 메모리에 존재하고, real DOM과 동기화되어있는 UI의 가상적인 표현
  • React에 의해 개척되었으며 Vue 등 다양한 프레임워크에서 채택하는 개념
  • 표준적인 구현은 없음. 기술보다는 패턴에 가까움
  • 예시
const vnode = {
  type: 'div',
  props: {
    id: 'hello'
  },
  children: [
    /* more vnodes */
  ]
}
  • 런타임 렌더러는 virtual DOM tree를 순회하면서 real DOM tree를 구성할 수 있음. 이러한 과정을 mount라고 함.
  • 2개의 virtual DOM tree가 있을 때, 렌더러는 이들을 순회하면서 두 트리를 비교하고, 차이를 확인한 후, 차이를 real DOM에 반영함. 이러한 과정을 patch (혹은 diffing 또는 reconcilation) 라고 함.
  • virtual DOM의 이점
    • 개발자가 UI를 선언적인 방식으로 구성할 수 있도록 하는 반면, 직접적인 DOM 조작은 렌더러한테 맡기도록 할 수 있음.

Render Pipeline

  1. Compile
    • Vue template가 render function으로 컴파일되는 단계
      • render function의 리턴값은 virtual DOM tree
      • build를 통해 미리 컴파일해둘 수도 있고, runtime compiler를 통해 그때 그때 컴파일할 수도 있음.
  2. Mount
    • runtime renderer는 render function을 호출해서 리턴된 가상 DOM tree를 탐색하고, 이를 통해 real DOM node를 생성함.
    • reactive effect로서 수행되므로, 사용된 모든 reactive state를 추적함.
  3. Patch
    • 컴포넌트의 state가 변경되면, 업데이트된 새로운 virtual DOM tree가 생성됨. runtime renderer는 새로운 vDOM tree를 탐색하고, 이전 tree랑 비교한 후 rDOM에 변경 사항을 적용함

 

Template vs. Render Functions

  • Vue template는 vDOM render function으로 컴파일됨
  • Vue는 template의 컴파일 과정을 건너뛰고 바로 render function을 작성할 수 있는 API도 제공하고 있음
    • 이 render function을 사용하면 JavaScript의 문법을 사용해서 자유자재로 vnode를 조작할 수 있음. → 동적인 로직으로 vnode를 다뤄야 할때 template보다 유리
  • 그럼 왜 편한 render function 안쓰고 굳이 template를 쓰는걸 추천할까?!
    1. 실제 HTML과 비슷하므로 쓰고 이해하기 더 편하다
      • 기존 html tag를 더 쉽게 사용하고, 기존 사례들을 적용하고, css를 사용할 수 있으며, 디자이너가 이해하고 수정하기도 쉬움
    2. 선언적인 문법을 사용하므로 정적 분석하기 쉬움
      • Vue의 template compiler가 컴파일 타임 최적화(아래에서 다룸)를 적용하기에 용이함 → vDOM의 성능 향상
  • 일반적으로는 template로도 웬만한 어플리케이션 다 만들 수 있음
  • render function은 매우 동적인 렌더링 로직을 처리해야 하는 component에서만 사용하면 됨

 

Compiler-Informed Virtual DOM

  • React를 포함한 대부분의 vDOM 구현은 순수한 runtime이다
    • reconcilation 알고리즘은 들어오는 vDOM에 대한 어떠한 가정도 할 수 없으므로, tree를 전부 순회하면서 모든 vnode의 props를 비교해야 함
    • tree의 일부가 전혀 변하지 않았더라도 리렌더링때마다 새로운 vnode가 생성되므로 불필요하게 메모리를 낭비함 → 이러한 brute force reconcilation은 선언성과 정확성을 대가로 효율성을 희생하는, virtual DOM의 가장 큰 취약점
  • 그러나 Vue는 다르다! 🤣 : 컴파일러와 런타임을 모두 제어하여 컴파일 타임 최적화를 수행함
    • Vue의 컴파일러는 template를 정적으로 분석하고 생성된 코드에 hint를 남겨 런타임 시 shortcut을 사용할 수 있도록 함
    • 이와 동시에 개발자가 엣지 케이스를 직접적으로 제어할 수 있도록 여지를 남겨둚
    ⇒ 이러한 하이브리드한 접근법을 Compiler-Informed Virtual DOM이라고 한다

Vue의 template compiler가 vDOM의 런타임 성능을 향상시키기 위해 사용하는 주요 최적화 기법을 알아보자:

Static Hoisting

template에는 종종 다음과 같이 어떠한 동적 바인딩도 없는 부분이 있음.

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

Inspect in Template Explorer

  • foo와 bar div는 정적이므로, 리렌더링 시 vnode를 재생성하고 dffing을 수행할 필요가 없음
  • Vue compiler는 렌더링 함수 내에서 이러한 정적 vnode의 생성을 호이스팅하고, 모든 렌더링에서 동일한 vnode가 재사용되도록 함. 렌더러는 이전 vnode와 새 vnode가 동일한 것을 발견하면 diffing을 완전히 건너뛸 수도 있음
  • 게다가 정적요소 여러개가 반복되면, 이들을 순수한 HTML string을 갖는 하나의 static vnode로 묶기도 함. (Example)
    • 초기 마운팅 시 HTML string을 통해 innerHTML을 세팅하며, 이 때 생성된 DOM node는 캐싱됨
    • 똑같은 내용을 가진 node가 어플리케이션의 다른 부분에서 재사용도는 경우, 새로운 DOM node는 네이티브 함수인 cloneNode()를 통해서 만들어지는데, 이는 매우 효율적임

 

Patch Flags

동적 바인딩이 있는 단일 element의 경우에도 컴파일 타임 많은 정보를 추론할 수 있음.

<!-- 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

  • 이러한 element의 render function을 생성할 때, Vue는 각 요소에 필요한 업데이트 타입을 인코딩함.
    • 마지막 인자(위 예시에서 2)는 patch flag.
      • 한 요소가 여러개의 patch flag를 가질 수도 있음 → 하나의 single number로 병합됨
      • 런타임 렌더러는 비트 연산을 통해 플래그를 확인하여 특정 업데이트 작업을 수행해야하는지 판단함
      if (vnode.patchFlag & PatchFlags.CLASS /* 2 */) {
        // update the element's class
      }
      
      • patch flag를 통해 동적 바인딩이 있는 요소를 업데이트할 때 필요한 작업을 최소한으로만 할 수 있음
  • 또한 Vue는 vnode가 가지고 있는 자식 노드의 타입도 인코드함.
    • 예를들어, 여러 개의 root node를 가지고 있는 template는 fragment로 표현되는데, 대부분의 경우 이들의 순서는 절대 바뀌지 않음.
      • 이러한 정보도 patch flag를 통해 표현됨
      export function render() {
        return (_openBlock(), _createElementBlock(_Fragment, null, [
          /* children */
        ], 64 /* STABLE_FRAGMENT */))
      }
      
      • 이 예시에서는 patch flag를 통해 런타임 시 자식 요소들의 순서 조정을 건너뛸 수 있음.

 

Tree Flattening

export function render() {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    /* children */
  ], 64 /* STABLE_FRAGMENT */))
}
  • 이 코드를 보면, virtual DOM tree의 root는 createElementBlock()이라는 특별한 함수 호출로 생성되는 것을 알 수 있음.
  • 개념적으로 ‘block’은 안정적인 내부 구조를 가진 template의 일부를 의미함.
    • 이 예시에서는 v-if나 v-for과 같은 구조적 지시문이 포함되어 있지 않기 때문에, 전체 템플릿은 하나의 block만을 가지고 있음.
  • 각 block은 patch flag가 있는 모든 자손 노드들을 추적함
    • 추적의 결과로, block은 동적인 자손 노드만 포함하는 평면화된 배열로 표현될 수 있음
    • 이 컴포넌트를 리렌더링하는 경우, 전체 트리 대신 평면화된 트리만 순회하면 됨. 이를 Tree Flattening이라고 하며, reconcilation 중 정적인 부분을 건너뛰어 검사해야하는 노드 수를 크게 줄일 수 있음.
  • v-if 또는 v-for는 새로운 block 노드를 생성함
  • 자손 block는 부모의 block array를 통해 추적됨

 

Impact on SSR Hydration

  • patch flag와 tree flattening은 Vue의 SSR Hydration 성능을 크게 상승시키는데 기여함
    • 단일 element의 hydration의 경우, vnode의 fatch flag를 통해 빠른 경로로 수행될 수 있음
    • Hydration 시 block node와 그의 동적 자손들만 순회하므로, template 레벨에서 부분적인 hydration을 효율적으로 수행할 수 있음

 

'Today I Learned' 카테고리의 다른 글

Vite와의 첫만남  (0) 2023.03.08
[네트워크] HTTPS  (0) 2023.02.05