재조정 (Reconciliation)

React는 선언적 API를 제공하므로 모든 업데이트에서 변경사항이 정확히 어떤 것인지 걱정할 필요가 없습니다. 이렇게 하면 어플리케이션 작성이 많이 쉬워지지만 React 내에서 어떻게 구현되어있는지 분명하지않을수 있습니다. 이 섹션에서는 React에서 선택한 고성능 앱에서 충분히 빠르고 컴포넌트 업데이트를 예측하는 “비교 (diffing)” 알고리즘에 대해 소개합니다.

동기

React를 사용할 때 한 시점에서 render() 함수가 React 요소 트리를 생성하는 것으로 생각할 수 있습니다. state나 props 업데이트 이후 render() 함수는 다른 React 요소 트리를 반환합니다. 그런 다음 React는 가장 최근 트리와 일치하도록 UI를 효율적으로 업데이트하는 방법을 찾아야합니다.

한 트리를 다른 트리로 변환하기 위한 최소한의 연산을 생성하는 알고리즘 문제에 대한 일반적인 해결방법이 있습니다. 그러나 state of the art algorithms 은 O (n3) 순서로 복잡성을 가지며 이때 n은 트리 내 요소 갯수입니다.

이 알고리즘을 React에서 사용하면 1000개 요소를 표시하는 데 10억번 비교가 필요합니다. 이 비용은 너무 비쌉니다. 대신 React는 두가지 가정에 따른 발견적 0(n) 알고리즘을 사용합니다.

  1. 다른 타입의 두 요소는 서로 다른 트리를 생성합니다.
  2. 개발자는 key prop을 이용해 다른 렌더링 사이에서 안정적인 자식 요소에대한 힌트를 얻을 수 있습니다.

실제로 이런 가정은 거의 모든 실제 사용 사례에 유효합니다.

비교 알고리즘 (Diffing Algorithm)

두 트리를 비교할 때 React는 두 루트 요소부터 비교합니다. 이 동작은 루트 요소의 타입에 따라 다릅니다.

다른 타입의 요소

루트 요소가 다른 타입을 가질때마다 React는 오래된 트리를 해체하고 새로운 트리를 처음부터 빌드합니다. <a> 에서 <img> 혹은 <Article> 에서 <Comment> 혹은 <Button> 에서 <div> 등 어떠한 것이던 완전히 재빌드로 이끕니다.

트리를 해체할 때 이전 DOM 노드는 제거됩니다. 컴포넌트 인스턴스는 componentWillUnmount() 를 받습니다. 새 트리가 만들어질 때 새 DOM 노드는 DOM에 들어갑니다. 컴포넌트 인스턴스가 componentWillMount() 를 받은 후 componentDidMount() 를 받습니다. 이전 트리와 관련한 모든 state가 손실됩니다.

루트 아래의 모든 컴포넌트의 마운트도 해제되고 state가 파괴됩니다. 예를 들어 아래 예제를 비교할 때:

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

이 예제는 이전 Counter 를 제거하고 새로운 것으로 다시 마운트합니다.

같은 타입의 DOM 요소

같은 타입의 두 React DOM 요소를 비교할 때 React는 두 요소의 속성을 보고 같은 DOM 노드를 유지하며 변경된 속성만 업데이트합니다. 예를 들어,

<div className="before" title="stuff" />

<div className="after" title="stuff" />

두 요소를 비교하여 React는 DOM 노드에서 className 만 수정되고있다는 사실을 알게됩니다.

style 을 업데이트할 때, React는 변경된 속성만 업데이트해야한다는 걸 알고있습니다. 예를 들어,

<div style={{color: 'red', fontWeight: 'bold'}} />

<div style={{color: 'green', fontWeight: 'bold'}} />

이 두 요소를 변환할 때 React는 fontWeight 에는 변경이 없고 color 스타일만 변경되었다는 것을 알고있습니다.

DOM 노드를 처리한 후 React는 자식에 대해 반복됩니다.

같은 타입의 컴포넌트 요소

컴포넌트가 업데이트될 때 인스턴스가 동일하게 유지되면 렌더링간에 state가 유지됩니다. React는 새 요소와 일치하도록 컴포넌트 인스턴스의 props를 업데이트하고 인스턴스에서 componentWillReceiveProps()componentWillUpdate() 를 호출합니다.

다음으로 render() 메서드가 호출되고 비교 알고리즘은 이전 결과와 새로운 결과에 반복됩니다.

자식에서 반복

기본적으로 DOM 노드의 자식에 대해 반복할 때 React는 동시에 두 자식 목록을 반복하여 실행하고 차이가 있을 때마다 변형을 생성합니다.

예를 들어 자식의 끝에 요소를 추가할 때 두 트리 사이의 변환은 잘 동작합니다.

<ul>
  <li>first</li>
  <li>second</li>
</ul>

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

React는 두 <li>first</li> 트리와 일치하고, 두번째 <li>second</li> 트리와 일치한 다음 <li>third</li> 트리를 넣습니다.

순진하게 구현하면 처음에 요소를 넣으면 성능이 떨어집니다. 예를 들어 아래 두 트리 사이 변환은 제대로 작동하지않습니다.

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

React는 모든 자식을 변형시켜 <li>Duke</li><li>Villanova</li> 서브 트리를 그대로 유지할 수 있음을 깨닫지 못합니다. 이러한 비효율은 문제가 될 수 있습니다.

Keys

이 이슈를 해결하기 위해 React는 key 속성을 지원합니다. 자식이 키를 가질 때 React는 오리지널 트리의 자식을 후속 트리의 자식과 비교할 때 키를 사용합니다. 예를 들어 위의 비효율적인 예제에 key 를 추가하면 트리를 효율적으로 변환할 수 있습니다.

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

이제 React는 '2014' 키를 가진 요소가 새로운 것임을 알고 있고 '2015''2016' 키를 가진 요소는 옮겨진 것임을 압니다.

실제로 키를 찾는 건 보통 어려지 않습니다. 표시하려는 요소에 이미 고유한 ID가 있을 수 있으므로 키는 데이터에서 올 수도 있습니다.

<li key={item.id}>{item.name}</li>

그렇지 않은 경우 모델에 새 ID 속성을 추가하거나 콘텐츠의 일부분을 해시하여 키를 생성할 수도 있습니다. 키는 글로벌하게 고유할 필요는 없고 형제 사이에서만 고유하면 됩니다.

마지막 수단으로 배열의 아이템 인덱스를 키로써 전달할 수 있습니다. 이는 재정렬하지 않은 아이템에서는 잘 동작하지만 재정렬할 때는 느리게 동작합니다.

인덱스를 키로써 사용하면 재정렬을 통해 컴포넌트 state에 문제가 발생할 수도 있습니다. 컴포넌트 인스턴스는 키에 따라 업데이트되고 재사용됩니다. 만약 키가 인덱스라면 아이템을 이동하면 해당 아이템이 변경됩니다. 그 결과로 제어되는 input 같은 컴포넌트 state가 예기치 않은 방식으로 혼합되고 업데이트될 수 있습니다.

다음은 CodePen에서 인덱스를 키로 사용해서 문제가 발생한 예제이며, 같은 예제지만 인덱스를 키로 사용하지 않아서 재정렬, 정렬 및 우선처리 문제를 해결하는 다른 예제가 있습니다.

Tradeoffs

재조정 알고리즘은 구현 세부 사항이라는 걸 기억하는 게 중요합니다. React가 모든 작업에서 전체 앱을 다시 렌더링할 수 있습니다. 최종 결과는 동일합니다. 이 문맥에서 다시 렌더링이란 모든 컴포넌트에 대해 render 를 호출하는 걸 의미하고 이는 React가 마운트를 해체하고 다시 마운트한다는 걸 의미하지는 않습니다. 이전 섹션에서 설명한 규칙에 따라 차이점에서만 적용됩니다.

일반적인 사용사례를 더 빠르게 만들기 위해 경험적 방법 (heuristics)을 정기적으로 수정하고 있습니다. 현재 구현에서는 하위 트리가 형제 사이에서 이동했다는 사실을 설명할 수 있지만 다른 곳으로 이동했다고 말할 수는 없습니다. 알고리즘은 전체 하위트리를 다시 렌더링합니다.

React는 경험적 방법에 의존하기 때문에 뒤의 가정이 충족하지 않으면 성능이 저하됩니다.

  1. 알고리즘은 다른 컴포넌트 타입의 서브트리를 일치시키려고 하지않습니다. 아주 유사한 출력을 가진 두 컴포넌트 타입을 번갈아 사용하는 경우 같은 타입으로 만들 수 있습니다. 실제로 이런 이슈를 발견하지 못했습니다.

  2. 키는 안정적이고 예측가능하며 고유해야합니다. Math.random() 을 이용해 생성된 키와 같은 불안정한 키는 많은 컴포넌트 인스턴스와 DOM 노드가 불필요하게 다시 작성되어 하위 컴포넌트의 성능 저하 및 state 손실을 초래할 수 있습니다.