[번역] How to write your own Virtual DOM

[번역] How to write your own Virtual DOM

How to write your own Virtual DOM | by deathmood | Medium

나만의 가상 DOM을 구축하기 위해 알아야 할 두 가지 사항이 있습니다. React 소스나 다른 Virtual DOM 구현들의 소스에 딥 다이브 할 필요는 없습니다. 그것들은 너무 크고 복잡합니다. 사실 Virtual DOM의 주요 부분은 50 줄 이하의 코드만으로도 작성할 수 있습니다. 바로 50. 줄. 의. 코드로!!!

여기 두 가지 개념이 있습니다.

  • 가상 DOM은 실제 DOM을 표현한 것이다
  • 가상 DOM 트리에서 무언가를 변경하면 새로운 가상 트리가 생성됩니다. 알고리즘은 이 두 트리(이전 트리와 새 트리)를 비교하고 차이점을 찾은 다음 실제 DOM에 꼭 필요한 작은 변경 사항만 적용하여 가상 DOM을 반영합니다.

이게 전부입니다! 이러한 각 개념에 대해 자세히 알아보겠습니다.

업데이트 : 가상 DOM에서 props 및 이벤트를 설정하는 방법에 대한 두 번째 기사는 여기에 있습니다.

DOM 트리 표현

음, 먼저 DOM 트리를 메모리에 저장해야 합니다. 그리고 우리는 평범하고 오래된 JS 객체를 사용하여 그렇게 할 수 있습니다. 다음 트리가 있다고 가정해 보겠습니다.

<ul class=”list”>
  <li>item 1</li>
  <li>item 2</li>
</ul>

꽤 단순해 보이죠? JS 객체만으로 어떻게 이를 표현할 수 있을까요?

{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [
  { type: ‘li’, props: {}, children: [‘item 1’] },
  { type: ‘li’, props: {}, children: [‘item 2’] }
] }

여기에서 두 가지 사실을 알 수 있습니다.

  • 우리는 DOM 요소를 다음과 같은 객체로 표현합니다.
{ type: ‘…’, props: { … }, children: [ … ] }
  • 일반 JS 문자열로 DOM 텍스트 노드를 나타냅니다.

그러나 그런 식으로 큰 트리를 쓰는 것은 매우 어렵습니다. 이제 구조를 더 쉽게 이해할 수 있도록 도우미 함수를 작성해 보겠습니다.

function h(type, props, …children) {
  return { type, props, children };
}

이제 다음과 같이 DOM 트리를 작성할 수 있습니다.

h(‘ul’, { ‘class’: ‘list’ },
  h(‘li’, {}, ‘item 1’),
  h(‘li’, {}, ‘item 2’),
);

훨씬 더 깔끔해 보이죠? 하지만 우리는 더 나아갈 수 있습니다. JSX에 대해 들어보셨죠? 네, 저도 여기에 작성하고 싶습니다. 그럼 어떻게 하면 되나요?

여기에서 공식 Babel JSX 문서를 읽으면 Babel이 다음 코드를 트랜스파일한다는 것을 알 수 있습니다.

<ul className="list”>
  <li>item 1</li>
  <li>item 2</li>
</ul>

다음과 같이 smth에 들어갑니다.

React.createElement(‘ul’, { className: ‘list’ },
  React.createElement(‘li’, {}, ‘item 1’),
  React.createElement(‘li’, {}, ‘item 2’),
);

유사점을 발견하셨나요? 예, 예.. 만약 우리가 React.createElement(…)’ 를 h(…)’ 호출 로 대체할 수 있다면… jsx pragma 라는 smth를 사용하여 그렇게 할 수 있다는 것이 밝혀졌습니다 . 소스 파일 상단에 주석과 같은 줄을 포함하기만 하면 됩니다.

/** @jsx h */
<ul className=”list”>
  <li>item 1</li>
  <li>item 2</li>
</ul>

글쎄요, 실제로 Babel에게 ‘이봐, jsx를 트랜스파일하고 React.createElement 대신 h ‘를 입력하라고 지시합니다 . 거기에는 ‘h’ 대신 무엇이든 넣을 수 있습니다. 그리고 그것은 트랜스파일될 것입니다.

따라서 제가 이전에 말한 내용을 요약하면 다음과 같은 방식으로 DOM을 작성하겠습니다.

/** @jsx h */
const a = (
  <ul className=”list”>
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

그리고 이는 Babel에 의해 다음 코드로 변환됩니다.

const a = (
  h(‘ul’, { className: ‘list’ },
    h(‘li’, {}, ‘item 1’),
    h(‘li’, {}, ‘item 2’),
  );
);

`h` 함수가 실행되면 일반 JS 객체(가상 DOM 표현)가 반환됩니다.

const a = (
  { type: ‘ul’, props: { className: ‘list’ }, children: [
    { type: ‘li’, props: {}, children: [‘item 1’] },
    { type: ‘li’, props: {}, children: [‘item 2’] }
  ] }
);

계속해서 JSFiddle에서 시도해 보세요(Babel을 언어로 설정하는 것을 잊지 마세요).

/** @jsx h */

function h(type, props, ...children) {
  return { type, props, children };
}

const a = (
  <ul class="list">
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

console.log(a);

DOM 표현 적용하기

자, 이제 우리는 자체 구조를 가진 일반 JS 객체로 표현된 DOM 트리를 갖게 되었습니다. 멋지지만 어떻게든 그것으로부터 실제 DOM을 생성해야 합니다. 왜냐하면 우리의 표현을 DOM에 추가할 수는 없기 때문입니다.

먼저 몇 가지 가정을 하고 용어를 설정해 보겠습니다.

  • ‘$’로 시작하는 실제 DOM 노드(요소, 텍스트 노드)를 사용하여 모든 변수를 작성할 것입니다. 따라서 $parent는 실제 DOM 요소가 됩니다.
  • 가상 DOM 표현은 node라는 변수에 있습니다.
  • React에서와 마찬가지로 하나의 루트 노드만 가질 수 있으며 다른 모든 노드는 내부에 있습니다.

좋습니다, 그렇다면 가상 DOM 노드를 가져와 실제 DOM 노드를 반환하는 createElement(…) 함수를 작성해 보겠습니다. 지금은 ‘props’와 ‘children’을 잊어버리세요. 나중에 설정하겠습니다.

function createElement(node) {
  if (typeof node === ‘string’) {
    return document.createTextNode(node);
  }
  return document.createElement(node.type);
}

따라서 일반 JS 문자열과 요소 텍스트 노드를 모두 가질 수 있기 때문에 다음과 같은 유형의 JS 객체입니다.

{ type: ‘…’, props: { … }, children: [ … ] }

따라서 여기에 virtual text 노드와 virtual element 노드를 모두 전달할 수 있습니다 — 그리고 그것은 작동할 것입니다.

이제 자식 요소에 대해 생각해 봅시다 — 각각은 텍스트 노드 또는 요소이기도 합니다. 따라서 createElement(…)  함수를 사용하여 생성할 수도 있습니다 . 네, 느껴지시나요? 재귀적인 느낌이 듭니다 :)) 따라서 각 요소의 하위 요소에 대해createElement(…) 를호출할 수 있고, 다음과 같이 요소에 appendChild()을 할 수 있습니다.

function createElement(node) {
  if (typeof node === ‘string’) {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

와우, 멋지군요. 지금은 노드 props 를 제쳐 두겠습니다. 우리는 나중에 그들에 대해 이야기 할 것입니다. 가상 DOM의 기본 개념을 이해하는 데 필요하지는 않고, 복잡성 더할 것입니다.

이제 JSFiddle에서 시도해 보세요.

/** @jsx h */

function h(type, props, ...children) {
  return { type, props, children };
}

function createElement(node) {
  if (typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

const a = (
  <ul class="list">
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

const $root = document.getElementById('root');
$root.appendChild(createElement(a));

변경사항 처리

자, 이제 가상 DOM을 실제 DOM으로 바꿀 수 있으므로 가상 트리를 비교하는 방법을 생각해 볼 차례입니다. 따라서 기본적으로 우리는 두 개의 가상 트리(오래된 트리와 새 트리)를 비교하고 실제 DOM에 필요한 변경 사항만 적용하는 알고리즘을 작성해야 합니다.

트리를 비교하는 방법? 음, 다음 사례를 처리해야 합니다.

  • 어떤 위치에는 오래된 노드가 없습니다. 그래서 노드가 추가되었고 우리는 그 노드에 appendChild(…) 야합니다.
  • 어떤 위치에는 새 노드가 없습니다. 따라서 노드가 삭제되었으므로 removeChild(…) 해야합니다.
  • 해당 위치에 다른 노드가 있습니다. 따라서 노드가 변경되었으므로  replaceChild(…) 해야합니다.
  • 노드는 동일하므로 더 깊이 들어가 하위 노드를 비교해야 합니다.

자, $parent , newNode 및 oldNode 라는 세 가지 매개변수를 취하는 updateElement(…) 라는 함수를 작성해 보겠습니다 . 여기서 $parent 는 가상 노드의 실제 DOM 요소 부모입니다. 이제 위에서 설명한 모든 경우를 처리하는 방법을 살펴보겠습니다.

오래된 노드가 없는 경우

음, 이 것은 매우 간단하므로 언급하지 않겠습니다.

function updateElement($parent, newNode, oldNode) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  }
}

새 노드가 없는 경우

여기에 문제가 있습니다. 새 가상 트리의 현재 위치에 노드가 없으면 실제 DOM에서 노드를 제거해야 합니다. 그런데 어떻게 해야 할까요? 예, 우리는 부모 요소(함수에 전달됨)를 알고 있으므로 $parent.removeChild(…)를 호출 하고 거기에 실제 DOM 요소 참조를 전달해야 합니다. 그러나 우리에게는 그런 것이 없습니다. 음, 부모 노드의 위치를 ​​알고 있다면 $parent.childNodes[index]를 사용하여 참조를 얻을 수 있습니다. 여기서 index는 부모 요소에서 노드의 위치입니다.

자, 이 인덱스가 함수에 전달된다고 가정해 봅시다(그리고 실제로 전달될 것입니다. 나중에 보게 될 것입니다). 따라서 코드는 다음과 같습니다.

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

다른 노드가 있는 경우

먼저 두 노드(이전 노드와 새 노드)를 비교하고 노드가 실제로 변경되었는지 알려주는 함수를 작성해야 합니다. 우리는 그것이 요소와 텍스트 노드 둘 다 될 수 있다는 것을 고려해야합니다.

function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
         typeof node1 === ‘string’ && node1 !== node2 ||
         node1.type !== node2.type
}

이제 부모에 현재 노드의 인덱스가 있으면 새로 생성 된 노드로 쉽게 바꿀 수 있습니다.

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  }
}

하위 노드를 비교해야 하는 경우

그리고 마지막으로, 두 노드의 모든 자식을 살펴보고 비교해야 합니다 — 실제로 각각에 대해 updateElement(…) 를 호출합니다. 예, 다시 재귀입니다.

그러나 코드를 작성하기 전에 고려해야 할 몇 가지 사항이 있습니다.

  • node가 요소인 경우에만 자식을 비교해야 합니다(텍스트 노드는 자식을 가질 수 없음).
  • 이제 현재 노드에 대한 참조를 부모로 전달합니다.
  • 우리는 모든 아이들을 하나씩 비교해야 합니다 — 어느 시점에서 우리가 ‘undefined’를 갖게 되더라도 — 괜찮습니다 — 우리의 함수는 그것을 처리할 수 있습니다
  • 그리고 마지막으로 index – ‘children’배열에있는 자식 노드의 인덱스입니다.
function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  } else if (newNode.type) {
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    for (let i = 0; i < newLength || i < oldLength; i++) {
      updateElement(
        $parent.childNodes[index],
        newNode.children[i],
        oldNode.children[i],
        i
      );
    }
  }
}

모두 합치기

네, 바로 이것입니다. 모든 코드를 JSFiddle에 넣었고 구현 부분은 약속 한대로 실제로 50 줄의 코드가 필요했습니다. 계속해서 이것을 가지고 즐겨보세요.

/** @jsx h */

function h(type, props, ...children) {
  return { type, props, children };
}

function createElement(node) {
  if (typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
         typeof node1 === 'string' && node1 !== node2 ||
         node1.type !== node2.type
}

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  } else if (newNode.type) {
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    for (let i = 0; i < newLength || i < oldLength; i++) {
      updateElement(
        $parent.childNodes[index],
        newNode.children[i],
        oldNode.children[i],
        i
      );
    }
  }
}

// ---------------------------------------------------------------------

const a = (
  <ul>
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

const b = (
  <ul>
    <li>item 1</li>
    <li>hello!</li>
  </ul>
);

const $root = document.getElementById('root');
const $reload = document.getElementById('reload');

updateElement($root, a);
$reload.addEventListener('click', () => {
  updateElement($root, b, a);
});

개발자 도구를 열고 ‘새로고침’ 버튼을 누를 때 적용된 변경 사항을 확인합니다.

결론

축하합니다! 우리는 그렇게 했습니다. 가상 DOM 구현을 작성했습니다. 그리고 그것은 작동합니다. 이 글을 읽으신 분들이 Virtual DOM이 어떻게 작동해야 하는지, React가 어떻게 작동하는지에 대한 기본 개념을 이해하셨기를 바랍니다.

그러나 여기에서 강조되지 않은 몇 가지 사항이 있습니다 (향후 아티클에서 다루려고 노력할 것입니다).

  • 요소 속성(props) 설정 및 비교/업데이트
  • 이벤트 다루기 — 요소에 이벤트 리스너 추가
  • Virtual DOM이 React와 같은 구성 요소와 함께 작동하도록 만들기
  • 실제 DOM 노드에 대한 참조 가져오기
  • 실제 DOM을 직접 변경하는 라이브러리와 함께 가상 DOM 사용 — jQuery 및 해당 플러그인
  • 그리고 더…

추신

코드나 기사에 오류가 있거나 이 코드에 대해 수행할 수 있는 최적화가 있는 경우 주석으로 자유롭게 표현하십시오 :)) 그리고 내 영어 실력 🙂 죄송합니다

업데이트 : 가상 DOM에서 props 및 이벤트를 설정하는 방법에 대한 두 번째 기사는 여기에 있습니다.

Comments

No comments yet. Why don’t you start the discussion?

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다