분류 javascript

가상 DOM 이해

컨텐츠 정보

  • 조회 271 (작성일 )

본문

React의 인기, 가상 DOM의 내부 및 구현은 기술 커뮤니티 및 인터뷰에서 가장 많이 논의 된 주제가 되었습니다. 

이 게시물에서는 Virtual DOM에 대해 소개하고 간단한 Virtual DOM 논리를 구현하는 방법을 설명합니다.


https://www.pixelstech.net/article/1575773786-Understand-Virtual-DOM 


가상 DOM을 이해하는 방법 


초기에는 프론트 엔드 개발자가 데이터 상태 변경 (일반적으로 AJAX 호출 후)을 기반으로 웹 페이지 보기를 업데이트했습니다. 그러나 업데이트가 빈번한 경우 페이지 리플 로우 및 다시 그리기를 유발하여 페이지가 멈출 수 있으므로 성능이 저하됩니다.


따라서 사람들은 전체 노드 트리를 업데이트하는 대신 실제로 업데이트 되는 노드를 업데이트하는 솔루션을 제공합니다. 이 프로세스는 기존 노드 트리와 업데이트 된 노드 트리를 비교하여 차이점을 찾은 다음 업데이트를 수행합니다. 

실제 DOM 표현을 사용하는 대신 사람들은 JS 객체를 사용하여 노드를 나타내며 이러한 종류의 비교를 수행합니다. 이것은 가상 DOM 개념이 그림에 나오는 것입니다. DOM과 JS 사이의 계층입니다.


가상 DOM을 나타내는 방법 


가상 DOM은 JS 객체로 표현 될 수 있습니다. ES6에서 class는 Virtual DOM 구조를 나타내는 데 사용될 수 있습니다. 기본 가상 DOM에는 tagName, 속성 및 하위 노드가 있어야 합니다.

class Element {
  constructor(tagName, props, children) {
    this.tagName = tagName;
    this.props = props;
    this.children = children;
  }
}


호출을 단순화하기 위해 매번 new를 호출하는 대신 새로운 Virtual DOM 객체를 반환하도록 도우미 함수를 만들 수 있습니다.


function el(tagName, props, children) {
  return new Element(tagName, props, children);
}


이제 DOM 트리 아래에 있다고 가정하십시오.


<div class="test">
  <span>span1</span>
</div>


가상 DOM에서 다음과 같이 나타낼 수 있습니다.


const span = el("span", {}, ["span1"]);
const div = el("div", { class: "test" }, [span]);


나중에 Virtual DOM의 차이점을 비교할 때 실제 DOM을 업데이트 해야 하므로 렌더링 함수도 정의해야 합니다.


class Element {
  constructor(tagName, props, children) {
    this.tagName = tagName;
    this.props = props;
    this.children = children;
  }

  render() {
    const dom = document.createElement(this.tagName);
    // update attributes
    Reflect.ownKeys(this.props).forEach(name =>
      dom.setAttribute(name, this.props[name])
    );

    // recusive update child nodes
    this.children.forEach(child => {
      const childDom =
        child instanceof Element
          ? child.render()
          : document.createTextNode(child);
      dom.appendChild(childDom);
    });

    return dom;
  }
}


가상 DOM 트리를 비교하고 업데이트하는 방법 


이제 Virtual DOM과 그 사용법이 소개되었습니다. 가상 DOM 트리에서 DOM을 비교하고 업데이트하는 방법에 대해 설명합니다. 여기에는 기본적으로 가상 DOM 트리에서 추가, 삭제 및 업데이트 작업이 포함됩니다.


이 과정에는 기본적으로 두 단계가 포함됩니다.


  • diff : 지정된 인덱스에서 두 개의 가상 DOM 트리에서 가상 DOM의 차이점을 재귀 적으로 비교
  • 패치 : 가상 DOM 트리의 차이를 기반으로 업데이트 수행

이를 위해 두 가지 방법이 있습니다.

  1. 전체 가상 DOM 트리에서 diff를 한 번 실행 한 다음 모두에 대해 한 번 업데이트를 수행하십시오.
  2. 비교 중에 Virtual DOM에 차이가 있으면 업데이트를 수행하십시오.

비교 및 업데이트를 수행하는 두 번째 방법은 다음과 같습니다.


먼저 diff와 patch의 논리를 updateEl라는 함수에 넣습니다. 기본 정의는


/**
 *
 * @param {HTMLElement} $parent
 * @param {Element} newNode
 * @param {Element} oldNode
 * @param {Number} index
 */
function updateEl($parent, newNode, oldNode, index = 0) {
  // ...
}

$로 시작하는 모든 변수는 페이지의 실제 DOM 요소입니다. index는 자식 노드 $parent 요소에서 oldNode의 위치를 ​​나타냅니다.


새 노드 추가 


oldNode가 정의되어 있지 않으면 newNode가 새로 추가 된 노드이므로 DOM 요소에 추가하면 됩니다.


function updateEl($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(newNode.render());
  }
}


11.gif 


노드 삭제 


newNode가 정의되지 않은 경우 DOM 트리의 해당 색인에 노드가 없음을 의미하므로 DOM 트리에서 노드를 제거하십시오.


function updateEl($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(newNode.render());
  } else if (!newNode) {
    $parent.removeChild($parent.childNodes[index]);
  }
}


노드 업데이트 


oldNode를 newNode와 비교하는 동안 세 가지 경우가 업데이트로 간주됩니다.

  • 노드 변경 유형, 텍스트를 가상 DOM으로 또는 가상 DOM을 텍스트로 변경
  • 두 노드 모두 텍스트 노드이지만 내용이 변경됩니다.
  • 노드의 속성이 변경됨


이 세 가지 경우를 나타 내기 위해 세 개의 기호를 정의 해 봅시다.


const CHANGE_TYPE_TEXT = Symbol("text");
const CHANGE_TYPE_PROP = Symbol("props");
const CHANGE_TYPE_REPLACE = Symbol("replace");


속성 변경을 위해서는 속성을 대체하는 기능을 정의해야 합니다.

function replaceAttribute($node, removedAttrs, newAttrs) {
  if (!$node) {
    return;
  }

  Reflect.ownKeys(removedAttrs).forEach(attr => $node.removeAttribute(attr));
  Reflect.ownKeys(newAttrs).forEach(attr =>
    $node.setAttribute(attr, newAttrs[attr])
  );
}


2.gif 


그리고 변경에 대한 사례를 확인하기 위해 다른 기능이 필요합니다.


function checkChangeType(newNode, oldNode) {
  if (
    typeof newNode !== typeof oldNode ||
    newNode.tagName !== oldNode.tagName
  ) {
    return CHANGE_TYPE_REPLACE;
  }

  if (typeof newNode === "string") {
    if (newNode !== oldNode) {
      return CHANGE_TYPE_TEXT;
    }
    return;
  }

  const propsChanged = Reflect.ownKeys(newNode.props).reduce(
    (prev, name) => prev || oldNode.props[name] !== newNode.props[name],
    false
  );

  if (propsChanged) {
    return CHANGE_TYPE_PROP;
  }
  return;
}


updateEl에서는 checkChangeType의 반환 값을 기반으로 해당 업데이트를 수행하며, 반환 값이 비어 있으면 변경할 필요가 없습니다.


function updateEl($parent, newNode, oldNode, index = 0) {
  let changeType = null;

  if (!oldNode) {
    $parent.appendChild(newNode.render());
  } else if (!newNode) {
    $parent.removeChild($parent.childNodes[index]);
  } else if ((changeType = checkChangeType(newNode, oldNode))) {
    if (changeType === CHANGE_TYPE_TEXT) {
      $parent.replaceChild(
        document.createTextNode(newNode),
        $parent.childNodes[index]
      );
    } else if (changeType === CHANGE_TYPE_REPLACE) {
      $parent.replaceChild(newNode.render(), $parent.childNodes[index]);
    } else if (changeType === CHANGE_TYPE_PROP) {
      replaceAttribute($parent.childNodes[index], oldNode.props, newNode.props);
    }
  }
}


재귀 diff 및 패치 


위의 세 가지 경우 중 어느 것도 발생하지 않으면 현재 Virtual DOM에 업데이트가 없음을 의미하므로 이제 해당 하위로 이동하고 동일한 비교 및 ​​업데이트를 수행해야 합니다.


function updateEl($parent, newNode, oldNode, index = 0) {
  let changeType = null;

  if (!oldNode) {
    $parent.appendChild(newNode.render());
  } else if (!newNode) {
    $parent.removeChild($parent.childNodes[index]);
  } else if ((changeType = checkChangeType(newNode, oldNode))) {
    if (changeType === CHANGE_TYPE_TEXT) {
      $parent.replaceChild(
        document.createTextNode(newNode),
        $parent.childNodes[index]
      );
    } else if (changeType === CHANGE_TYPE_REPLACE) {
      $parent.replaceChild(newNode.render(), $parent.childNodes[index]);
    } else if (changeType === CHANGE_TYPE_PROP) {
      replaceAttribute($parent.childNodes[index], oldNode.props, newNode.props);
    }
  } else if (newNode.tagName) {
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    for (let i = 0; i < newLength || i < oldLength; ++i) {
      updateEl(
        $parent.childNodes[index],
        newNode.children[i],
        oldNode.children[i],
        i
      );
    }
  }
}


가상 DOM에 대한 실제 코드 구현은 이러한 성숙한 프레임 워크에서 훨씬 더 복잡합니다. 위의 내용은 Virtual DOM의 작동 방식에 대한 간단한 설명이며 Virtual DOM에 대한 일반적인 아이디어를 제공해야 합니다.