원래 react를 사용하다 vue로 넘어오게 되었습니다.
리액트의 작동 방식, 렌더링 방식을 본 적이 있는데
뷰는 어떨지 궁금해 찾아보게 되었습니다.
Vue 렌더링 방식
먼저 공식문서에 있는 그림입니다.
하나씩 보겠습니다.
Template
<template>
<article class='my-data-container'>
<div>
<h1 class='my-data-hub-h1'>
마이 데이터 허브
</h1>
<section class='my-data-hub-box my-data-content'>
<MyDataHubNav />
<router-view :key='$route.fullPath'/>
</section>
</div>
</article>
</template>
뷰로 UI를 그릴때는 위처럼 html 문법으로 템플릿을 만듭니다.
그러면 뷰는 템플릿을 컴파일해서 render function을 만듭니다.
Render Function
render(h) {
return h('article', { class: 'my-data-container' }, [
h('div', {}, [
h('h1', { class: 'my-data-hub-h1' }, '마이 데이터 허브'),
h('section', { class: 'my-data-hub-box my-data-content' }, [
h(MyDataHubNav),
h('router-view', { key: this.$route.fullPath })
])
])
]);
}
(Vue Template Explorer 참고용)
node의 많은 정보를 담고있어서 상당히 복잡한 함수가 나옵니다.
vue2 에서는 위 h (createElement)를 파라미터로 받아와 사용하지만
vue3 에서는 import { h } from ‘vue’ 로 바로 h 를 사용합니다.
참고로 h 는 하이퍼스크립트(hyperscript)의 약자라고 합니다.
아무튼 위 리턴값으로 가상 DOM을 반환합니다.
Render Function 과정을 거치는 이유
- 일관성 유지
뷰는 템플릿 말고도 jsx나 render method를 통해서 컴포넌트를 만들 수 있는데
어떤 방식을 쓰든 렌더 함수를 거쳐서 가상 DOM 을 반환하도록 만들어 내부 메커니즘의
일관성을 유지합니다.
- 최적화
컴포넌트의 정적인 부분을 캐시해 최적화 기능을 만듭니다.
Render Function 을 써야하는 경우
선언적으로 동적 컴포넌트를 사용하기 힘들때
export default class Test extends Vue {
tmp = 3
render(h) {
let element = h('div', 'test');
for (let i = 0; i < this.tmp; i++) {
element = h('div', [element]);
}
return element;
}
}
<div>
<div>
<div>
test
</div>
</div>
</div>
위 예시처럼 중첩(nesting) 태그를 사용할 때 렌더 함수를 써야 합니다.
Virtual DOM
{
tag: 'article',
data: {
class: 'my-data-container'
},
children: [
{
tag: 'div',
data: {},
children: [
{
tag: 'h1',
data: {
class: 'my-data-hub-h1'
},
text: '마이 데이터 허브'
},
{
tag: 'section',
data: {
class: 'my-data-hub-box my-data-content'
},
children: [
{
tag: 'MyDataHubNav',
data: {}
},
{
tag: 'router-view',
data: {
key: this.$route.fullPath
}
}
]
}
]
}
]
}
가상 DOM은 리액트나 뷰에서 효율적인 DOM 업데이트를 위해 사용하는 방법(패턴)이며
워낙 자료도 많고 또 자세히 파헤치려면 내용이 많아서 한마디로 요약하자면
‘js 객체’ 라고 보시면 됩니다.
이 가상 DOM을 가지고 뷰는 실제 DOM에 적용하여 렌더링을 합니다.
Vue의 가상 DOM 렌더링 최적화
가상 DOM이 업데이트되어 이전 가상 DOM과 비교가 일어날 때
어디 부분에서 변화가 일어났는지 무슨 변화가 일어났는지 DOM 트리의 노드들을
하나하나 비교해봐야합니다. (완전 탐색)
하지만 이렇게 원시적으로 한다면 좋은 성능을 기대하기 힘듭니다.
그래서 뷰는 Compiler-Informed Virtual DOM 방식을 사용합니다.
Compiler-Informed Virtual DOM
단어 뜻 그대로 컴파일(템플릿 -> 렌더 함수) 할 때 힌트를 알려준다고 보면 될 것 같습니다.
렌더 함수를 작성할 때 가상 DOM이 어디에서 무엇이 업데이트 되었는지 찾기 쉽도록 흔적을 남겨준다고 생각하면 됩니다.
여러 방법이 있지만 공식문서에는 크게 효율적인 세가지 방법이 소개됩니다.
- Static Hoisting
첫번째는 정적 호이스팅입니다.
자바스크립트에서 호이스팅은 실행 컨텍스트, 렉시컬 환경등 다른 핵심 개념들과 관련이 있어 더 정확한 정의가 있지만 특정 코드나 선언을 코드의 상위 레벨로 이동시키는 것을 의미한다고 봐도 무방할 것 같습니다.
템플릿 코드
<div>
<div>foo</div>
<div>bar</div>
<div></div>
</div>
렌더 함수
import {
createElementVNode as _createElementVNode,
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock
} from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, "foo", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", null, "bar", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_hoisted_2,
_createElementVNode("div", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
]))
}
위 코드에서 보시다시피 <div>foo</div>
와 <div>bar</div>
는 정적인 부분이라
호이스팅을 통해 렌더 함수 밖 상단으로 보내 변수로 선언한다음 재사용을 해서 createElementVNode 의 무분별한 사용을 줄여 렌더링 최적화를 하고있습니다.
이 밖에도
템플릿
<div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div></div>
</div>
반복되는 정적 코드를
렌더 함수
import {
createElementVNode as _createElementVNode,
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
createStaticVNode as _createStaticVNode
} from "vue"
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<div class=\"foo\">foo</div><div class=\"foo\">foo</div><div class=\"foo\">foo</div><div class=\"foo\">foo</div><div class=\"foo\">foo</div>", 5)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_createElementVNode("div", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
]))
}
호이스팅해서 하나의 변수로 만들어 가상 DOM이 동일한 부분은 빨리 처리할 수 있도록 도와 렌더링 최적화를 하고 있습니다.
- Patch Flags
패치 플래그는 가상 DOM이 어떻게 업데이트를 해야하는지 신호를 제공합니다.
<div :class="{ active }"></div>
<input :id="id" :value="value">
<div></div>
import {
normalizeClass as _normalizeClass,
createElementVNode as _createElementVNode,
toDisplayString as _toDisplayString,
Fragment as _Fragment,
openBlock as _openBlock,
createElementBlock as _createElementBlock
} from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_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 */))
}
createElementVNode 함수의 네번째 인자를 보면 숫자가 있습니다.
각 숫자는 가상 DOM이 무슨 업데이트를 해야하는지를 알려줍니다.
https://github.com/vuejs/core/blob/main/packages/shared/src/patchFlags.ts
위 vue git code를 보면 1은 문자만 바꼈을 때, 2는 class, 3은 스타일 등등 뭐가 업데이트 되었는지 알려줍니다. (이때 뷰는 비트 연산을 사용해 더욱 빠르게 연산할 수 있습니다.)
- Tree Flattening
마지막 세번째는 트리 플래트닝입니다.
예시를 보면 바로 이해할 수 있습니다.
<div> <!-- 루트 블록 -->
<div>...</div> <!-- 추적하지 않음 -->
<div :id="id"></div> <!-- 추적함 -->
<div> <!-- 추적하지 않음 -->
<div></div> <!-- 추적함 -->
</div>
</div>
정적 노드들을 제외하고 :id
, `````` 를 가지고 있는 정적 노드들만을
묶어서 병합(flattened)시킵니다.
div (block root)
- div with :id binding
- div with binding
그러면 나중에 업데이트가 필요할 때 전체 노드가 아닌 위 병합된 노드만 탐색해서
최적화를 시킬 수 있습니다.
Vue 2 와 Vue 3 최적화의 차이
위 Compiler-Informed Virtual DOM 기법은 Vue 3의 최적화 방법입니다.
이전 Vue 2에서는 keep-alive, diffing 알고리즘, static tree optimization, 비동기 컴포넌트 등등 여러 방법을 쓴다고 합니다.
위 Vue 3의 최적화 기법보다 좋지 않아 그냥 이런게 있다고만 알면 충분할 것 같습니다.
[참고]