Virtual DOM (VDOM)은 UI의 이상적인 또는 “가상"적인 표현을 메모리에 저장하고 ReactDOM과 같은 라이브러리에 의해 “실제" DOM과 동기화하는 프로그래밍 개념이다.
라고 React 공식문서에 적혀있다. 그리고 이러한 과정을 재조정이라고한다.
또한 virtual DOM은 브라우저 API 위에 있는 JavaScript 라이브러리에서 구현되는 개념이다.
이러한 접근방식이 React의 선언적 API를 가능하게 한다.
React에게 원하는 UI의 상태를 알려주면 DOM이 그 상태와 일치하도록한다. 이러한 방식은 앱 구축에 사용해야 하는 어트리뷰트 조작, 이벤트 처리, 수동 DOM 업데이트를 추상화한다.
명령형 프로그래밍에서는 “어떻게 할 것인지를 설명"한다. 그에 반해 선언형 프로그래밍에서는 “무엇을 할 것인가를 정의"한다. UI를 조작할 때, 명령형에서는 어떤 상황이 발생할 때 UI를 조작하려면 상황에 대처할 정확한 지침을 작성해야한다.
이러한 방식은 작은 프로젝트에서 충분히 잘 작동하지만 복잡한 시스템에서는 관리하기가 기하급수적으로 어려워진다. 새로운 UI 요소나 새로운 상호작용을 추가하려면 버그가 발생하지 않았는지 모든 기존 코드를 주의 깊게 확인해야한다.
실제로 나는 우아한테크코스 레벨1의 "나만의 유튜브 강의실" 미션을 진행하면서 상태관리의 어려움을 느꼈다. 동영상 저장버튼을 누르면 이전 코드들에서 상태를 어떻게 바꿨는지를 다시 살펴보고 모든 UI 요소에게 새로운 상태에 어떻게 변화할지 명령해줬어야하는데 예상치 못한 버그들이 많이 생겼었다.
이 문제를 해결하기 위해 React가 만들어졌다. React에서는 UI를 직접 조작하지 않는다. 즉, 구성 요소를 직접 활성화, 비활성화, 표시 또는 숨기지 않는다. 대신 표시하려는 것을 선언하면 React가 UI를 업데이트하는 방법을 알아낸다.
UI에 대해 선언적인 코드를 작성하는 방법은 아래링크를 참고하자.
위에서 Virtual DOM
이 무엇인지, 이를 통해 어떻게, 왜 더 관리하기 쉬운 코드를 작성할 수 있는지 알아보았다. 그러나 우리는 React 내부에서 어떤 일이 일어나는지 모른다. 이제 이에 대해 알아보자.
React는 선언적 API를 제공하기 때문에 갱신이 될 때마다 매번 무엇이 바뀌었는지를 걱정할 필요가 없다. 이는 애플리케이션 작성을 무척 쉽게 만들어주지만, React 내부에서 어떤 일이 일어나고 있는지는 명확히 눈에 보이지 않는다.
React의 “비교 (diffing)” 알고리즘이 어떻게 동작하는지 알아보자. 이 비교 알고리즘 덕분에 컴포넌트의 갱신이 예측 가능해지면서도 고성능 앱이라고 불러도 손색없을 만큼 충분히 빠른 앱을 만들 수 있다.
React를 사용할 때, state
나 props
가 갱신되면 컴포넌트는 새로운 React 엘리먼트 트리를 반환한다. 이때 React는 방금 만들어진 트리에 맞게 가장 효과적으로 UI를 갱신하는 방법을 알아내야한다.
React는 두 가지 가정을 기반하여 O(n) 복잡도의 휴리스틱 알고리즘을 구현했다.
key
prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할 지 표시해 줄 수 있다.두 개의 트리를 비교할 때, React는 두 엘리먼트의 루트(root) 엘리먼트부터 비교한다.
이후의 동작은 엘리먼트의 타입에 따라 달라진다.
두 “루트 엘리먼트"의 타입이 다르면, React는 이전 트리를 버리고 새로운 트리를 구축한다.
<a>
→ <img>
, <div>
-> <span>
로 바뀌는 경우 등이 트리 전체를 재구축하는 경우이다. 트리를 버릴 때 이전 DOM 노드들은 모두 파괴되고, 루트 엘리먼트 아래의 모든 컴포넌트도 언마운트되고 그 state도 사라진다.
<div>
<Counter />
</div>
<span>
<Counter />
</span>
위 아래를 비교하게 된다면 이전의 <Counter>
트리는 버려지고 새로 그려지게 될 것이다.
같은 타입은 두 React DOM 엘리먼트를 비교할 때, React는 두 엘리먼트의 속성을 확인하여, 동일한 내역은 유지하고 변경된 속성들만 갱신한다.
<div className="before" title="stuff" />
<div className="after" title="stuff" />
이 두 엘리먼트를 비교하면, React는 현재 DOM 노드 상에 className
만 수정한다.
DOM 노드의 처리가 끝나면, React는 이어서 해당 노드의 자식들을 재귀적으로 처리한다.
컴포넌트가 갱신되면 인스턴스는 동일하게 유지되어 렌더링 간 state가 유지된다. React는 새로운 엘리먼트의 내용을 반영하기 위해 현재 컴포넌트 인스턴스의 props를 갱신한다.
DOM 노드의 자식들을 재귀적으로 처리할 때, React는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성한다. 아래의 예시를 보자.
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
React는 두 트리에서 <li>first</li>
가 일치하는 것을 확인하고, <li>second</li>
가 일치하는 것을 확인합니다. 그리고 마지막으로 <li>third</li>
를 트리에 추가한다.
하지만 위와 같이 단순하게 구현하면, 리스트의 맨 앞에 엘리먼트를 추가하는 경우 성능이 좋지 않다. 예를 들어, 아래의 두 트리 변환은 형편없이 작동한다.
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
React는 <li>Duke</li>
와 <li>Villanova</li>
종속 트리를 그대로 유지하는 대신 모든 자식을 변경한다. 이러한 비효율은 문제가 될 수 있다.
이러한 문제를 해결하기 위해, React는 key
속성을 지원한다. 자식들이 key를 가지고 있다면, React는 key를 통해 기존 트리와 이후 트리의 자식들이 일치하는지 확인한다.
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
이제 React는 '2014'
key를 가진 엘리먼트가 새로 추가되었고, '2015'
와 '2016'
key를 가진 엘리먼트는 그저 이동만 하면 되는 것을 알 수 있다. 해당 key는 오로지 형제 사이에서만 유일하면 되고, 전역에서 유일할 필요는 없다.
인덱스를 key로 사용하는 것는 항목의 순서가 바뀌었을 때
의도하지 않은 방식으로 바뀔 수 있어 사용하지 않는 것을 추천한다.
Fiber는 React 16의 새로운 재조정 엔진이다.
기존의 React가 가지고 있는 재조정 과정의 문제를 해결하기위해 등장했는데,
이는 Fiber를 주제로 다루는 글에서 다시 살펴보도록 하자.
VirtualDOM이란 UI의 가상적인 표현을 메모리에 저장하고, 이를 ReactDOM
과 같은 라이브러리를 통해 실제 DOM
과 동기화하는 프로그래밍 개념이다.
"UI의 가상적인 표현의 저장 -> 실제 DOM과 동기화" 과정을 재조정(Reconciliation)이라한다.
비교 알고리즘 (Diffing Algorithm)은 root element
부터 재귀적으로 트리를 비교한다.
Fiber는 React 16 이전의 재조정 과정의 문제를 해결하기 위해 등장한 새로운 재조정 엔진이다.