React에서 특별한 비교(diffing)알고리즘을 통해 효과적으로 UI 갱신을 해서 성능이 빠르다.
다른 비교 알고리즘은 최소 O(n^3)의 시간복잡도를 가지지만, React의 알고리즘은 다음과 같은 특징이 있는 O(n) 복잡도의 휴리스틱 알고리즘을 구현했다.
-
서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
-
key
prop을 통해서 개발자가 렌더링시 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.
두 개의 트리를 비교할 때, React는 두 엘리먼트의 root 엘리먼트부터 비교한다. 이후의 동작은 루트 엘리먼트의 타입에 따라 달라진다.
에서 로, 에서로 바뀌는 것 모두 포함된다.
비교하는 두 엘리먼트의 root 엘리먼트 타입이 다르면, React는 새로운 트리를 구축한다.
루트 엘리먼트 아래의 모든 컴포넌트도 언마운트되고 그 state도 사라진다. 예를 들어, 아래와 같은 비교가 일어나면, 이전 Counter
는 사라지고, 새로 다시 마운트가 될 것이다.
<div>
<Counter />
</div>
<span>
<Counter />
</span>
같은 타입의 두 React DOM 엘리먼트를 비교할 때는 두 엘리먼트의 속성을 확인해서 동일한 내역은 유지하고 변경된 속성들만 현재의 DOM 노드에 갱신한다.
DOM 노드의 처리가 끝나면, React는 이어서 해당 노드의 자식들을 재귀적으로 처리한다.
[예시] className
만 수정
<div className="before" title="stuff" />
<div className="after" title="stuff" />
[예시] style
만 수정
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />
컴포넌트가 갱신되면 인스턴스는 동일하게 유지되어 렌더링 간 state가 유지된다. React는 새로운 엘리먼트의 내용을 반영하기 위해 현재 컴포넌트 인스턴스의 props를 갱신한다. 이때 해당 인스턴스의 componentDidUpdate
를 호출한다.
다음으로 render()
메서드가 호출되고 비교 알고리즘이 이전 결과와 새로운 결과를 재귀적으로 처리한다.
DOM 노드의 자식들을 재귀적으로 처리할 때, React는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 mutation을 생성한다.
이러한 방식으로는 리스트의 맨 앞에 엘리먼트를 추가하는 경우 성능이 좋지 않다.
[예시]
<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>
종속 트리를 그대로 유지하는 대신 모든 자식을 변경한다. 아주 비효율적이다.
이러한 문제를 해결하기 위해, React는
key
속성을 지원한다.
자식들이 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'
key를 가진 엘리먼트가 새로 추가되었고, '2015'
와 '2016'
key를 가진 엘리먼트는 그저 이동만 하면 되는 것을 알 수 있다.
key값으로는 데이터에 ID 속성을 추가해주거나 데이터 일부에 해시를 적용해서 key를 생성할 수 있다. 해당 key는 오로지 형제 노드 사이에서만 유일하면 되고, 전역에서 유일할 필요는 없다.
하지만 항목들이 sort되는 경우에는 인덱스를 key로 사용하는 것은 비효율적이다. 인덱스를 key로 사용하면, 항목의 순서가 바뀌었을 때 key 또한 바뀔 것이다. 컴포넌트의 state가 엉망이 되거나 의도하지 않은 방식으로 바뀔 수도 있다.
여기서 말하는 재렌더링은 모든 컴포넌트의 render
를 호출하는 것이지 React가 언마운트시키고 다시 마운트하는 것은 아니다. 즉, 앞에서 규칙에 따라 렌더링 전후에 변경된 부분만을 적용할 것이다.
React는 휴리스틱에 의존하고 있기 때문에 휴리스틱이 기반하고 있는 조건에 부합하지 않는 경우 성능이 나빠질 수 있다.
-
이 알고리즘은 다른 컴포넌트 타입을 갖는 종속 트리들의 일치 여부를 확인하지 않는다.
매우 비슷한 결과물을 출력하는 두 컴포넌트를 교체하고 있다면, 그 둘을 같은 타입으로 만드는 것이 더 나을 수도 있다.
-
key는 반드시 변하지 않고, 예상 가능하며, 유일해야 한다.
변하는 key(
Math.random()
으로 생성된 값 등)를 사용하면 많은 컴포넌트 인스턴스와 DOM 노드를 불필요하게 재생성하여 성능이 나빠지거나 자식 컴포넌트의 state가 유실될 수 있다.