Real DOM (DOM)
DOM(Document Object Model)은 뜻을 그대로 풀자면 문서 객체 모델을 의미한다.
여기서 문서 객체란 브라우저가 javascript와 같은 스크립팅 언어가 , , 와 같은 태그들에 접근하고 조작할 수 있도록 문서를 트리 구조로 객체화한 것을 의미한다. 다시 말하자면 DOM은 브라우저가 HTML 문서를 조작할 수 있도록 트리 구조화한 객체 모델이다.
따라서 이미지의 왼쪽과 같은 HTML 문서가 있을 때 이를 브라우저에서 읽는다면 이미지의 오른쪽과 같은 DOM이 생성된다. 그리고 querySelector, addEventListener와 같은 DOM API 를 통해 문서의 요소들을 조작할 수 있다.
DOM은 계층적 형태의 트리 구조로 구성되어 있다.
자료구조 중에서도 특히 트리는 "데이터 저장"의 의미보다는 "저장된 데이터를 더 효과적으로 탐색" 하기 위해 사용되므로 빠른 자료 탐색 성능이 장점인 자료구조이다
DOM이 변경되고 업데이트가 된다는 것은 결국 브라우저의 렌더링 엔진 또한 리플로우(Reflow)한다는 것을 의미함.
브라우저는 렌더링 과정에서 DOM 트리와 CSSOM 트리를 토대로 Render 트리를 생성하고 각 요소가 배치될 공간을 계산(Layout)한 뒤 이를 화면에 그려낸다(Paint).
DOM이 변경된다면 업데이트된 요소와 그에 해당하는 자식 요소들에 의해 DOM 트리를 재구축하게 된다.
그 과정에서 이에 대한 레이아웃 재연산을 수행하는 리플로우, 그리고 이를 화면에 그려내는 Repaint 과정을 거치게 된다. 이때 변화가 필요 없는 부분은 변경되면서 잦은 리플로우 발생으로 인해 성능을 떨어뜨리는 문제를 야기하게 된다.
따라서 JavaScript를 통한 DOM 조작이 많아질수록 이에 대한 리플로우가 발생하므로 DOM 업데이트 비용이 커질 수 있다.
실제로 DOM 조작시 변화가 필요하지 않은 요소도 변경될 수 있다.
=> 결국, "바뀐 부분만 비교해서 그 부분만 렌더링할 수는 없을까?" 라는 아이디어를 기반으로 React의 Virtual DOM이 등장하게 된다.
가상 DOM은 실제 DOM 과 동기화되어, 상태가 변경될 때 마다 가상 DOM을 새로 생성하여 이전 상태와 비교한다. 그리고 변경이 필요한 부분만 실제 DOM에 반영하여 업데이트하므로 전체 UI를 다시 그리지 않아도 된다.
React에는 DOM 객체에 대응하는 가상의 DOM 객체가 있다.
상대적으로 무거운 DOM 에 비하여 React의 가상 DOM 객체는 자바스크립트 객체로 이루어져 있기 때문에 실제 DOM 객체와 동일한 속성을 가지고 있음에도 "훨씬 가벼운 사본" 이라고 표현할 수 있다.
가상 DOM 객체는 말그대로 가사상이기 때문에 비교를 위해 사용되며, 실제 DOM 객체처럼 표시되는 내용을 직접 변경하는 것은 아니다.
이 DOM 트리는 다음과 같이 JavaScript 객체로도 표현할 수 있습니다.
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"
}
]
]
}
]
}
리액트는 상태를 변경하는 작업(e.g 이벤트) 이 일어났을 때, 가상 DOM에 저장된 이전 상태와 변경된 상태를 비교한다.
React 는 Diffing 알고리즘을 사용하여 변경된 부분을 감지한다. 따라서 React에서 상태를 변경하는 경우에는 Diffing 알고리즘에서 이를 감지할 수 있도록 직접 할당이 아닌 setState 와 같은 메서드를 활용해 상태를 변경한다.
가상 DOM과 변경된 새로운 가상 DOM을 비교하여 변경이 필요한 부분만 실제 DOM에 반영하여 업데이트한다. 이것을 Reconciliation
즉 재조정
이라고 한다.
이 과정에서 여러 개의 상태 변화가 있을 경우 이를 일일이 수행하지 않고 일괄적으로 한번에 업데이트(Batch Update)한다. 이를 통해 성능을 최적화하고 불필요한 리렌더링을 최소화 할 수 있다.
React가 기존 가상 DOM과 변경된 새로운 가상 DOM을 비교했을 때, React 입장에서는 변경된 새로운 가상 DOM 트리에 부합하도록 기존의 UI를 율적으로 갱신하는 방법을 알아내야만 했는데, 알아낸 조작 방식 알고리즘은 O(n^3) 의 복잡도를 가지고 있다.
알고리즘 그대로 React에 적용한다면 1000개의 엘리먼트를 실제 화면에 표시하기 위해 10djr(1000^3)번의 비교 연산을 해야만 한다.
이것은 너무 비싼 연산이고 React는 두 가지 가정을 가지고 시간 복잡도O(n)의 새로운 휴리스틱 알고리즘을 구현해낸다.
두가지 가정 이러하다
각기 서로 다른 두 요소는 다른 트리를 구축할 것이다.
개발자가 제공하는 key 프로퍼티를 가지고, 여러번 렌더링을 거쳐도 변경되지 말아야 하는 자식 요소가 무엇인지 알아낼수 있다.
실제 이 두가정은 거의 모든 실제 사용 사례애 들어맞게 된다.
React 는 기존의 가상 DOM 트리와 새롭게 변경된 가상 DOM 트리를 비교할 때, 트리의 레벨 순서대로 순회하는 방식으로 탐색한다. 즉 같은 레벨(위치)끼리 비교한다는 뜻.(이는 너비 우선 탐색(BFS)의 일종)
[그림] React의 DOM 트리 순회 방식
React는 이런 식으로 동일 선산에 있는 노드를 파악한 뒤 다음 자식 세대의 노드를 순차적으로 파악해 나간다.
태그 안에
태그를 또 쓰지 못하는 것입니다.) 자식 태그의 부모 태그 또한 정해져 있다는 특징이 있기 때문에, 부모 태그가 달라진다면 React는 이전 트리를 버리고 새로운 트리를 구축해버립니다.
<div>
<Counter />
</div>
//부모 태그가 div에서 span으로 바뀝니다.
<span>
<Counter />
</span>
<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 스타일만 바뀌고 있음을 눈치챕니다. 따라서 React는 color 스타일만 수정하고 fontWeight 및 다른 요소는 수정하지 않습니다. 이렇게 하나의 DOM 노드를 처리한 뒤 React는 뒤이어서 해당 노드들 밑의 자식들을 순차적으로 동시에 순회하면서 차이가 발견될 때마다 변경합니다. 이를 재귀적으로 처리
한다고 표현합니다.
예를 들면 이렇게 자식 엘리먼트가 변경이 된다고 가정하겠습니다.
<ul>
<li>first</li>
<li>second</li>
</ul>
//자식 엘리먼트의 끝에 새로운 자식 엘리먼트를 추가했습니다.
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
React는 기존
이렇게 React는 위에서 아래로 순차적으로 비교하기 때문에, 이 동작 방식에 대해 고민하지 않고 리스트의 처음에 엘리먼트를 삽입하게 되면 이전의 코드에 비해 훨씬 나쁜 성능을 내게 됩니다.
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
//자식 엘리먼트를 처음에 추가합니다.
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
처음의 자식 노드를 비교할 때,
그래서 React는 이 문제를 해결하기 위해 key라는 속성을 지원합니다. 이는 효율적으로 가상 DOM을 조작할 수 있도록 합니다. 만일 개발할 당시 key라는 속성을 사용하지 않으면 React 에서 key값을 달라고 경고문을 띄우는 것도 이 때문인 것입니다. 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>