Virtual DOM 에 대해서 잘 정리되어 있는 글을 발견하여 공유하고자 번역을 하였습니다.
원본은 시리즈가 2편으로 되어있으나, 번역본에서는 한 게시글에 모두 담았습니다.번역을 하면서 글을 자연스럽게 만들기 위해 의역을 많이 첨가하였습니다. (따라서, 오역이 많을 수 있습니다... 댓글로 알려주시면 수정하겠습니다. 훈수 좋아합니다...)
원본(시리즈 1) What is the Virtual DOM? (Let's build it!)
원본(시리즈2) Let's build a VDOM
프론트엔드를 개발하다 보면 아마 가상 돔 혹은 쉐도우 돔에 대해서 들어보고 사용해보셨을 겁니다. 아마 여러분들이 많이 사용하시는 JSX 또한 사실은 가상 돔의 문법적 설탕입니다. 이번 튜토리얼에서는 Virtual DOM 이 어떻게 생겼는지 알아볼 것이고, 이어서 어떻게 구현할 수 있는지 보여드리도록 하겠습니다!
Virtual DOM?
DOM 조작은 상당히 무거운 작업입니다. 자바스크립트 객체에 프로퍼티를 할당하는 것과의 속도 차이는 약 0.4ms 정도가 차이가 납니다. 한 번으로는 그 차이가 크게 느껴지지 않습니다. 그러나 횟수가 누적될수록 그 차이가 점점 커집니다.
// Assigning a property to an object 1000 times
let obj = {};
console.time("obj");
for (let i = 0; i < 1000; i++) {
obj[i] = i;
}
console.timeEnd("obj");
// Manipulating dom 1000 times
console.time("dom");
for (let i = 0; i < 1000; i++) {
document.querySelector(".some-element").innerHTML += i;
}
console.timeEnd("dom");
위 코드를 실행시켰을 때, 첫번째 반복문은 약 ~3ms이 시간이 걸렸던 반면에 두 번째 반복문은 약 ~41ms의 시간이 소요되었습니다.
이제 실제 예시를 한번 살펴봅시다.
function generateList(list) {
let ul = document.createElement('ul');
document.getElementByClassName('.fruits').appendChild(ul);
fruits.forEach(function (item) {
let li = document.createElement('li');
ul.appendChild(li);
li.innerHTML += item;
});
return ul;
}
document.querySelector("ul.some-selector").innerHTML = generateList(["Banana", "Apple", "Orange"])
만약 여기서 배열의 값들이 변경된다면 DOM을 조작해서 요소들을 수정해야하만 합니다. 아래와 같이 말입니다.
document.querySelector("ul.some-selector").innerHTML = generateList(["Banana", "Apple", "Mango"])
여기서 혹시 어디가 잘못되었는지 아시겠나요?
배열에서 Orange가 빠지고 Mango가 추가되었습니다. 즉, 배열에서는 단 하나의 변경만 일어났는데 우리는 전체를 다 수정해버렸습니다. 변경분만 DOM에 반영되도록 코드를 작성해야겠지만, 우리 개발자의 천성은 게으릅니다.
그래서 Virtual DOM이라는 것이 만들어진 것입니다!
이제 우리는 Virtual DOM이 왜 필요한지를 알았으니 바로 Virtual DOM 핵심으로 들어가보겠습니다.
Virtual DOM?!
Virtual DOM이란 DOM을 객체로써 표현을 한 것입니다.
<div class="contents">
<p>Text here</p>
<p>Some other <b>Bold</b> content</p>
</div>
그래서 위의 HTML 코드는 아래와 같은 VDOM 객체로 작성될 수 있습니다.
let vdom = {
tag: "div",
props: { class: 'contents' },
children: [
{
tag: "p",
children: "Text here"
},
{
tag: "p",
children: ["Some other ", { tag: "b", children: "Bold" }, " content"]
}
]
}
// 실제로는 더 많은 프로퍼티들이 존재할 수 있습니다. 위 VDOM 객체는 간략화된 버전입니다.
위 VDOM을 보시면 어느 정도 이해가 가실 거라고 생각합니다. 특히나 React를 사용해보신 분이라면 더욱 친숙하게 느껴지실 수도 있습니다.
VDOM은 기본적으로 아래의 프로퍼티를 갖는 객체입니다
- tag: 태그의 이름을 나타냅니다. (가끔 type이라고 불리기도 합니다.)
- props: 모든 props을 소유하고 있습니다.
- children: (1) 콘텐츠가 text인 경우 string 값을 가집니다. (2) 콘텐츠가 엘리먼트들이면 VDOM 배열 값을 가집니다.
VDOM 사용법
- DOM을 변경하는 대신에 VDOM을 변경합니다.
- 어느 함수가 DOM과 VDOM의 차이점을 체크하여 실제로 변경된 부분만 DOM에 반영합니다.
- 변경 사항으로 사용된 VDOM은 최신의 DOM 내용과 같기 때문에, 다음 번에 변경될 VDOM과 비교를 할 때에 최신의 VDOM을 재활용할 수 있습니다.
VDOM을 사용하므로 얻을 수 있는 이점
아까 사용했던 generateList
함수를 더 발전시켜 실전적인 예제를 만들어봅시다.
function generateList(list) {
// VDOM 생성하는 부분.
// 추후 설명하겠습니다...
}
patch(oldUL, generateList(["Banana", "Apple", "Orange"]));
patch 함수가 무엇인지 신경 쓰지 않으셔도 됩니다. 단순히 DOM에 변경분을 반영한다고 생각하시면 됩니다.
자, 이제 다시 배열의 원소가 변경되었다고 생각해봅시다!
patch(oldUL, generateList(["Banana", "Apple", "Mango"]));
patch
함수는 3번째 li
만 변경되었음을 찾아내고 3번째 li
만 DOM에서 바꿀 겁니다. 더 이상 모든 요소를 바꿀 필요가 없습니다!
이것이 VDOM의 전부입니다. 다음에는 어떻게 VDOM을 구현하는지 알아보도록 하겠습니다.
VDOM은 기능 상으로는 아래 4가지를 할 수 있어야 합니다.
- Virtual Node를 만듭니다. (편의상 vnode라고 부르겠습니다.)
- VDOM을 마운트(혹은 로드)합니다.
- VDOM을 언마운트합니다.
- Patch 합니다. (두 vnode를 비교하고 변경분을 파악하여 마운트 합니다.)
vnode 만들기
이 함수는 간단한 유틸리티 함수입니다.
function createVNode(tag, props = {}, children = []) {
return { tag, props, children}
}
// Vue 혹은 다른곳에서는 이 함수를 h 라고 부릅니다. (hyperscript)
// hypertext를 javascript 객체로 바꾸어줍니다.
vnode 마운팅
마운팅이란 #app
과 같은 컨테이너에 vnode를 추가하는 것을 의미합니다.
아래 함수는 모든 노드의 자식을 반복적으로 살펴보고 각 컨테이너에 마운트 합니다.
function mount(vnode, container) { ... }
// 이 후 나오는 코드들은 모드 이 mount안에 쓰여진 코드들입니다.
1. DOM 엘리먼트 생성
const element = (vnode.element = document.createElement(vnode, tag))
vnode.element이 무엇인지 궁금해하실 수 있습니다. vnode.element는 어떤 element가 vnode의 부모인 줄 알 수 있도록 내부적으로 설정하는 프로퍼티입니다.
2. props 객체로부터 attributes 설정
Object.entries(vnode.props || {}).forEach([key, value] => {
element.setAttribute(key, value)
})
3. children 마운트
2가지의 경우의 수가 있습니다.
children
이 단순 text인 겨우children
이 vnode 배열인 경우
if (typeof vnode.children === 'string') {
element.textContent = vnode.children
} else {
vnode.children.forEach(child => {
mount(child, elemment) // 재귀적으로 children을 마운트합니다
})
}
4. DOM에 추가하기
container.appendChild(element)
최종 코드
function mount(vnode, container) {
const element = (vnode.element = document.createElement(vnode.tag))
Object.entries(vnode.props || {}).forEach([key, value]) => {
element.setAttribute(key, value)
})
if (typeof vnode.children === 'string') {
element.textContent = vnode.children
} else {
vnode.children.forEach(child => {
mount(child, element)
})
}
container.appendChild(element)
}
vnode 언마운팅
언마우팅은 DOM에서 간단히 element를 제거하는 것입니다.
function unmount(vnode) {
vnode.element.parentNode.removeChild(vnode.element)
}
vnode 패칭
이 부분은 상대적으로 우리가 작성해야 하는 코드들 중 가장 복잡한 부분입니다. 기본적으로 두 vnodes을 비교하여 변경분을 찾아내어 반영해야 합니다.
이번에는 아래 코드의 이해를 돕기 위해 주석을 달아놓았습니다.
function patch(VNode1, VNode2) {
// 부모 DOM 엘리먼트를 할당합니다.
const element = (VNode2.element = VNode1.element)
// 이제 두 vnode의 차이점을 체크합니다.
// 만약 노드들의 태그가 다르다면, 전체 내용이 변경되었다고 가정합니다.
if (VNode1.tag !== VNode2.tag) {
// 기존 노드를 언마운트하고 새로운 노드를 마운트 합니다.
mount(VNode2. element.parentNode)
unmount(VNode1)
} else {
// 노드가 같은 태그일 때, 두 가지 비교가 남아있습니다.
// - Props
// - Children
// props 체크는 현재 하지않겠습니다. 핵심에서 벗어난 내용이라 다음에 글을 쓴다면 해당 내용을 포함시키도록 하겠습니다.
// Checking the children
// 새로운 노드의 children이 string인 경우
if (typeof VNode2.children == "string") {
// children이 **엄격히** 다른 경우
if (VNode2.children !== VNode1.children) {
element.textContent = VNode2.children;
} else {
// 만일 새로운 노드가 children 배열인 경우
// - children 배열의 크기가 같은 경우
// - 기존의 노드가 새로운 노드보다 children 배열의 크기가 더 큰 경우
// - 새로운 노드가 기존의 노드보다 children 배열의 크기가 더 큰 경우
// 배열의 크기 찾기
const children1 = VNode1.children;
const children2 = VNode2.children;
const commonLen = Math.min(children1.length, children2.length);
// 재귀적으로 공통 children 패칭하기
for (let i = 0; i < commonLen; i++) {
patch(children1[i], children2[i])
}
// 새로운 노드가 더 적은 children을 가질 때,
if (chidren1.length > children2.length) {
children1.slice(children.length).forEach(child => {
unmount(child)
})
}
// 새로운 노드가 더 많은 children을 가질 때,
if(children2.length > children1.length) {
children2.slice(children1.length).forEach(child => {
mount(child, element)
})
}
}
}
}
완성되었습니다. vdom의 기본적인 부분을 구현했습니다. props 체킹과 최적화와 같은 부분들이 더 남아있지만 이로써 vdom의 기본적인 개념을 이해할 수 있을거라 생각합니다.
다시, generateList
예제로 돌아가봅시다. vdom 구현을 마쳤으니, 아래와 같이 수정할 수 있습니다.
function generateList(list) {
let children = list.map(child => createVNode("li", null, child));
return createVNode("ul", { class: 'fruits-ul' }, children)
}
mount(generateList(["apple", "banana", "oragne"], document.querySelector("#app"))
'개발 > Frontend' 카테고리의 다른 글
[번역] create-react-app 없이 리액트 프로젝트 생성 및 설정 완벽 가이드 (0) | 2021.06.13 |
---|