Real DOM은 우리가 알고 있는 DOM과 같은 의미이며, Virtual DOM과 구분하기 위해 DOM을 Real DOM이라도 부른다.
DOM은 Document Object Model의 약자로, 브라우저가 트리 구조로 만든 문서 객체 모델이다.
<html>
, <head>
, <body>
태그에 접근하거나 조작할 수 있도록 태그들을 트리 구조로 객체화 시킨 것이다.위와 같이 DOM 객체는 트리구조로 되어 있기 때문에 JS가 쉽게 DOM 객체에 접근하거나 조작할 수 있다.
자료구조에서 트리는 저장된 데이터를 더 효과적으로 탐색
하기 위해 사용된다. 때문에 DOM은 JS가 쉽게 DOM 객체에 접근&탐색하는 속도가 빠르고, 업데이트 속도도 빠르다.
DOM의 조작 속도가 느려지는 이유:
DOM이 변경되고 업데이트가 되면 브라우저 렌더링 엔진 또한 변경 사항을 나타내기 위해 Reflow, Repaint가 된다. 업데이트된 요소와 그에 해당하는 자식 요소들이 DOM 트리로 재구축된 후 리렌더링 과정을 거쳐 업데이트가 되면, 브라우저는 다시 레이아웃 및 페인트에 대한 재연산을 해야되기 때문에 속도가 느려진다. 즉, DOM의 렌더링은 브라우저의 구동 능력에 의존하기 때문에 DOM의 조작 속도는 느려지게 되는 것이다. DOM을 조작하는 정도가 잦다면 성능에 영향을 미칠 수 있다.
DOM을 추상화한 가상의 객체로, Virtual DOM은 Real 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"
}
]
]
}
]
}
Diffing Algorithm, 비교 알고리즘이란, React에서 두 개의 트리를 비교할 때 두 엘리먼트의
루트 엘리먼트
부터 비교한 후,루트 엘리먼트 타입에 따라 트리를 구축
하는 방식의 알고리즘이다.
- 엘리먼트 타입이 다른 경우:
- 이전 트리를 버리고 완전히 새로운 트리를 구축한다.
- 제거된 엘리먼트의 하위 엘리먼트 state도 사라진다. 부모가 사라지면 자식들도 함께 사라진다는 뜻.
<!-- 타입이 다르기 때문에 div는 제거되고 span과 하위 요소인 Counter를 사용 --> <div> <Counter /> </div> <span> <Counter /> </span>
- DOM의 엘리먼트 타입이 같고 속성이 다른 경우:
- 두 엘리먼트 속성을 확인하여 동일한 내역은 유지하고 변경된 속성만 갱신한다.
<!-- 변경된 속성은 className이므로, className만 갱신 --> <div className="before" title="stuff" /> <div className="after" title="stuff" /> <!-- style 속성도 변경사항인 color만 갱신 --> <div style={{color: 'red', fontWeight: 'bold'}} /> <div style={{color: 'green', fontWeight: 'bold'}} />
- 자식 요소가 바뀌는 경우:
- DOM 노드의 처리가 끝나면, 해당 노드의 자식들을 재귀적으로 처리한다.
- 첫번째 자식부터 비교하며 변경하기 때문에, 첫 요소가 변경되면 처음부터 비교를 해야하는 문제를 가지며, 이는 성능저하를 야기한다. 이 문제는 key porp을 통해 해결할 수 있다. key에 관한 건 아래에 자세히 메모.
<!-- 이전 트리 --> <ul> <li>first</li> <li>second</li> </ul> <!-- 새로운 트리 --> <!-- first, second 요소가 같지만, 첫 번째부터 새로운 요소가 추가 되었기 때문에 첫 번째부터 트리를 만든다. --> <ul> <li>third</li> // 첫 번째에 새로운 요소 추가 <li>first</li> <li>second</li> </ul>
기존의 DOM 트리를 새로운 트리로 변환하기 위하여 최소한의 연산을 하는 알고리즘을 사용한다. 이때 알아낸 조작 방식은 알고리즘 O(n^3) 의 복잡도를 가지고 있다. 만약,이 알고리즘을 React에 적용한다면, 1000개의 엘리먼트가 있다는 가정하에 실제 화면에 표시하기 위해 1000^3인
10억번의 비교 연산
을 해야한다. 이는 너무비싼 연산
이기에 React는 두 가지 가정을 가지고 시간 복잡도 O(n)의 새로운 Heuristic Algorithm을 구현했다.
1. Two elements of differnt types will produce different trees.
: 각기 서로 다른 타입을 가진 두 요소는 다른 트리를 구축한다.
2. The developer can hit at which child elements may be stable across different renders with a key prop.
: 개발자가 제공하는 key 프로퍼티를 통해 자식 요소의 변경 여부를 표시할 수 있다.=> 여러 번 렌더링을 거쳐도 변경되지 말아야 하는 자식 요소가 무엇인지 알아낼 수 있다.
위와 같은 가정을 통해 React는 비교 알고리즘을 사용한다.
위에서 말했다 싶이, 만약 자식 노드들이 key를 갖고 있다면, React는 그 key를 이용해 기존 트리의 자식과 새로운 트리의 자식이 일치하는지 아닌지 확인할 수 있다.
<ul>
<li key="2021">Duke</li>
<li key="2022">Villanova</li>
</ul>
<!-- key가 2014인 자식 엘리먼트를 처음에 추가. -->
<ul>
<li key="2020">Connecticut</li>
<li key="2021">Duke</li>
<li key="2022">Villanova</li>
</ul>
위 예제에서는 key 속성을 통해 ‘2020’라는 자식 엘리먼트가 새롭게 추가됐다.
React는 기존의 동작 방식대로 다른 자식 엘리먼트는 변경하지 않고, key 속성을 통해 해당 key가 존재하는지 확인한 후 추가된 요소만을 변경한다.
이때 key는 유일한 값을 사용해야 한다. key 속성에는 보통 데이터 베이스 상의 유니크한 값(ex. Id)을 부여한다. key는 형제 엘리먼트 사이에서만 유일하면 된다.(전역에서 유일할 필요는 없음)
key가 유니크한 값이 아니라면, 배열의 인덱스를 key로 사용할 수 있다.
다만, 배열이 다르게 정렬될 경우가 생긴다면 배열의 인덱스를 key로 선택했을 경우는 비효율적으로 동작할 수 있다. 그 이유는 배열이 다르게 정렬된다 하더라도 인덱스는 그대로 유지되기 때문이다. 인덱스는 그대로지만 그 요소가 바뀌어버린다면 React는 배열의 전체가 바뀌었다 생각하고 기존의 DOM 트리를 버리고 새로운 DOM 트리를 구축해버린다.