위에서 설명한 바와 같이, 리액트가 선언적 API를 제공하기 때문에 리액트의 사용자는 렌더링 작업을 함에 있어서 매번 무엇이 바뀌었는지를 걱정할 필요가 없을 것이다. 하지만, 내부에서는 기존의 Virtual DOM과 변경사항이 생긴 Virtual DOM 의 비교작업이 이루어지는데,이 과정을 Reconciliation(비교 조정, 재조정)이라고 한다.
컴포넌트에서 prop이나 state가 변경될 때, 직전에 렌더링된 요소(element)와 새로 반환된 요소를 비교하여 두 element가 일치하지 않으면 리액트는 새로운 요소로 DOM을 업데이트 하는데, 이러한 프로세스를 Reconciliation(비교 조정, 재조정) 이라고 한다.
실제로는 모든 DOM 트리를 순회하면서 탐색 및 변경하는 과정을 거쳐야 하는데, 지금까지 알려진 알고리즘은 O(n^3)의 시간복잡도를 가지므로 1000개의 요소를 표시하려면 무려 10억번의 비교가 필요하다. 따라서, 리액트에서는 이 대신 아래 두 가지 가정에 따른 휴리스틱 알고리즘을 채택하였다.
key prop
을 이용해 다른 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 안정적인 자식 요소에 대한 힌트를 얻을 수 있다. // before
<div className="before" title="stuff" />
// after
<div className="after" title="stuff" />
두 앨리먼트를 비교했을 떄, className
만 변경되었으므로 동일한 속성은 유지하고 className
만 변경해준다.// 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
를 추가하는 경우 간단하게 구현할 수 있겠지만 만약, 첫번째 요소에 엘리먼트를 추가 할 경우 성능이 좋지 않을 것 이다.
첫번째 요소부터 일치하지 않으므로 리액트는 다른 타입으로 판단해 모든 자식을 변경한다. 사실은 한 요소만 추가된 것인데 전체 자식을 변경하게 되므로 성능적인 측면에서 심각한 낭비를 초래할 것이다.
위와 같은 상황을 해결하기 위해 리액트에서는 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는 형제 내에서만 유일하면 된다.)