프레임워크 렌더링과 가상DOM

1. createElement의 역할과 동작 원리

1.1 createElement 함수의 역할

  • DOM 노드 생성 및 관리: 주어진 태그, 속성, 자식 요소들을 조합하여 새로운 DOM 노드 또는 가상 DOM(Virtual DOM) 노드를 생성.

  • 컴포넌트 구조화: 함수형 컴포넌트와 클래스형 컴포넌트와 같이 다양한 컴포넌트를 일관된 인터페이스로 제공.

1.2 createElement 함수의 동작 방식

  1. 노드 타입 확인divspan 같은 문자열은 기본 HTML 요소로, 함수나 클래스는 사용자 정의 컴포넌트로 인식.

  2. 속성(Props) 처리: 전달된 속성(props)들을 파싱하여 노드에 필요한 속성을 설정.

  3. 자식 노드 처리: 자식 요소들을 재귀적으로 처리하여 노드 트리를 구성.

  4. Virtual DOM 객체 반환: DOM 노드 그 자체를 반환하는 것이 아니라, 노드의 정보(타입, 속성, 자식)를 담은 Virtual DOM 객체를 반환.

1.3 createElement 함수 예시

createElement가 반환하는 형태?

const element = {
  type: 'div',
  props: {
    id: 'container',
    children: [
      { type: 'h1', props: { children: [{ type: 'TEXT_ELEMENT', props: { nodeValue: '안녕! 코드스쿼드' } }] } },
      { type: 'p', props: { children: [{ type: 'TEXT_ELEMENT', props: { nodeValue: '궁시렁 궁시렁' } }] } }
    ]
  }
};

<개념 코드>

function createElement(type, props, ...children) {
  // 주어진 타입과 속성, 자식 요소들을 기반으로 가상 DOM 객체를 생성.
}

function createTextElement(text) {
  // 텍스트 노드를 위한 가상 DOM 객체를 생성.
}

// 사용 예
const element = createElement(
  'div', 
  { id: 'container' }, 
  createElement('h1', null, 'Hello, World!'), 
  createElement('p', null, 'This is a paragraph.')
);

1.5 createElement 함수에 최적화가 필요할까? 🤔

  • 동일한 입력이 오면 또 동일한 V DOM 객체를 반환해야할까?

  • 절대 변경이 안되는(?) 노드도 가상DOM에 있어야 하나?

  • list 의 변화를 쉽게 파악하는 방법은?


2. Virtual DOM과 Diff 알고리즘

2.1 Virtual DOM 개요

실제 DOM을 직접 조작하는 대신, 가상의 DOM을 메모리에 유지하고 상태가 변경될 때마다 가상의 DOM 트리와 이전의 트리를 비교(Diffing)하여 변경된 부분만 실제 DOM에 반영.

전체 DOM을 다시 그리지 않고 필요한 부분만 효율적으로 업데이트.

2.2 Virtual DOM을 사용하는 이유

  • 먼저 리액트 프레임워크의 주요 목표? 🤔

  • DOM 조작의 비용

    • DOM 조작은 브라우저의 렌더링 엔진에서 많은 리소스를 소비함.

    • 예를 들어, 페이지 전체를 새로 그리는 데 많은 시간이 소요될 수 있음.

  • 효율적인 업데이트

    • 가상 DOM을 사용하면 변경된 부분만 찾아내어 업데이트하므로 불필요한 DOM 조작을 피하고 성능을 최적화할 수 있음.

2.3 Diff 알고리즘의 동작 원리

React의 Diff 알고리즘은 두 개의 Virtual DOM 트리 간의 변화를 찾는 과정에서 노드의 타입과 속성(Props)을 비교.

그 결과, React는 DOM을 효율적으로 갱신할 수 있음.

  1. 노드 타입 비교: 노드의 타입이 다르면, 해당 노드를 완전히 교체함. 예를 들어, <div>가 <span>으로 바뀌면, React는 기존 <div>를 삭제하고 새로 <span>을 생성함.

  2. 속성(Props) 비교: 노드의 타입이 동일한 경우, 그 노드의 속성을 비교하여 달라진 부분만 수정함. 예를 들어, <div id="header">에서 <div id="footer">로 바뀌면, id 속성만 변경함.

  3. 자식 노드 비교: 각 노드의 자식들 역시 순서대로 비교하여, 자식 노드에서 변화가 생긴 경우에만 업데이트함.

    1. 자식노드를 탐색하는 방법은?

    2. 리스트 노드를 쉽게 비교하는 방법은?

2.4 Diff 알고리즘 예시

function diff(oldNode, newNode) {
  // 1. 노드 타입 비교
  if (oldNode.type !== newNode.type) {
    console.log(`Replace ${oldNode.type} with ${newNode.type}`);
    return newNode;
  }

  // 2. 속성(Props) 비교
  if (oldNode.props.id !== newNode.props.id) {
    console.log(`Update id from ${oldNode.props.id} to ${newNode.props.id}`);
  }

  // 3. 자식 노드 비교 - 재귀적..?
  if (oldNode.children && newNode.children) {
    ...
  }

  return newNode;
}

// 예시 가상 DOM
const oldNode = { type: 'div', props: { id: 'header' }, children: [{ type: 'span', props: { id: 'text1' }}] };
const newNode = { type: 'div', props: { id: 'footer' }, children: [{ type: 'span', props: { id: 'text2' }}] };

// Virtual DOM 비교
const updatedNode = diff(oldNode, newNode);

2.5 Virtual DOM 업데이트 절차

V DOM을 활용한 화면 갱신 절차는? 🤔

  1. 새로운 Virtual DOM 생성

    • 상태(state)나 props가 변경되면, React는 새로운 Virtual DOM을 생성함. 이 새로운 Virtual DOM은 변경된 상태를 반영하는 트리 구조를 가짐.
  2. 기존 Virtual DOM과 비교

    • 새로운 Virtual DOM과 기존의 Virtual DOM을 비교하여 차이점을 찾음. 이 비교 과정이 React의 Diff 알고리즘임. 이 과정에서는 노드의 타입, 속성, 자식 노드 등을 비교하여 변경된 부분을 파악함.
  3. 차이점을 기반으로 실제 DOM 업데이트

    • Diff 알고리즘이 발견한 차이점(변경 사항)을 실제 DOM에 반영함. 이때, React는 필요한 부분만 실제 DOM에 업데이트하므로, 전체 DOM을 다시 그리지 않아서 성능이 효율적으로 유지됨.
  4. 현재 Virtual DOM을 새로운 Virtual DOM으로 교체

    • 실제 DOM이 업데이트된 후, 새로운 Virtual DOM이 현재 Virtual DOM으로 교체됨. 즉, React는 이 새로운 Virtual DOM을 기준으로 다음 상태 변화가 발생했을 때 또 다른 새로운 Virtual DOM을 생성하고, 다시 비교하는 과정을 반복함.

그런데 바로 업데이트 하면 안되나? 🤔

2.7 실제 DOM 업데이트의 두 가지 방법

  1. 비교와 동시에 업데이트

    • 방법: 변경 사항을 감지할 때마다 즉시 실제 DOM을 업데이트함.

    • 장점: 간단하고 구현이 쉬움. 바로 업데이트되므로 빠르게 반응하는 것처럼 보일 수 있음.

    • 단점: 작은 변경 사항이 많을 경우, 각 변경마다 DOM을 조작하게 되어 성능 저하가 발생할 수 있음. 불필요한 리플로우와 리페인트가 빈번하게 발생함.

  2. 모아서 업데이트 (배치 처리)

  • 방법: 변경 사항을 모아서, 한 번에 실제 DOM에 반영함. React는 이 방식을 사용함.

  • 장점: 변경 사항을 모아서 한 번에 처리하면 DOM 조작을 최소화할 수 있어 성능이 향상됨. 리플로우와 리페인트를 줄여 브라우저 성능에 유리함.

  • 단점: 변경 사항이 많아질 경우, 초기 계산 비용이 증가할 수 있음. 즉각적인 반응성을 요구하는 경우 적합하지 않을 수 있음.


3. DOM 업데이트 시 변경 사항 수집과 반영

  1. 변경 사항 수집collectChanges 함수는 두 개의 Virtual DOM을 비교하여 변경 사항을 수집함. 새 노드를 생성하거나 속성을 업데이트할지, 자식 노드를 비교할지 결정함.

  2. DOM 변경 적용updateDOM 함수는 수집된 변경 사항을 한 번에 실제 DOM에 반영함. replaceChild를 사용하여 부모 노드의 자식을 새로운 노드로 교체함.

3.1 예시 코드: 변경 사항 수집 및 반영

function updateDOM(changes) {
  //change 를 반복하면서, replace 타입이 발견되면, 노드를 교체.
  ...
}

function collectChanges(oldNode, newNode, changes = [], parentElement = null) {
//  노드 비교해서 변경사항이 발견되면 changes 항목에 객체형태로 저장. 
    /*
    ...어쩌구 저꺼구 비교하고나서..
    changes.push({
      type: 'updateProp',
      oldElement: parentElement.querySelector(`#${oldNode.props.id}`),
      newProps: newNode.props
    });
    */
  }

  return changes;
}


// 가상 DOM 비교 후 변경 사항 수집
const changes = collectChanges(oldNode, newNode);

// 수집한 변경 사항을 실제 DOM에 반영
updateDOM(changes);

4. 리스트 항목과 Diffing을 위한 힌트(key) 설정

리스트 항목을 다룰 때, Keyed Diffing은 성능 최적화를 위한 중요한 전략 중 하나임.

각 리스트 항목에 고유한 key를 부여해, React가 항목을 추적하고 변화를 쉽게 파악할 수 있음. 이를 통해 항목의 추가나 삭제 시에도 불필요한 DOM 업데이트를 최소화할 수 있음.

4.1 Key의 중요성

  • key는 React가 각 리스트 항목을 구분하고, 변경이 발생했을 때 정확하게 그 위치를 추적할 수 있도록 도와줌.

  • key가 없거나 잘못 설정되었을 때, 성능 저하나 잘못된 업데이트가 발생할 수 있음. 그렇다면, key를 어떻게 설정해야 성능이 최적화될까?

// 잘못된 key 예시
const items = ['Apple', 'Banana', 'Cherry'];
items.map((item, index) => <div key={index}>{item}</div>);

// 올바른 key 예시
const items = [{ id: 1, name: 'Apple' }, { id: 2, name: 'Banana' }, { id: 3, name: 'Cherry' }];
items.map((item) => <div key={item.id}>{item.name}</div>);

5. Virtual DOM을 사용하지 않는 렌더링 방식

Svelte는 컴파일 단계에서 상태가 변경될 때 어떤 DOM 조작이 필요할지 미리 결정하고, 그에 맞춰 최적화된 JavaScript 코드를 생성함.

<script>
  let items = ['Item 1', 'Item 2', 'Item 3'];

  function addItem() {
    items = [...items, `Item ${items.length + 1}`];
  }
</script>

<ul>
  {#each items as item}
    <li>{item}</li>
  {/each}
</ul>

<button on:click={addItem}>Add Item</button>

5.1 컴파일된 코드 예시 (개념적인 코드임)

let items = ['Item 1', 'Item 2', 'Item 3'];

// 리스트 데이터 DOM에 추가
const ul = document.createElement('ul');
items.forEach(item => {
  const li = document.createElement('li');
  li.textContent = item;
  ul.appendChild(li);
});
document.body.appendChild(ul);

// 버튼 생성
const button = document.createElement('button');
button.textContent = 'Add Item';
document.body.appendChild(button);

// 클릭 핸들러
button.addEventListener('click', () => {
  items.push(`Item ${items.length + 1}`);
  const li = document.createElement('li');
  li.textContent = `Item ${items.length}`;
  ul.appendChild(li); // 새 항목만 추가함
});