- React는 선언적 API를 제공하기 때문에 갱신이될 때마다 매번 무엇이 바뀌었는지 걱정하지 않아도 된다.
React의 비교 알고리즘을 만들 때 어떤 선택을 했는지
- render() 함수는 "React의 엘리먼트 트리 를 만드는 것이다." 라고 생각이 드는 순간이 있다.
- state, props가 갱신되면 render() 함수는 새로운 React 엘리먼트 트리를 반환할 것이다.
- 이때 React는 방금 만들어진 React 엘리먼트 트리에 맞게 가장 효과적으로 ui를 갱신하는 방법을 알아낼 필요가 있다.
- 이러한 트리를 다른 트리로 변환하기 위한 최소한의 연산 수를 구하는 알고리즘을 풀기 위한 일반적인 해결책이 있긴 하지만,
- n개의 엘리먼트가 있는 트리에 대해 O(n3승) 복잡도를 가진다.
- React에 해당 알고리즘을 적용 시, 1000개의 엘리먼트를 그리기 위해 10억 번의 비교 연산을 수행해야 한다.
그래서 React는 2가지 가정을 기반하여 O(n) 복잡도의 휴리스틱 알고리즘을 통해 이를 개선함.
휴리스틱 알고리즘
- 컴퓨터로 어떤 문제를 해결하기 위한 명확한 절차가 없거나 또는 있다할지라도 그것이 방대한 시간과 비용을 필요로 하는 경우에, 일반적으로 경험적 지식을 도입하여 순차적이며 체계적으로 그 해결방법을 산출해 가는 방법.
2가지 가정
1. 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
- 개발자가 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해줄 수 있다.
비교 알고리즘
- 두 개의 엘리먼트 트리를 비교할 때, React는 두 엘리먼트의 root 엘리먼트로 부터 비교한다.
- 이후의 동작은 루트 엘리먼트의 타입에 따라 달라진다.
엘리먼트의 타입이 다른 경우
- 두 루트 엘리먼트의 타입이 다르면, React는 이전 트리를 버리고 완전히 새로운 트리를 구축한다.
<a> -> <img>
<Article> -> <Comment>
<Button> -> <div>
- 트리를 버릴 때 이전 DOM 노드들은 모두 파괴된다.
-> 컴포넌트 인스턴스의 경우 componentWillUnmount() 가 실행된다.
- 새로운 트리가 만들어질 때, 새로운 DOM 노드들이 DOM에 삽입된다.
-> 컴포넌트 인스턴스의 경우 UNSAFE_componentWillMount() 가 실행되고
-> 이어 componentDidMount()가 실행된다.
- 이전 트리와 연관된 모든 state는 사라진다.
루트 엘리먼트 아래의 모든 컴포넌트도 Unmount되고, state또한 사라진다.
// 두 루트 엘리먼트의 타입이 다름.
<div>
<Counter />
</div>
<span>
<Counter />
</span>
- 위의 경우 보여지는 모습이 같더라도 React 구조는 서로 다르다.
<div>
{isToggle ? <Counter /> : <Counter />
</div>
<div>
{isToggle ? <div><Counter /></div> : <span><Counter /></span>
</div>
- A는 Counter의 state는 유지된다. -> 루트 엘리먼트 타입이 같기에.
- B는 Counter의 state가 유지되지 않는다. -> 루트 엘리먼트 타입이 다름.
DOM 엘리먼트의 타입이 같은 경우
- 같은 타입의 두 React DOM 엘리먼트를 비교할 때, React는 두 엘리먼트의 속성을 확인하여 동일한 내역은 유지하고 변경된 속성들만 갱신한다.
<div className="before" title="stuff" />
<div className="after" title="stuff" />
- 위 두 엘리먼트를 비교하면, React는 현재 DOM노드 상에 className만 수정한다.
DOM 노드의 처리가 끝나면, React는 이어서 해당 노드의 자식들을 재귀적으로 처리한다.
- Root 노드의 변화에 따라 자식 노드도 변화함.
같은 타입의 컴포넌트 엘리먼트
- 컴포넌트가 갱신되면 인스턴스는 동일하게 유지되어 렌더링 간 state가 유지된다.
- React는 새로운 엘리먼트의 내용을 반영하기 위해 현재 컴포넌트의 인스턴스의 props를 갱신한다.
- 해당 인스턴스의 componentWillReceiveProps, WillUpdate, DidUpdate를 호출한다.
- 다음으로 render() 메서드가 호출되고 비교 알고리즘이 이전 결과와 새로운 결과를 재귀적으로 처리한다.
자식에 대한 재귀적 처리
- React는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성한다.
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
- 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성한다.
- 위와 같이 단순하게 구현 시, 리스트의 맨 앞에 엘리먼트를 추가하는 경우 성능이 좋지 않다. 변경을 한번 더 순회 후 생성 -> 비효율.
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
- 굳이 변경하지 않아도 되는 Duke, Villanova 종속 트리를 그대로 유지하는 대신, 모든 자식을 변경한다.
keys
- 이러한 문제를 해결하기 위해, 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>
- 2014 key 를 가진 엘리먼트가 새로 추가
- 2015, 2016 key를 가진 엘리먼트는 그저 이동만 하면 되는 것을 React가 알 수 있다.
<li key={item.id}>{item.name}</li>
- 해당 key 는 오로지 형제 사이에서만 유일하면 된다. 전역에서 유일할 필요는 없음.
- 배열의 인덱스를 사용할 경우 배열이 재배열될 경우 key가 형제 사이에서 얽힌다.
-> key를 사용하는 이유가 퇴색됨. -> 재조정 과정에서 형제 사이에 key를 통해 변경이 필요한지 안한지 React에게 알려줄 수 있는데, 배열이 재조정되버리면 다시 모든 엘리먼트를 비교해야함.
재조정 알고리즘은 구현상의 세부사항이라는 것을 명심.
-
React는 항상 전체 앱을 재렌더링할 수 있지만, 최종적으로 출력되는 결과는 항상 같을 것이다.
-
재렌더링은 모든 컴포넌트의 render() 를 호출하는 것이지 React가 Unmount -> Mount 하는 것이 아니다.
-
위 규칙에 따라 렌더링 전후에 변경된 부분만을 적용할 것이다.
즉, 규칙에 따라 렌더링 전후에 변경된 부분만을 효율적으로 적용하는 것이다.
-
현재 구현체에서는 한 종속 트리가 그 형제 사이에서 이동했다는 사실을 표현 가능하지만
-
아예 다른 곳으로 이동했다는 사실을 표현할 수 없다.
-
알고리즘은 전체 종속 트리를 재렌더링할 것이다.
리액트는 휴리스틱에 의존하고 있기 때문에, 휴리스틱 기반이 하고 있는 가정에 부합하지 않는 경우 성능이 나빠질 수 있다.
1. 알고리즘은 다른 컴포넌트의 타입을 갖는 종속 트리들의 일치 여부를 확인하지 않는다.
- 매우 비슷한 결과물을 출력하는 두 컴포넌트를 교체할 경우
- 그 둘을 같은 타입으로 만드는 것이 더 나을 수 있다.
- key는 반드시 변하지 않고, 예상이 가능하며, 유일해야 한다.
- 많은 컴포넌트의 인스턴스와 DOM 노드를 불필요하게 재생성하여 성능이 나빠지거나 자식 컴포넌트의 state가 유실될 수 있다.
재조정 (Reconciliation)
글 잘 봤습니다.