학습 자료

가상 DOM과 재조정


Article

1강에서 "React가 바뀐 곳만 고친다"고 하고 넘어갔다. 13강에서 그 일이 커밋 단계에서 일어난다고 했다.

이번 강의는 그 사이에서 무슨 일이 벌어지는지 본다.

DOM 조작은 왜 비싼가

document.createElement로 요소 하나를 만들면 브라우저는 생각보다 많은 일을 한다.

DOM 을 하나 고치면
  1. 01

    DOM 트리를 수정한다

    이것 자체는 그리 비싸지 않다.

  2. 02

    레이아웃을 다시 계산한다

    요소의 크기와 위치가 바뀌면 주변 요소들도 전부 다시 계산해야 한다. 리플로우라 부른다.

  3. 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 노드가 아니라 그냥 객체다. 만들고 버리는 게 싸다.

React 가 화면을 갱신하는 길
  1. 렌더새 가상 DOM 을 만든다
  2. 비교이전 것과 견준다
  3. 커밋달라진 부분만 실제 DOM 에 반영

이 비교 과정을 **재조정(reconciliation)**이라 부른다.

비교 규칙

두 트리를 완벽하게 비교하는 건 아주 비싸다. 그래서 React는 두 가지 가정을 두고 지름길을 택한다.

React 의 두 가지 가정

타입이 다르면

통째로 갈아엎는다

  • <div><span>
  • <Counter><Timer>
  • 안을 비교하지 않는다
  • 상태도 전부 버린다

타입이 같으면

속성만 고친다

  • DOM 노드를 재사용한다
  • 달라진 속성만 갱신
  • 자식들을 재귀적으로 비교
  • 상태가 유지된다

첫 번째 규칙이 무섭다.

{isLoading ? <div><Chart /></div> : <span><Chart /></span>}

divspan으로 바뀌는 순간 안의 Chart통째로 새로 만들어진다. 상태도, 스크롤 위치도 날아간다. 13강에서 본 "위치가 바뀌면 상태를 버린다"의 실체다.

자식들을 비교하는 방법

목록을 비교할 때가 까다롭다. React는 기본적으로 순서대로 견준다.

이전:  <li>사과</li>  <li>바나나</li>
이후:  <li>포도</li>  <li>사과</li>  <li>바나나</li>

앞에 하나 추가됐을 뿐인데, 순서로 비교하면 이렇게 판단한다.

key 없이 비교하면
  1. 01

    0번째: 사과 → 포도

    텍스트가 다르다. 고친다.

  2. 02

    1번째: 바나나 → 사과

    또 고친다.

  3. 03

    2번째: 없음 → 바나나

    새로 만든다.

세 번 작업했다. 실제로는 하나만 앞에 넣으면 될 일이다.

key를 주면 React가 정체를 알아본다.

3개짜리 목록 앞에 하나 추가할 때 DOM 작업 수
key 없음3회
key 있음1회

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부터 본다.

가상 DOM과 재조정 — 디코드랩(DCODELAB)