UI의 이상적인 또는 "가상"적인 표현을 메모리에 저장하고 ReactDOM과 같은 라이브러리에 의해 "실제" DOM과 동기화하는 프로그래밍 개념!
문서객체모델(The Document Object Model)은 HTML, XML 문서의 프로그래밍 인터페이스다.
구조화된 표현을 제공하며 프로그래밍 언어가 돔 구조에 접근할 수 있는 방법을 제공하여 문서 구조, 스타일, 내용 등을 변경할 수 있게 돕는다.
nodes와 objects로 문서를 표현하여 웹 페이지를 스크립트 또는 프로그래밍 언에들에서 사용할 수 있게 연결시켜주는 역할을 한다.
DOM은 문서를 논리 트리로 표현한다. 트리의 각 브랜치는 노드에서 끝나며, 각 노드는 객체를 갖는다.
DOM 메서드를 사용하면 프로그래밍적으로 트리에 접근할 수 있다.
노드는 이벤트 처리기도 포함할 수 있다. 이벤트가 발생한 순간, 해당 이벤트와 연결한 처리기가 발동하게 된다.
실제 돔을 곧바로 조작하지 않고 가상 돔의 객체를 만들고 메모리에 저장합니다. 일부분 변경 요청이 들어오면 먼저 바닐라로 만들때는 이전것은 삭제하고 새로 만들어버리지만, 가상 돔의 경우에는 반드시 비교과정을 통해 다른 부분을 찾아 교체하여 실제로는 일부분만 랜더링을 하게 됩니다.
먼저 폴더 구조 사진을 보여드리겠습니다.
다음은 블로그 코드 전문입니다.
//main.js
import createElement from "./vdom/createElement.js";
import render from "./vdom/render.js";
import mount from "./vdom/mount.js";
import diff from "./vdom/diff.js";
const createVApp = (count) =>
createElement("div", {
attrs: {
id: "app",
dataCount: count, // we use the count here
},
children: [
"The current count is: ",
String(count), // and here
createElement("img", {
attrs: {
src: "https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif",
},
}),
],
});
let count = 0;
const vApp = createVApp(count);
const $app = render(vApp);
let $rootEl = mount($app, document.getElementById("app"));
//createElement.js
// "가상 요소"를 반환
export default (tagName, { attrs, children }) => {
// 프로토타입을 상속받지 않는 순수 오브젝트를 생성하게 만들어 준다!
const vElem = Object.create(null);
console.log("children :", children);
Object.assign(vElem, {
tagName,
attrs,
children,
});
return vElem;
};
// render.js
const renderElem = ({ tagName, attrs, children }) => {
// HTML 요소 생성
const $el = document.createElement(tagName);
// 어트리튜트 for문 돌면서 실제 요소에 setAttribute 메서드로 등록
for (const [k, v] of Object.entries(attrs)) {
$el.setAttribute(k, v);
}
// 가상의 자식들도 실제 돔으로 요소까지 넣어주기
if (children) {
for (const child of children) {
$el.appendChild(render(child));
}
}
return $el;
};
const render = (vNode) => {
if (typeof vNode === "string") {
return document.createTextNode(vNode);
}
return renderElem(vNode);
};
export default render;
// mount.js
// 만든 html, 부모 hmtl
export default ($node, $target) => {
$target.replaceWith($node);
return $node;
};
// diff.js
import render from "./render.js";
const zip = (xs, ys) => {
const zipped = [];
for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
zipped.push([xs[i], ys[i]]);
}
return zipped;
};
const diffAttrs = (oldAttrs, newAttrs) => {
const patches = [];
// setting newAttrs
for (const [k, v] of Object.entries(newAttrs)) {
patches.push(($node) => {
$node.setAttribute(k, v);
return $node;
});
}
// removing attrs
for (const k in oldAttrs) {
if (!(k in newAttrs)) {
patches.push(($node) => {
$node.removeAttribute(k);
return $node;
});
}
}
return ($node) => {
for (const patch of patches) {
patch($node);
}
return $node;
};
};
// children은 배열로 들어온다.
const diffChildren = (oldVChildren, newVChildren) => {
const childPatches = [];
// 기존 것만큼만 돈다. 기존것과 새로운 것 비교.
// 에러 : Uncaught TypeError: Cannot read properties of undefined (reading 'forEach')
oldVChildren.forEach((oldVChild, i) => {
childPatches.push(diff(oldVChild, newVChildren[i]));
});
const additionalPatches = [];
// 배열 메서드 slice, (시작, 끝 인덱스 미포함) 얕은 복사로 새로운 배열을 생성하여 준다. 인수가 하나면 시작 인덱스부터 끝까지
// 위에서는 비교를 위해 과거 인덱스 수도 돌리고 새 인덱스가 길다면 이후 새 인덱스 안돌린 것부터 시작!
for (const additionalVChild of newVChildren.slice(oldVChildren.length)) {
additionalPatches.push(($node) => {
$node.appendChild(render(newVChildren));
return $node;
});
}
return ($parent) => {
for (const [patch, $child] of zip(childPatches, $parent.childNodes)) {
patch($child);
}
for (const patch of additionalPatches) {
patch($parent);
}
return $parent;
};
};
// Diffing Algorithm
const diff = (oldVTree, newVTree) => {
// undefined 일 때
if (newVTree === undefined) {
return ($node) => {
$node.remove();
return undefined;
};
}
// 둘중의 하나가 스트링
if (typeof oldVTree === "string" || typeof newVTree === "string") {
// 하나만 스트링 => 굳이 비교할 것 없이 그냥 바로 새로 생성하기
if (oldVTree !== newVTree) {
return ($node) => {
const $newNode = render(newVTree);
$node.replaceWith($newNode);
return $newNode;
};
} else {
// 둘다 스트링임.
return ($node) => $node;
}
}
// 둘다 스트링 아님. 완전 다른 태그라면?
if (oldVTree.tagName !== newVTree.tagName) {
return ($node) => {
const $newNode = render(newVTree);
$node.replaceWith($newNode);
return $newNode;
};
}
// 다른 태그가 아닌거야! => 비교 시작!!!
const patchAttrs = diffAttrs(oldVTree.attrs, newVTree.attrs);
const patchChildren = diffChildren(oldVTree.children, newVTree.children);
return ($node) => {
patchAttrs($node);
patchChildren($node);
return $node;
};
};
export default diff;
위의 코드를 간략하게 설명해드리겠습니다.
가상돔은 돔을 조작해야할 일이 생기면 먼저 가상 돔을 만듭니다.
만든 가상돔과 기존의 가상돔을 서로 비교한 후에 필요헌 부분만 교체하는 방법입니다.
가상돔으로 만들게 되면 처음부터 다시 다 만들지 않아도 되기에 빠르다고 자연스럽게 생각할 수 있으나...
과연 해당 코드를 작성하면서 빠를지는 잘 모르겠습니다...
DOM : https://developer.mozilla.org/ko/docs/Web/API/Document_Object_Model/Introduction
Virtual DOM : https://ko.legacy.reactjs.org/docs/faq-internals.html
Virtual DOM 개념 :https://minemanemo.tistory.com/120
이미지 출처 : https://minemanemo.tistory.com/120
https://dev.to/ycmjason/building-a-simple-virtual-dom-from-scratch-3d05