Virtual DOM
리액트는 UI의 상태를 추적하고 변화가 일어나는 요소를 빠르게 업데이트 할 수 있게 virtual DOM이라는 가상 DOM 객체를 활용한다.
가상 DOM 객체에 접근해 변화 전과 후를 비교해서 바뀐 부분을 적용한다.
Real DOM(DOM)
Document Object Model의 약자로 문서 객체모델을 의미한다.
DOM은 브라우저가 HTML 문서를 조작할 수 있게 트리 구조화한 객체 모델이다.
DOM의 조작 속도가 느려지는 이유
DOM은 계층적 형태의 트리구조로 구성되어 있다.
트리는 저장된 데이터를 더 효과적으로 탐색하는 용도로 사용되어 빠른 자료 탐색 성능이 장점인 자료구조이다.
하지만 DOM이 변경되고 업데이트 된다는 것은 브라우저의 렌더링 엔진 또한 리플로우 되는 것을 의미한다.
DOM이 변경되면 업데이트된 요소와 그에 해당하는 자식 요소들에 의해 DOM트리를 재구축하게 된다.
이 과정에서 레이아웃 재연산을 수행하는 리플로우, 화면을 그려내는 Repaint 과정을 거치게 되고 이 과정에서 변화가 필요없는 부분도 변경되며 잦은 리플로우 발생으로 성능을 떨어뜨리는 문제를 발생시킨다.
변경된 부분만 비교해 그 부분에 대해서만 렌더링 할 수 없을까? 하는 아이디어를 기반으로 Virtual DOM이 등장하게 되었다.
Virtual DOM
가상 DOM을 조작하는것과 실제 DOM조작을 예로 들면 가상 DOM은 이삿날 가구를 배치하기 전에 미리 가구배치에 대한 생각을 해놓고 실제로 배치하는 것과 같다.
Virtual DOM 형태
가상 DOM은 추상화된 js객체의 형태를 갖고있다.
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은 리액트에서 컴포넌트의 상태나 속성이 바뀔때마다 새로 생성되고, 리액트는 이전 가상 DOM과 새로운 DOM을 비교해 변경된 부분만 실제 DOM에 반영한다.
리액트는 상태를 변경하는 작업이 일어났을 때, 가상 DOM에 저정된 이전상태와 변경된 현재 상태를 비교한다.
비교과정에서 Diffing알고리즘을 사용해 변경된 부분을 감지한다.
React Diffing Algorithm
리액트는 상태 변화에 따라 UI를 업데이트해야 할때 전체가 아닌 변경된 부분만 업데이트한다. 이것을 가능하게 하는 것이 Diffing Algorithm이다.
이전 가상 DOM트리와 현재 가상 DOM트리를 비교해 변경된 부분을 찾아내는 것
둘의 차이를 계산해 실제 DOM에 반영해야하는 최소한의 변경사항을 결정한다.
diffing Algorithm은 효율적인 비교를 위해 다양한 최적화 기법을 사용한다.
1. 각기 서로 다른 두 요소는 다른 트리를 구축할 것
2. 개발자가 제공하는key
프로퍼티를 가지고, 여러번 렌더링을 거쳐도 변경되지 않는 자식요소가 무엇인지 알아낼 수 있는 것
리액트의 DOM트리 탐색 방법
트리의 레벨 순서대로 순회하는 방식으로 탐색한다.(같은 레벨끼리 비교 = BFS)
- 다른 타입의 DOM 엘리먼트인 경우
<div> <Counter /> </div> //부모 태그가 div에서 span으로 바뀝니다. <span> <Counter /> </span>
부모태그가 바뀌어버리면 리액트는 기존의 트리를 버리고 새로운 트리를 구축하기 때문에 이전의 DOM 노드들은 전부 파괴된다.
이전<div>
태그 속<Counter />
는 파괴되고<span>
태그 속 새로운<Counter />
가 다시 실행된다.
새로운 컴포넌트가 실행되면서 기존의 컴포넌트는 완전히 해제되어버리기 때문에<Counter />
가 갖고 있던 기존의 state도 파괴된다.
- 같은 타입의 DOM 엘리먼트인 경우
타입이 바뀌지 않는 경우 리액트는 최대한 렌더링을 하지 않는 방향으로 최소한의 변경사항만 업데이트한다.<div className="before" title="stuff" /> //기존의 엘리먼트가 태그는 바뀌지 않은 채 className만 바뀌었습니다. <div className="after" title="stuff" />
두 요소를 비교할때
className
만 수정되고 있다는것을 알게 된다.//className이 before인 컴포넌트 <div style={{color: 'red', fontWeight: 'bold"}} title="stuff" /> //className이 after인 컴포넌트 <div style={{color: 'green', fontWeight: 'bold"}} title="stuff" />
이 경우 리액트는 정확히
color
스타일만 바뀌고 있다는 것을 알게 되어color
스타일만 수정하고fontWeigth
와 그밖의 다른 요소는 수정하지 않는다.
하나의 DOM노드를 처리한 뒤 리액트는 뒤이어 해당 노드들 밑의 자식들을 순차적으로 동시에 순회하며 차이가 발견될때마다 변경하고 이를 재귀적으로 처리한다고 표현한다.
- 자식 엘리먼트 재귀적 처리
<ul> <li>first</li> <li>second</li> </ul> //자식 엘리먼트의 끝에 새로운 자식 엘리먼트를 추가했습니다. <ul> <li>first</li> <li>second</li> <li>third</li> </ul>
이 경우 리액트는 첫번재 자식 노드들과 두번째 자식 노드들이 일치하는 걸 확인하고 세번째 자식노드를 추가한다.
<ul> <li>Duke</li> <li>Villanova</li> </ul> //자식 엘리먼트를 처음에 추가합니다. <ul> <li>Connecticut</li> <li>Duke</li> <li>Villanova</li> </ul>
이 경우에는 리액트는 최소한으로 동작하지 못한다.
처음의 자식노드와 변경된 자식노드를 비교할때<li>Duke</li>와<li>Connecticut</li>
가 서로 다르다고 인지하고 리스트 전체가 바뀌었다고 인식해 전부 새롭게 렌더링 한다.
이 동작은 비효율적이다. 그래서 이 문제를 해결하기 위해key
속성을 지원한다.
- 키(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>
이 경우 리액트는 기존의 동작 방식대로 다른 자식 엘리먼트는 변경하지 않고 추가된 엘리먼트만 변경한다.
key
속성에는 유니크한 값을 부여해 주면 된다.
키는 전역적으로 유일할 필요는 없고, 형제 엘리먼트사이에서만 유일하면 된다.
내용출처, 참조 : 코드스테이츠
이미지 출처 : 코드스테이츠