virtualDOM?

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;
}

Untitled

확실히 _render 함수의 길이가 확 줄은 것을 볼 수 있다.

리팩토링