[React] Virtual DOM? Reconciliation(비교조정, 재조정)이란?

82.831·2023년 4월 17일
0

  • DOM (Document Object Model) 이란?
    • HTML과 XML과 같은 문서구조를 scripts나 프로그래밍 언어로 연결시켜주는 API이다.
    • 애플리케이션의 UI를 나타내고, 애플리케이션 UI의 상태가 변경될 때마다 해당 변경 사항을 나타내기 위해 업데이트된다.
    • 단점
      • DOM은 트리구조로 표현되기 때문에 DOM의 변경과 업데이트는 비교적 빠르다.
        하지만, 변경된 후 업데이트된 element들을 다시 렌더링하여 UI를 업데이트 해야 한다.
        이 때, css 재연산, 레이아웃 구성, 페이지 리페인트 등을 하기 때문에 UI 구성 요소가 많아지면 모든 DOM을 리렌더링하는데 속도가 느려질 수 밖에 없다.

그렇게 나오게 된 Virtual DOM !

  • 리액트는 위와 같은 문제를 해결하기 위해 실제 DOM 대신 실제 DOM의 사본과 같은 가상의 DOM (Virtual DOM) 개념을 도입하였고, 다음과 같이 이 가상의 DOM을 업데이트하는 방식을 사용해 실제 DOM의 업데이트 횟수를 줄인다.
    1. 업데이트한 전체 UI를 Virtual DOM에 리렌더링한다.
    2. 실제 DOM과 생성된 Virtual DOM을 비교한다.
    → 이 때 Diffing Algorithm을 따른다. 이 알고리즘은 아래에서 설명한다.
    3. 바뀐 부분만 실제 DOM에 적용한다. 최소한의 렌더링만 할 수 있도록)
    → ReactDOM.render()가 React element를 container DOM에 렌더링할 때 필요한 부분만 변경한다.
  • 위와 같은 방식을 통해 리액트는 선언적 API가 가능하게 된다. 즉, 우리가 리액트에게 원하는 UI의 상태를 알려주면(선언적), 그 상태를 기반으로 DOM은 자동적으로 업데이트가 되는 것이다.

Reconciliation 이란?

  • 위에서 설명한 바와 같이, 리액트가 선언적 API를 제공하기 때문에 리액트의 사용자는 렌더링 작업을 함에 있어서 매번 무엇이 바뀌었는지를 걱정할 필요가 없을 것이다. 하지만, 내부에서는 기존의 Virtual DOM과 변경사항이 생긴 Virtual DOM 의 비교작업이 이루어지는데,이 과정을 Reconciliation(비교 조정, 재조정)이라고 한다.

  • 컴포넌트에서 prop이나 state가 변경될 때, 직전에 렌더링된 요소(element)와 새로 반환된 요소를 비교하여 두 element가 일치하지 않으면 리액트는 새로운 요소로 DOM을 업데이트 하는데, 이러한 프로세스를 Reconciliation(비교 조정, 재조정) 이라고 한다.

    • 실제로는 모든 DOM 트리를 순회하면서 탐색 및 변경하는 과정을 거쳐야 하는데, 지금까지 알려진 알고리즘은 O(n^3)의 시간복잡도를 가지므로 1000개의 요소를 표시하려면 무려 10억번의 비교가 필요하다. 따라서, 리액트에서는 이 대신 아래 두 가지 가정에 따른 휴리스틱 알고리즘을 채택하였다.

      1. 서로 다른 타입의 두 요소는 서로 다른 트리를 만들어낸다.
      2. key prop을 이용해 다른 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 안정적인 자식 요소에 대한 힌트를 얻을 수 있다.
      • Diffing Algorithm (비교 알고리즘)
        두개의 트리를 비교할 때, 리액트는 두 엘리먼트의 루트(root)부터 비교한다. 이후 동작에서는 루트 요소의 타입에 따라 달라지는데, 비교하는 두 요소의 타입이 같을 때와 다를 때로 나눌 수 있다.
        • DOM 요소 타입이 같은 경우
          • 리액트는 두 엘리먼트의 속성을 확인하여, 동일한 내역은 유지하고 변경된 속성들만 갱신한다.
            // before
            <div className="before" title="stuff" />
            
            // after
            <div className="after" title="stuff" />
            두 앨리먼트를 비교했을 떄, className만 변경되었으므로 동일한 속성은 유지하고 className만 변경해준다.
        • DOM 요소 타입이 다른 경우
          • 리액트는 이전의 트리를 버리고 완전히 새로운 트리를 구축한다.
            // before
            <div>
              <Counter />
            </div>
            
            // after
            <span>
              <Counter />
            </span>
            바뀐 이후의 트리의 루트 엘리먼트는 div 에서 span태그로 바뀐 이 경우 새로운 트리를 구축하게 되는 것이다. 트리를 버릴 때 이전 DOM 노드들은 모두 삭제되며, 새로운 트리가 만들어 질 때 새로운 DOM 노드들이 DOM에 삽입된다. (이전의 state들은 모두 사라진다.)

      • 자식에 대한 재귀적인 처리 (key prop)

        • DOM 노드의 자식들을 재귀적으로 처리할 때, 리액트는 기본적으로 동시에 두 리스트를 순회하 고 차이점이 있으면 변경한다.

           // before
          <ul>
            <li>first</li>
            <li>second</li>
          </ul>
          
          // after
          <ul>
            <li>first</li>
            <li>second</li>
            <li>third</li>
          </ul>

          만약 위와 같은 트리 구조의 DOM이 바뀐다고 할 떄, first와 second를 비교하여 일치하는 것을 확인하고 마지막 요소에 third를 추가하는 경우 간단하게 구현할 수 있겠지만 만약, 첫번째 요소에 엘리먼트를 추가 할 경우 성능이 좋지 않을 것 이다.
          첫번째 요소부터 일치하지 않으므로 리액트는 다른 타입으로 판단해 모든 자식을 변경한다. 사실은 한 요소만 추가된 것인데 전체 자식을 변경하게 되므로 성능적인 측면에서 심각한 낭비를 초래할 것이다.

          keys

          위와 같은 상황을 해결하기 위해 리액트에서는 key 속성을 지원하여 다음과 같이 구현한다.

          // before 
          <ul>
            <li key="first">first</li>
            <li key="second">second</li>
          </ul>
          
          // after
          <ul>
            <li key="zero">zero</li>
            <li key="first">first</li>
            <li key="second">second</li>
          </ul>

          자식들이 key를 가지고 있다면, 리액트는 key를 통해 기존 트리와 이후 트리의 자식들이 일치하는지 확인한다.

          first와 second라는 key를 가진 li요소는 이미 있으므로 변경하지 않고, zero라는 key를 가진 li 요소만 맨 위에 추가하는 것으로 해결할 수 있다!

          컴포넌트 인스턴스는 key를 기반으로 갱신되고 재사용되므로 요소를 인덱스 대신 유일하게 식별 가능한 요소로 두는 것이 바람직하다. key 값으로 인덱스를 사용했을 경우 항목의 순서가 바뀌었을 때 재배열되어 컴포넌트의 순서가 엉망이 될 수 있기 때문이다. (key는 형제 내에서만 유일하면 된다.)

0개의 댓글