DOM 구조를 본따 만든 Javascript 객체 (트리구조)
변경된 점만 확인하고 그 부분만 수정해서 한 번에 레이아웃 계산 → 성능 개선 (Diff 알고리즘)
저번 주차에서 JSX와 virtualDOM, 그리고 realDOM으로 옮기는 작업까지 했다.
하지만 변경된 점만 확인해서 그 부분만 수정하지 않고, 전체를 바꾸도록 구현했기 때문에, 이번 주차에서는 Diff 알고리즘을 구현해보았다.
저번 주차에서는 _render
함수에서 virtualDOM을 만드는 코드도 들어가 있어서, 따로 KreactDOM
폴더를 생성해서 분리해두었다.
virtualDOM 객체를 만드는 함수를 분리하여 createVirtualDOM
을 만들고, virtualDOM을 비교하는 로직(Diff 알고리즘)이 들어간 updateVirtualDOM
을 만들었다.
export function createVirtualDOM(element) {
const { type, props } = element;
if (type === 'TEXT_ELEMENT') return document.createTextNode(props.nodeValue);
if (type === 'FRAGMENT') {
const fragment = document.createDocumentFragment();
props.children.forEach(child => {
fragment.appendChild(createVirtualDOM(child));
});
return fragment;
}
const newElement = document.createElement(type);
Object.keys(props).forEach(prop => {
if (prop === 'ref' || prop === 'key' || prop === 'children') return;
if (prop === 'className') {
newElement.setAttribute('class', props[prop]);
return;
}
if (prop === 'style' && typeof props[prop] === 'object') {
const style = props[prop];
Object.keys(style).forEach(styleName => {
newElement.style[styleName] = style[styleName];
});
return;
}
if (prop.startsWith('on')) {
const eventName = prop.substring(2).toLowerCase();
newElement.addEventListener(eventName, props[prop]);
return;
}
const newAttribute = document.createAttribute(prop);
newAttribute.value = props[prop];
newElement.setAttributeNode(newAttribute);
});
props.children.forEach(child => {
newElement.appendChild(createVirtualDOM(child));
});
return newElement;
};
export function updateVirtualDOM(root, oldNode, newNode, index = 0) {
console.log('updateVirtualDOM', root, oldNode, newNode)
if (!oldNode) return root.appendChild(createVirtualDOM(newNode));
if (!newNode) return root.removeChild(root.childNodes[index]);
if (oldNode.type !== newNode.type) return root.replaceChild(createVirtualDOM(newNode), root.childNodes[index]);
if (oldNode.type === 'TEXT_ELEMENT' && newNode.type === 'TEXT_ELEMENT') {
console.log(root, oldNode, newNode)
if (oldNode.props.nodeValue !== newNode.props.nodeValue) {
console.log('텍스트 업데이트');
const newElement = createVirtualDOM(newNode);
return root.replaceChild(newElement, root.childNodes[index]);
}
}
if (oldNode.type === 'FRAGMENT') {
const oldChildren = oldNode.props.children;
const newChildren = newNode.props.children;
const max = Math.max(oldChildren.length, newChildren.length);
for (let i = 0; i < max; i++) {
updateVirtualDOM(root, oldChildren[i], newChildren[i], i);
}
return root;
}
const oldProps = oldNode.props;
const newProps = newNode.props;
if (oldNode.type !== 'TEXT_ELEMENT') {
Object.keys(newProps).forEach(prop => {
if (prop === 'ref' || prop === 'key' || prop === 'children') return;
if (prop === 'className') {
root.childNodes[index].setAttribute('class', newProps[prop]);
return;
}
if (prop === 'style' && typeof newProps[prop] === 'object') {
const style = newProps[prop];
Object.keys(style).forEach(styleName => {
root.childNodes[index].style[styleName] = style[styleName];
});
return;
}
if (prop.startsWith('on')) {
const eventName = prop.substring(2).toLowerCase();
root.childNodes[index].removeEventListener(eventName, oldProps[prop]);
root.childNodes[index].addEventListener(eventName, newProps[prop]);
return;
}
const newAttribute = document.createAttribute(prop);
newAttribute.value = newProps[prop];
root.childNodes[index].setAttributeNode(newAttribute);
});
}
const oldChildren = oldNode.props.children;
const newChildren = newNode.props.children;
const max = Math.max(oldChildren.length, newChildren.length);
for (let i = 0; i < max; i++) {
console.log(`for loop${i}`, i, root, oldChildren[i], newChildren[i])
updateVirtualDOM(root.childNodes[index], oldChildren[i], newChildren[i], i);
}
return root;
}
리팩토링 해야할 부분이 많이 보이지만, 일단 이것에 맞춰 렌더링 시 로직도 수정했다.
function _render() {
console.log('렌더링')
_newNode = _rootComponent();
updateVirtualDOM(_root, _oldNode, _newNode);
_oldNode = _newNode;
}
확실히 _render
함수의 길이가 확 줄은 것을 볼 수 있다.