Virtual DOM & React Diffing Algorithm

이유정·2022년 11월 25일
0

코드스테이츠 TIL

목록 보기
52/62

개요

React 심화

React에는 Virtual DOM이라는 가상의 DOM 객체가 있다.
실제 DOM객체에 접근하여 조작하는가? (X)
가상의 DOM 객체에 접근하여 변화 전과 변화 후를 비교하고 바뀐 부분만 적용하는가? (O)

Virtual DOM

Virtual dom이 나오게 된 배경

Virtual DOM은 Real DOM의 가벼운 사본과 같다. (Real DOM은 우리가 그동안 배워온 DOM과 같다.)

Real DOM (DOM)

Document Object Model 의 약자.
문서객체 모델

  • 문서 객체란 브라우저가 js와 같은 스크립팅 언어가 태그들에 접근하고 조작할 수 있도록 태그들을 트리 구조로 객체화 시킨것
  • 즉, DOM은 브라우저가 트리 구조로 만든 객체 모델이다.
  • 트리구조로 DOM 객체가 이뤄져 있게 때문에 쉽게 DOM 객체에 접근, 조작 가능하다.
  • UI가 변경될 때마다 DOM은 업데이트를 한다.
  • DOM을 조작하는 경우가 많아지면 성능에 영향을 미치고,
  • DOM의 렌더링은 브라우저의 구동 능력에 의존하기 때문에 조작속도가 느려진다.

DOM의 조작 속도가 느려지는 이유

DOM의 구조는 계층적 구조인 트리구조다.
트리구조는 데이터의 저장 보다는 "저장된 데이터를 더 효과적으로 탐색"을 위해 사용된다. 따라서 빠른 자료 탐색 성능이 장점이다.
그러나,
DOM이 업데이트 된다는 것은 브라우저 렌더링 엔진이 리플로우 한다는 것이다.
즉, 업데이트된 요소와 그 자식 요소들에 의해 DOM 트리를 재구축, 재렌더링, UI를 업데이트한다. 리플로우와 리페인트 과정은 레이아웃 및 페인트에 해당하는 재연산을 하기 때문에 속도가 그만큼 느려진다.
DOM 조작은 모던 웹의 인터랙티브 요소에 빠질 수 없지만, JS프레임워크는 필요이상으로 DOM을 업데이트 시킨다. 계속해서 비효율적인 업데이트를 반복하면 극단적으로 프레임 드랍과 같은 치명적인 UX가 발생할 수 있다.

바뀐 부분만 비교해서 그 부분만 렌더링할 수 없을까? 라는 질문의 아이디어로 react는 Virtual DOM이 탄생!

Virtual DOM 이란?

  • 모든 DOM 객체에 대응하는 가상의 DOM 객체가 있다.
  • 실제 DOM 객체와 동일한 속성을 가지고 있지만 훨씬 가벼운 사본이다.
  • 가상 DOM 객체는 화면에 표시되는 내용을 직접 변경하는 것은 아니다.
  • 가상의 UI 요소를 메모리에 유지시키고, 그 유지시킨 가상의 UI 요소를 React DOM과 같은 라이브러리를 통해 실제 DOM과 동기화시킨다.
  • 실제로 브라우저 화면에 그리는 것이 아니기 때문에 훨씬 속도가 빠르다.

어떻게 가상 DOM이 더 빠를까?

React는 새로운 요소가 UI에 추가되면 ?

=> 트리 구조로 표현이 되는 가상의 DOM이 만들어진다.
=> 이러한 요소의 상태가 변경이 되면 다시 새로운 가상의 DOM 트리가 만들어진다.
=> 그리고 이전의 가상의 DOM과 이후의 가상의 DOM의 차이를 비교
=> 가상의 DOM은 실제 DOM에 변경을 수행할 수 있는 최상의 방법을 계산
=> 실제 DOM은 최소한의 작업만 수행해도 렌더링을 할 수 있다.
=> 실제 DOM의 업데이트 비용을 줄일 수 있다.
=> 브라우저의 파워를 덜 쓴다는 의미이므로, 더 빠른 렌더링이 가능해진다.

Virtual DOM의 형태

const vDom = {
	tagName: "html",
	children: [
		{ tagName: "head" },
		{ tagName: "body",
			children: [
				tagName: "ul",
				attributes: { "class": "list"},
				children: [
					{
						tagName: "li",
						attributes: { "class": "list_item" },
						textContent: "List item"
					}
				]
			]
		}
	]
}

가상 DOM 또한 HTML 문서 객체 기반으로 한다.
추상화가 된거지만 평범한 자바스크립트 객체이므로, 실제 DOM을 건드리지 않고도 자유롭게 조작가능하다.

React Diffing Algorithm

React가 기존 가상 dom과 변경된 새로운 가상 dom을 비교하여 기존의 ui를 효율적으로 갱신하는 방법을 알아내야 한다.

하나의 트리를 다른 트리로 변형시키는 조작방식은 O(n^3) 복잡도를 가지는 알고리즘이다.
너무 비싼 연산이기 때문에,
두 가지 가정을 가지고 시간 복잡도의 새로운 휴리스틱 알고리즘을 구현해낸다.
1. 두 요소는 각기 다른 트리를 구축할 것이다.
2. 개발자가 제공하는 key를 가지고, 여러 번 렌더링을 거쳐도 변경되지 말아야 하는 자식 요소를 알아낼 수 있을 것이다.
이때, 비교 알고리즘 (Diffing Algorithm) 사용

React DOM 트리 탐색하는 방법

React는 기존의 가상 dom과 새롭게 변경된 가상dom 트리를 비교할 때 트리의 레벨 순서대로 순회하는 방식으로 탐색한다. 같은 레벨끼리 비교한다는 뜻이고, 너비 우선 탐색의 일종이기도 하다.

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

다른 타입의 dom 엘리먼트인 경우

DOM 트리는 각 HTML 태그마다 각각의 규칙이 있어 그 아래 들어가는 자식 태그가 한정적이다. (예를 들어 ul 태그 밑에는 li 태그만 와야 한다던가, p 태그 안에 p 태그를 또 쓰지 못하는 것) 자식 태그의 부모 태그 또한 정해져 있다는 특징이 있기 때문에, 부모 태그가 달라진다면 React는 이전 트리를 버리고 새로운 트리를 구축한다.
이렇게 부모 태그가 바뀌어버리면, React는 기존의 트리를 버리고 새로운 트리를 구축하기 때문에 이전의 DOM 노드들은 전부 파괴된다.
부모 노드였던 div가 span으로 바뀌어버리면 자식 노드인 Counter 는 완전히 해제됩니다. 즉 이전 div 태그 속 Counter는 파괴되고 span태그 속 새로운 Counter가 다시 실행된다.
=> 새로운 컴포넌트가 실행되면서 기존의 컴포넌트는 완전히 해제(Unmount)되어버리기 때문에 Counter가 갖고 있던 기존의 state 또한 파괴된다.

  <div>
	<Counter />
  </div>

//부모 태그가 div에서 span으로 바뀝니다.
<span>
	<Counter />
</span>

같은 타입의 dom 엘리먼트인 경우

React는 최대한 렌더링을 하지 않는 방향으로 최소한의 변경 사항만 업데이트한다.
이것이 가능한 이유는 앞서 React가 실제 DOM이 아닌 가상 DOM을 사용해 조작하기 때문이다. 업데이트 할 내용이 생기면 virtual DOM 내부의 프로퍼티만 수정한 뒤, 모든 노드에 걸친 업데이트가 끝나면 그때 단 한번 실제 DOM으로의 렌더링을 시도한다.

<div className="before" title="stuff" />

//기존의 엘리먼트가 태그는 바뀌지 않은 채 className만 바뀌었습니다.
<div className="after" title="stuff" />
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 스타일만 바뀌고 있음을 눈치채고, color 스타일만 수정하고 fontWeight 및 다른 요소는 수정하지 않는다. 이렇게 하나의 DOM 노드를 처리한 뒤 React는 뒤이어서 해당 노드들 밑의 자식들을 순차적으로 동시에 순회하면서 차이가 발견될 때마다 변경한다.
이를 재귀적으로 처리한다고 표현

자식 엘리먼트의 재귀적 처리

자식 엘리먼트가 변경이 됐을 때)

<ul>
  <li>first</li>
  <li>second</li>
</ul>

//자식 엘리먼트의 끝에 새로운 자식 엘리먼트를 추가했습니다.
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

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

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

<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>

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

이렇게 구현하게 되면 React는 최소한으로 동작하지 못하게 된다. React는 원래의 동작하던 방식대로 처음의 노드들을 비교하게 된다.

처음의 자식 노드를 비교할 때,

<li>Duke</li><li>Connecticut</li>

로 자식 노드가 서로 다르다고 인지하게 된 React는 리스트 전체가 바뀌었다고 받아들인다. 즉 Duke와 Villanova는 그대로기 때문에 두 자식 노드는 유지시켜도 된다는 것을 깨닫지 못하고 전부 버리고 새롭게 렌더링한다. 굉장히 비효율적인 동작 방식!

=> 이 문제를 해결하기 위해 key라는 속성을 지원
이는 효율적으로 가상 DOM을 조작할 수 있도록 한다. 만일 개발할 당시 key라는 속성을 사용하지 않으면 React 에서 key값을 달라고 경고문을 띄우는 것도 이 때문이다.

키(key)

만약 자식 노드들이 key를 갖고 있다면, React는 그 key를 이용해 기존 트리의 자식과 새로운 트리의 자식이 일치하는지 아닌지 확인할 수 있다.

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

//key가 2014인 자식 엘리먼트를 처음에 추가합니다.
<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

React는 key 속성을 통해 ‘2014’라는 자식 엘리먼트가 새롭게 생겼고, ‘2015’, ‘2016’ 키를 가진 엘리먼트는 그저 위치만 이동했다는 걸 알게 된다.
따라서 React는 기존의 동작 방식대로 다른 자식 엘리먼트는 변경하지 않고 추가된 엘리먼트만 변경한다.
이 key 속성에는 보통 데이터 베이스 상의 유니크한 값(ex. Id)을 부여해주면 됩니다. (키는 전역적으로 유일할 필요는 없고, 형제 엘리먼트 사이에서만 유일하면 됨)

만약 이런 유니크한 값이 없다면 최후의 수단으로 배열의 인덱스를 key로 사용할 수 있습니다. (다만 배열이 다르게 정렬될 경우가 생긴다면 배열의 인덱스를 key로 선택했을 경우는 비효율적으로 동작한다. 왜냐하면 배열이 다르게 정렬되어도 인덱스는 그대로 유지되기 때문. 인덱스는 그대로지만 그 요소가 바뀌어버린다면 React는 배열의 전체가 바뀌었다고 받아들이고, 기존의 DOM 트리를 버리고 새로운 DOM 트리를 구축한다.)

profile
팀에 기여하고, 개발자 생태계에 기여하는 엔지니어로

0개의 댓글