리액트 공식문서의 재조정(Reconciliation)을 읽고 정리해 봤다.
개요
리액트는 UI를 갱신할 때 (컴포넌트를 다시 그릴 때) 2가지 조건에 기반하여 두 개의 리액트 엘리먼트 트리를 비교하고 트리를 갱신한다.
- 1) 엘리먼트의 타입이 다르면 서로 다른 트리이다.
- 2) key가 같으면 같은 엘리먼트다 (변경하지 않는다).
이 2가지 조건에 기반하여 트리를 비교하면, 기존 O(n^3) 에서 O(n)으로 시간 복잡도를 줄일 수 있다.
비교 알고리즘
2개 트리를 비교할 때 root 엘리먼트부터 비교한다.
엘리먼트의 타입이 다른 경우
이전 트리를 버리고 완전 새로운 트리를 구축한다. 이전 트리와 연관된 모든 state는 사라진다. 자식 컴포넌트들의 state도 사라진다.
ex)
...child도 똑같이 5초에 childCount 1씩 증가하게 했음
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount((count) => {
return count + 1;
});
}, 5000);
return () => {
clearInterval(intervalId);
};
}, []);
if(count <= 10) {
return (
<div className="App">
{count}
<Child />
</div>
);
} else {
return (
<main className="App">
{count}
<Child />
</main>
);
}
}
5초마다 count 값이 증가하는 컴포넌트를 만들었다. 그리고 count가 10이 넘으면 엘리먼트 타입을 바꿨다. 그랬더니
출력이
(0 0) , (1 1) 이렇게 가다가 10이 넘어서 타입이 바뀌니까 (11 0) (12 1) 이렇게 바뀌었다.
child의 state가 날아간 것이다. 하지만 root의 count는 안 사라졌다. 왜일까?
그래서 App과 Child들의 useEffect(f(),[])
안에 콘솔 출력을 해서 봤더니
Child가 그려졌어요
App이 그려졌어요
--- 엘리먼트 타입이 바뀜 ---
Child가 그려졌어요
이렇게 Child만 다시 그리는 것을 확인할 수 있었다. 즉, root의 state는 남아있고 하위 노드들을 전부 새로 그려서 하위 노드의 state는 다 날아간다.
DOM 엘리먼트의 타입이 같은 경우
두 엘리먼트의 속성을 확인해서 변경된 속성들만 갱신
root부터 그 자식노드까지 재귀적으로 처리
같은 타입의 컴포넌트 엘리먼트
트리를 갖다버리지 않아서 state 유지, props를 갱신한다. 그 다음 렌더한다.
자식에 대한 재귀적 처리
DOM 노드의 자식들을 재귀적으로 처리할 때 두 리스트를 순회하고 차이점이 있으면 변경한다.
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
요런 식으로 뒤에다 추가하면 앞에 2개는 똑같아서 변경하지 않고 뒤에 추가만 해서 변경이 적다.
그러나 앞에다 추가하면 순회하면서 차례로 비교하기 때문에 변경이 많아 성능이 좋지 못하다. -> 성능 개선할 때 체크
요런 문제들을 방지하기 위해 key
속성을 지원한다.
Keys
자식들이 Key를 가지고 있으면 Key를 통해 기존과 새로 생성된 애를 비교한다. 그래서 Key가 같으면 만들어진 애를 다시 가져다 쓴다. (변경하지 않는다) -> 성능에 중요한 포인트일 듯
key는 변하지 않아야 한다. 변하는 키는 컴포넌트를 재생성시키고, 컴포넌트 state가 날아갈 수도 있다.
'프론트엔드 > React' 카테고리의 다른 글
HOC (1) | 2022.06.04 |
---|---|
useCallback 과 useMemo (0) | 2022.06.04 |
Memoization (0) | 2022.06.04 |
useContext (0) | 2022.05.14 |
useReducer (0) | 2022.05.14 |