가상 DOM과 재조정
Article
1강에서 "React가 바뀐 곳만 고친다"고 하고 넘어갔다. 13강에서 그 일이 커밋 단계에서 일어난다고 했다.
이번 강의는 그 사이에서 무슨 일이 벌어지는지 본다.
DOM 조작은 왜 비싼가
document.createElement로 요소 하나를 만들면 브라우저는 생각보다 많은 일을 한다.
- 01
DOM 트리를 수정한다
이것 자체는 그리 비싸지 않다.
- 02
레이아웃을 다시 계산한다
요소의 크기와 위치가 바뀌면 주변 요소들도 전부 다시 계산해야 한다. 리플로우라 부른다.
- 03
화면을 다시 칠한다
리페인트. 여기까지 마쳐야 사용자 눈에 보인다.
문제는 이걸 여러 번 반복할 때다.
for (let i = 0; i < 100; i++) {
list.appendChild(createItem(i));
}
레이아웃 계산이 백 번 일어날 수도 있다. 한 번에 모아서 하면 한 번이면 될 일이다.
가상 DOM — 자바스크립트 객체로 만든 사본
React는 실제 DOM을 바로 건드리지 않는다. 자바스크립트 객체로 화면 구조를 흉내 낸 것을 먼저 만든다.
3강에서 본 그것이다.
{
type: "ul",
props: {
children: [
{ type: "li", props: { children: "사과" } },
{ type: "li", props: { children: "포도" } }
]
}
}
이게 가상 DOM이다. 진짜 DOM 노드가 아니라 그냥 객체다. 만들고 버리는 게 싸다.
- 렌더새 가상 DOM 을 만든다
- 비교이전 것과 견준다
- 커밋달라진 부분만 실제 DOM 에 반영
이 비교 과정을 **재조정(reconciliation)**이라 부른다.
비교 규칙
두 트리를 완벽하게 비교하는 건 아주 비싸다. 그래서 React는 두 가지 가정을 두고 지름길을 택한다.
타입이 다르면
통째로 갈아엎는다
<div>→<span><Counter>→<Timer>- 안을 비교하지 않는다
- 상태도 전부 버린다
타입이 같으면
속성만 고친다
- DOM 노드를 재사용한다
- 달라진 속성만 갱신
- 자식들을 재귀적으로 비교
- 상태가 유지된다
첫 번째 규칙이 무섭다.
{isLoading ? <div><Chart /></div> : <span><Chart /></span>}
div가 span으로 바뀌는 순간 안의 Chart가 통째로 새로 만들어진다. 상태도, 스크롤 위치도 날아간다. 13강에서 본 "위치가 바뀌면 상태를 버린다"의 실체다.
자식들을 비교하는 방법
목록을 비교할 때가 까다롭다. React는 기본적으로 순서대로 견준다.
이전: <li>사과</li> <li>바나나</li>
이후: <li>포도</li> <li>사과</li> <li>바나나</li>
앞에 하나 추가됐을 뿐인데, 순서로 비교하면 이렇게 판단한다.
- 01
0번째: 사과 → 포도
텍스트가 다르다. 고친다.
- 02
1번째: 바나나 → 사과
또 고친다.
- 03
2번째: 없음 → 바나나
새로 만든다.
세 번 작업했다. 실제로는 하나만 앞에 넣으면 될 일이다.
key를 주면 React가 정체를 알아본다.
11강에서 key가 신분증이라고 했던 것이 여기서 쓰인다. 재조정 알고리즘이 key를 보고 무엇이 이동·추가·삭제됐는지 판단한다.
가상 DOM 은 빠르지 않다
가장 중요한 오해를 짚는다.
그럼 왜 쓰는가.
직접 조작
가장 빠르다
- 무엇을 고칠지 사람이 정한다
- 어디를 고칠지 다 기억해야 한다
- 빠뜨리면 화면이 어긋난다
- 1강의 그 문제
가상 DOM
충분히 빠르다
- 무엇을 고칠지 React 가 정한다
- 우리는 결과만 선언한다
- 어긋날 자리가 없다
- 충분히 빠르면 그걸로 됐다
가상 DOM은 속도를 위한 기술이 아니라 선언형을 가능하게 하는 기술이다. "화면이 이렇게 생겨야 한다"고만 쓰면, 거기 도달하는 최소한의 DOM 작업을 React가 알아서 찾아준다.
성능은 그 대가로 "감당할 만한 수준"을 확보하는 것이다. 손으로 최적화한 코드보다 느리지만, 손으로 최적화하다 버그를 내는 것보다 낫다.
실제로 무엇이 느려지나
React 앱이 느려지는 원인은 대개 가상 DOM이 아니다.
| 진짜 원인 | 해결 |
|---|---|
| 렌더 함수 안의 무거운 계산 | useMemo 로 캐싱 |
| 필요 없는 자식 리렌더링 | React.memo · 합성 |
| key 를 index 로 준 긴 목록 | 고유 id 를 준다 |
| 수천 개 항목을 한 번에 그림 | 가상 스크롤 |
| 렌더마다 새 객체·함수 생성 | useCallback · 상수로 빼기 |
13강에서 말했듯 재보고 고친다. React DevTools의 Profiler가 어느 컴포넌트가 몇 번, 얼마나 오래 렌더링됐는지 알려준다.
Fiber — 이름만 알아둔다
React 16부터 재조정 엔진이 Fiber로 바뀌었다. 핵심은 하나다.
비교 작업을 잘게 쪼개서 중간에 멈출 수 있게 만들었다. 긴 목록을 비교하는 도중에 사용자가 버튼을 누르면, 하던 일을 잠시 멈추고 클릭부터 처리한다.
자바스크립트 편 20강에서 "콜 스택은 한 번에 하나씩만 처리한다"고 했다. 무거운 렌더링이 스택을 오래 붙잡으면 화면이 얼어붙는다. Fiber는 그걸 막는다.
쓰는 입장에서 알아야 할 건 없다. 이름을 들으면 "재조정 엔진"이라고 떠올리면 충분하다.
정리하면
- React는 실제 DOM 대신 자바스크립트 객체로 된 사본을 먼저 만든다. 그게 가상 DOM이다.
- 이전 것과 새 것을 비교해 바뀐 부분만 실제 DOM에 반영한다. 이 과정이 재조정이다.
- 타입이 다르면 통째로 갈아엎고, 같으면 속성만 고친다. 갈아엎으면 상태도 사라진다.
- 목록은
key로 정체를 판단한다.key가 없으면 순서로 비교해 헛수고한다. - 가상 DOM은 직접 조작보다 느리다. 속도가 아니라 선언형을 가능하게 하는 기술이다.
- 실제로 느려지는 원인은 대개 다른 곳이다. 재보고 고친다.
Part 4가 끝났다. 다음 강의부터 마지막 Part, 훅이다. React 바깥 세상과 연결하는 useEffect부터 본다.