비교 알고리즘(React Diffing Algorithm)

Donggu(oo)·2023년 1월 20일
0

React

목록 보기
8/30
post-thumbnail

1. 비교 알고리즘(React Diffing Algorithm)


  • React는 두 가지의 가정을 가지고 시간 복잡도 O(n)의 새로운 휴리스틱 알고리즘(Heuristic Algorithm)을 구현해낸다.
  1. 서로 다른 두 요소는 다른 트리를 구축할 것이다.
  2. 개발자가 제공하는 key 프로퍼티를 가지고, 여러 번 렌더링을 거쳐도 변경되지 말아야 하는 자식 요소가 무엇인지 알아낼 수 있을 것이다.
  • 실제 이 두 가정은 거의 모든 실제 사용 사례에 들어맞게 되며, 여기서 React는 비교 알고리즘(Diffing Algorithm)을 사용한다.

1) React가 DOM 트리를 탐색하는 방법

  • React는 기존의 가상 DOM 트리와 새롭게 변경된 가상 DOM 트리를 비교할 때, 트리의 레벨 순서대로 순회하는 방식으로 탐색한다. 즉 같은 레벨(위치)끼리 비교한다는 뜻이며, 이는 너비 우선 탐색(BFS)의 일종이라고 볼 수 있다.

  • React는 이런 식으로 동일 선상에 있는 노드를 파악한 뒤 다음 자식 세대의 노드를 순차적으로 파악해 나간다.

1-1. 다른 타입의 DOM 엘리먼트인 경우

  • DOM 트리는 각 HTML 태그마다 각각의 규칙이 있어 그 아래 들어가는 자식 태그가 한정적이라는 특징이 있다. (예를 들어 <ul> 태그 밑에는 <li> 태그만 와야 한다던가, <p> 태그 안에 <p> 태그를 또 쓰지 못하는 것이다.)

  • 자식 태그의 부모 태그 또한 정해져 있다는 특징이 있기 때문에, 부모 태그가 달라진다면 React는 이전 트리를 버리고 새로운 트리를 구축한다.

<div>
	<Counter />
</div>

//부모 태그가 div에서 span으로 바뀐다.
<span>
	<Counter />
</span>
  • 이렇게 부모 태그가 바뀌어버리면, React는 기존의 트리를 버리고 새로운 트리를 구축하기 때문에 이전의 DOM 노드들은 전부 파괴된다. 부모 노드였던 <div><span>으로 바뀌어버리면 자식 노드인 <Counter />는 완전히 해제된다.

  • 즉 이전 <div> 태그 속 <Counter />는 파괴되고 <span> 태그 속 새로운 <Counter />가 다시 실행된다. 새로운 컴포넌트가 실행되면서 기존의 컴포넌트는 완전히 해제(Unmount)되어버리기 때문에 <Counter />가 가지고 있던 기존의 state 또한 파괴된다.

1-2. 같은 타입의 DOM 엘리먼트인 경우

  • 반대로 타입이 바뀌지 않는다면 React는 최대한 렌더링을 하지 않는 방향으로 최소한의 변경 사항만 업데이트한다. 이것이 가능한 이유는 앞서 React가 실제 DOM이 아닌 가상 DOM을 사용해 조작하기 때문이다.

  • 업데이트 할 내용이 생기면 virtual DOM 내부의 프로퍼티만 수정한 뒤, 모든 노드에 걸친 업데이트가 끝나면 그때 단 한번 실제 DOM으로의 렌더링을 시도한다.

  • React는 두 요소를 비교했을 때 className만 수정되고 있다는 것을 알게 된다. 그리고 className before와 after는 각자의 스타일을 가지고 있다.

<!-- className이 before인 컴포넌트 -->
<div style={{color: 'red', fontWeight: 'bold"}} title="stuff" />

<!-- className이 after인 컴포넌트 -->
<div style={{color: 'green', fontWeight: 'bold"}} title="stuff" />
  • 두 엘리먼트를 비교했을 때 React는 정확히 color 스타일만 바뀌고 있음을 확인한다. 따라서 React는 color 스타일만 수정하고 fontWeight 및 다른 요소는 수정하지 않는다.

  • 이렇게 하나의 DOM 노드를 처리한 뒤 React는 뒤이어서 해당 노드들 밑의 자식들을 순차적으로 동시에 순회하면서 차이가 발견될 때마다 변경한다. 이를 재귀적으로 처리한다고 표현한다.

1-3. 자식 엘리먼트의 재귀적 처리

  • React는 기존 <ul>과 새로운 <ul>을 비교할 때 자식 노드를 순차적으로 위에서부터 아래로 비교하면서 바뀐 점을 찾는다. 그렇기 때문에 예상대로 React는 첫 번째 자식 노드들과 두 번째 자식 노드들이 일치하는 걸 확인한 뒤 세 번째 자식 노드를 추가한다.

  • 이렇게 React는 위에서 아래로 순차적으로 비교하기 때문에, 이 동작 방식에 대해 고민하지 않고 리스트의 처음에 엘리먼트를 삽입하게 되면 이전의 코드에 비해 훨씬 나쁜 성능을 내게 된다.

<!-- 나쁜 예 -->
<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

//자식 엘리먼트를 처음에 추가한다.
<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<!-- 올바른 예 -->
<ul>
  <li>first</li>
  <li>second</li>
</ul>

//자식 엘리먼트의 끝에 새로운 자식 엘리먼트를 추가했다.
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>
  • 처음의 자식 노드를 비교할 때, <li>Duke</li><li>Connecticut</li>로 자식 노드가 서로 다르다고 인지하게 된 React는 리스트 전체가 바뀌었다고 받아들인다. 즉 <li>Duke</li><li>Villanova</li>는 그대로기 때문에 두 자식 노드는 유지시켜도 된다는 것을 깨닫지 못하고 전부 버리고 새롭게 렌더링 해버리기 때문에 이는 굉장히 비효율적인 동작 방식이다.

  • 그래서 React는 이 문제를 해결하기 위해 key라는 속성을 지원한다. 이는 효율적으로 가상 DOM을 조작할 수 있도록 하며, key 속성을 사용하지 않으면 React 에서 key값을 달라고 경고문을 띄우는 것도 이 때문이다. key값이 없는 노드는 비효율적으로 동작할 수 있기 때문이다.

0개의 댓글