
시작 하기전, 리액트의 렌더링 단계를 포스팅 했던 적이 있습니다. 단순하게 Trigger 단계, Render 단계, Commit 단계를 설명했는데요.
비교 알고리즘에 대해 좀 더 공부해보고자 기존에 썼던 렌더링 단계 포스팅을 여기 함께 녹여내도록 했습니다.
리액트의 렌더링 단계를 다시 살펴보자면
리액트가 렌더링을 일으키게 하는 트리거 발생 단계
초기 렌더링
리렌더링(상태 업데이트) 트리거 예시
값이 변경되면 React가 이를 감지하고 리렌더링을 예약합니다.
화면에 컴포넌트를 그려서 반영하는 과정인 브라우저 렌더링과는 다른 렌더링을 화면에 표시할 컴포넌트를 호출하는 작업
즉, 재조정 단계를 거쳐 가상 DOM요소의 변화를 감지하고 필요한 업데이트를 결정하는 단계
여기서 Diffing 알고리즘(Reconciliation) 이 실행됩니다.
초기 렌더링
초기 렌더에서 렌더 단계는 render() 메소드의 루트 컴포넌트를 호출합니다
function App() {
return (
<main>
<h1>hello world</h1>
<Item />
<Item />
</main>
);
}
function Item() {
return <div>I am a Child</div>;
}
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<App />
);
대상 DOM 노드로 createRoot(React 18부터 새로 도입된 루트 api)를 호출한 다음 render() 메소드를 호출하고 루트 컴포넌트를 화면에 그리는 동작을 진행합니다.
render() 메소드가 호출되면 리액트는 createElement()로 <main>, <h1>, <div> 태그명의 HTML 요소들을 생성합니다.

리렌더링(상태 업데이트)
이전에 생성한 가상 DOM 트리와 새로 만든 가상 DOM 트리를 비교해 실제 DOM에 반영할 변경 사항들을 파악합니다. 최소한의 변경 사항만 파악하기 위해 상태 업데이트가 발생한 컴포넌트를 호출하고 새로운 가상 DOM 트리를 만듭니다. 리액트가 이전 렌더와 다음 렌더의 변화를 비교하는 과정을 재조정이라고 합니다.

리렌더가 발생하면 리액트는 렌더 간 어떤 요소와 속성들이 변했는지를 파악하고, 이 정보를 커밋 단계에서 사용합니다.
리액트는 컴포넌트를 호출하고 모든 자식 요소들을 재귀적으로 처리해 트리의 구성 요소들을 파악합니다. 여기서 재귀적으로 처리한다는 것은 업데이트된 컴포넌트가 다른 컴포넌트를 반환하면 React는 다음으로 해당 컴포넌트를 렌더링하고 해당 컴포넌트도 컴포넌트를 반환하면 반환된 컴포넌트를 다음에 렌더링하는 방식입니다. 중첩된 컴포넌트가 더 이상 없고 React가 화면에 표시되어야 하는 내용을 정확히 알 때까지 이 단계는 계속됩니다.
함수가 연쇄적으로 호출될 때 내부의 JSX는 React.createElement() 함수로 JSX를 리액트 요소로 변환하는데요, 재귀적으로 생성된 리액트 요소들은 UI의 구조를 나타내는 객체이자 DOM의 가상 복사본인 가상 DOM으로 유지됩니다. 트리를 따라 호출을 반복하다가 최종적으로 더 이상 컴포넌트가 반환되지 않으면 비로소 가상 DOM 트리가 그려집니다.
초기 렌더링
렌더 단계에서 호출 된 모든 컴포넌트들을 DOM 노드에 배치(반영)하게 되는 작업
React는 appendChild() DOM API를 사용하여 생성한 모든 DOM 노드를 화면에 표시
리렌더링(상태 업데이트)
이전 단계에서 계산한 차이점이 발견된 경우에만 DOM 노드를 변경하는 작업
React는 필요한 최소한의 작업(렌더링하는 동안 계산된 것)을 적용하여 DOM이 최신 렌더링 출력과 일치하도록 합니다.

두 개의 트리를 비교할 때, React는 두 엘리먼트의 루트(root) 엘리먼트부터 비교합니다. 이후의 동작은 루트 엘리먼트의 타입에 따라 달라집니다.
e.g) <a>태그에서 <img>태그로 변경 되는 경우
트리를 버릴 때 이전 DOM 노드들은 모두 파괴됩니다. 컴포넌트 인스턴스는 componentWillUnmount()가 실행됩니다. 새로운 트리가 만들어질 때, 새로운 DOM 노드들이 DOM에 삽입됩니다. 그에 따라 컴포넌트 인스턴스는 UNSAFE_componentWillMount()가 실행되고 componentDidMount()가 이어서 실행됩니다. 이전 트리와 연관된 모든 state는 사라집니다.
componentWillUnmount: 컴포넌트가 마운트 해제되어 제거되기 직전에 호출
*UNSAFE_componentWillMount()는 마운트가 발생하기 전에 호출됩니다. render()가 실행되기 전에 호출되므로, 이 메서드 내에서 setState()를 동기적으로 호출하더라도 추가적인 렌더링이 발생하지 않습니다. state를 초기화하는 경우라면, 보통은 constructor()를 사용하는 것이 좋습니다.
e.g)
```html
// className만 수정합니다.
<div className="before" title="stuff" />
<div className="after" title="stuff" />
```
```html
// style 안에서도 color 속성만 갱신 합니다.
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />
```
// 두 트리에서 <li>first</li> <li>second</li>가 일치 하는것을 확인하고 <li>third</li>를 트리에 추가합니다.
<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>
위와 같은 문제를 해결하기 위해 key속성을 지원합니다. 자식들이 key를 가지고 있다면, React는 key를 통해 기존 트리와 이후 트리의 자식들이 일치하는지 확인합니다.
// '2014' key를 가진 엘리먼트가 새로 추가되었고, '2015'와 '2016' 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>
이러한 상황에 해당하지 않는다면, 여러분의 데이터 구조에 ID라는 속성을 추가해주거나 데이터 일부에 해시를 적용해서 key를 생성할 수 있습니다. 해당 key는 오로지 형제 사이에서만 유일하면 되고, 전역에서 유일할 필요는 없습니다.
최후의 수단으로 배열의 인덱스를 key로 사용할 수 있습니다. 항목들이 재배열되지 않는다면 이 방법도 잘 동작할 것이지만, 재배열되는 경우 비효율적으로 동작할 것입니다.
인덱스를 key로 사용 중 배열이 재배열되면 컴포넌트의 state와 관련된 문제가 발생할 수 있습니다. 컴포넌트 인스턴스는 key를 기반으로 갱신되고 재사용됩니다. 인덱스를 key로 사용하면, 항목의 순서가 바뀌었을 때 key 또한 바뀔 것입니다. 그 결과로, 컴포넌트의 state가 엉망이 되거나 의도하지 않은 방식으로 바뀔 수도 있습니다.
ref
https://ko.legacy.reactjs.org/docs/reconciliation.html
https://www.moonkorea.dev/React-%EB%A0%8C%EB%8D%94%EB%8B%A8%EA%B3%84-%EC%BB%A4%EB%B0%8B%EB%8B%A8%EA%B3%84#%ED%8A%B8%EB%A6%AC%EA%B1%B0-%EB%8B%A8%EA%B3%84
https://react.dev/learn/render-and-commit