이전 작업에서 JS에서도 jsx를 사용할 수 있게함으로써 기존의 코드를 가독성 좋게 변경하고 유지/보수를 용이하게 할 수 있었다. 하지만 상태가 변경될때마다 매번 모든 Node를 새로 생성하기 때문에 불필요한 렌더링을 하고 있는 문제점이 있었다.
그래서 이번에는 성능을 조금이라도 개선시키고자 React의 Reconcilliation 과정과 비슷하게 가상DOM을 활용하여 필요한 부분만을 변경할 수 있게 해보려고 한다.
먼저 다음과 같은 코드가 있다고 가정해보면 이전에 만들었던 jsx를 바탕으로 화면에 잘 렌더링 되는 것을 볼 수 있다.
이제 여기서 우리는 책추가 버튼을 클릭하여 상태를 업데이트하면 모든 노드를 새롭게 생성하는 방식이 아닌 변경이 필요한 부분만을 업데이트하는 과정을 구현해야한다.
먼저, React와 동일하게 루트 엘리먼트를 시작으로 상태 업데이트가 되면 가상DOM을 생성하여 현재DOM과 비교하는 함수를 호출했다.
// core/Dj/Component.ts
setState(newState: any){
this.state = newState;
const vDOM = this.render();
const realDOM = this._DOM;
nodeCompare(vDOM, realDOM.parentNode, realDOM);
}
nodeCompare 함수는 새로운 가상DOM인 vDOM, 현재DOM의 부모노드인 container, 현재DOM을 나타내는 realDOM 그리고 부모노드의 몇번째 자식인지를 판별하기 위한 idx를 매개변수로 받는 함수로 이후의 동작은 가상DOM의 타입에 따라 달라지게 된다.
// core/Dj/diff.ts
function nodeCompare(vDOM: IDom, container: Node | null , realDOM?: INode , idx: number = 0){
const oldVDOM: IDom = realDOM && realDOM._vDOM;
// 여러가지 타입에 대한 처리가 필요
}
여기서 언급이 안된 oldVDOM은 vDOM과 비교하기 위해 이전의 vDOM 객체를 담고있는 변수이다.
vDOM: 실제 보여져야 할 노드들의 정보를 담고 있는 객체
realDOM: 화면에 렌더링되어 보여지고 있는 노드들
oldVDOM: 화면에 렌더링되어 보여지고 있는 노드의 vDOM(객체)
비교하고 있는 현재노드가 과거에는 없었던 노드라면 해당 노드를 생성하여 부모노드에 추가해준다.
if(vDOM && !oldVDOM){
return vDomToNode(vDOM, container);
}
비교하고 있는 현재노드는 없지만 과거에는 있었던 노드라면 해당 노드를 삭제해준다.
if(!vDOM && oldVDOM){
return container.removeChild(container.childNodes[idx]);
}
노드의 타입이 다른 경우에는 이전의 노드들을 버리고 완전히 새로운 노드를 생성하여 추가해준다.
if(vDOM.type !== oldVDOM.type) {
return container.replaceChild(createOriginNode(vDOM), container.childNodes[idx]);
}
해당 경우가 핵심이다. 다른 경우들이야 삭제하거나 새로 생성하는 과정이 있었지만 이 경우에는 기존의 노드를 변경해야 하는 과정이 필요하다.
우선, 크게 text 타입인 경우와 그렇지 않은 타입으로 나눌 수 있다.
text 타입인 경우에는 텍스트 내용만 변경해주면 된다.
나머지의 타입은 두 노드의 속성을 확인하여, 동일한 내역은 유지하고 변경된 속성들만 갱신해주면 된다.
if(vDOM.type === oldVDOM.type) {
// type이 text일 경우
if(vDOM.type === 'TEXT_NODE') {
const { textContent: newTextContent } = vDOM.attributes;
const { textContent: oldTextContent } = oldVDOM.attributes;
if(newTextContent === oldTextContent) return;
realDOM.textContent = newTextContent;
injectVDOMInToNode(realDOM, vDOM); // 해당 부분이 이전의 vDOM 객체를 저장하는 함수
return;
}
// 두 노드의 속성을 확인하여, 동일한 내역은 유지하고 변경된 속성들만 갱신
updateNode(container.childNodes[idx] as Element, vDOM, oldVDOM);
}
노드속성 업데이트 함수
export function updateNode(newNode: Element, vDOM: IDom, oldDOM?: IDom) { const newProps = vDOM.attributes || {}; const oldProps = oldDOM && oldDOM.attributes || {}; // 새로운 이벤트나 속성 처리 Object.entries(newProps).forEach(([key, value]) => { const newProp = newProps[key]; const oldProp = oldProps[key]; if(newProp === oldProp) return; if(!value) return; if(key.startsWith('on')){ const eventType = key.slice(2).toLowerCase(); newNode.addEventListener(eventType, value); if(oldProp) newNode.removeEventListener(eventType, oldProp); return; } newNode.setAttribute(key, value); }); // 기존 이벤트나 속성 삭제 처리 Object.entries(oldProps).forEach(([key, value]) => { const newProp = newProps[key]; if(newProp != null) return; if(key.startsWith('on')){ const eventType = key.slice(2).toLowerCase(); newNode.removeEventListener(eventType, value); return; } newNode.removeAttribute(key); }); injectVDOMInToNode(newNode, vDOM); } // 이후 비교를 위한 과거 노드의 정보를 가진 vDOM객체 삽입 export const injectVDOMInToNode = (node: INode, vDOM: IDom) => { node._vDOM = vDOM; // 가장 최상단의 Node(컴포넌트)일 경우 해당 노드를 저장 injectRealDOMToComponent(vDOM.DJ_COMPONENT, node); } export const injectRealDOMToComponent = (component: Component, realDOM: INode) => { if(component) component._DOM = realDOM; }
마지막으로 DOM 노드의 처리가 끝나면 해당 노드의 자식들을 재귀적으로 처리하여 위의 과정을 반복해주면 된다.
// DOM 노드의 처리가 끝나면 이어서 해당 노드의 자식들을 재귀적으로 처리
for(let i=0; i < getMaxLength(vDOM.children.length, realDOM.childNodes.length); i+=1){
const vDOMChild = vDOM.children[i];
const realDOMChild = realDOM.childNodes[i];
nodeCompare(vDOMChild, container.childNodes[idx], realDOMChild, i);
}
아직 구현하지 못한 부분이다.