제목 날짜 내용 발행일 23.03.22
해당 포스트는
Virtual DOM
에 대해 학습한 내용을 정리하며 기록한 내용입니다.
React가 기존 가상 DOM과 변경된 새로운 가상 DOM을 비교할 때,
변경된 새로운 가상 DOM 트리가 기존의 UI를 효율적으로 갱신하는 방법을 알아낼 필요가 있었다.
즉 하나의 트리를 다른 트리로 변형을 시키는 가장 작은 조작 방식을 알아내야만 했는데, 알아낸 조작 방식 알고리즘은 O(n3)의 복잡도를 가지고 있었다.
만약 이 알고리즘을 그대로 React에 적용한다면 1000개의 엘리먼트를 실제 화면에 표시하기 위해 10억(1000^3)번의 비교 연산을 해야만 한다.
사실 이것은 너무 비싼 연산이기 때문에 React는 두 가지의 가정을 가지고 시간 복잡도 O(n)의 새로운 휴리스틱 알고리즘(Heuristic Algorithm)을 구현해낸다.
두 가지 가정은 이렇다.
실제 이 두 가정은 거의 모든 실제 사용 사례에 맞았고, 여기서 React는 비교 알고리즘(Diffing Algorithm)
을 사용
React는 기존의 가상 DOM 트리와 새롭게 변경된 가상 DOM 트리를 비교할 때, 트리의 레벨 순서대로 순회하는 방식으로 탐색
즉 같은 레벨(위치)끼리 비교
이는 너비 우선 탐색(BFS)의 일종
동일 선상에 있는 노드를 파악한 뒤 다음 자식 세대의 노드를 순차적으로 파악
DOM 트리는 각 HTML 태그마다 각각의 규칙이 있어 그 아래 들어가는 자식 태그가 한정적
<ul>
태그 밑에는 <li>
태그만 와야 함<p>
태그 안에 <p>
태그를 또 쓰지 못함자식 태그의 부모 태그 또한 정해져 있다는 특징이 있기 때문에, 부모 태그가 달라진다면 React는 이전 트리를 버리고 새로운 트리를 구축
<div>
<Counter />
</div>
//부모 태그가 div에서 span으로 바뀝니다.
<span>
<Counter />
</span>
이렇게 부모 태그가 바뀌면,
1. React는 기존의 트리를 버리고 새로운 트리를 구축
2. 이전의 DOM 노드들은 전부 파괴
부모 노드였던 <div>
가 <span>
으로 바뀌어버리면 자식 노드인 <Counter />
는 완전히 해제
이전 <div>
태그 속 <Counter />
는 파괴되고, <span>
태그 속 새로운 <Counter />
가 다시 실행
새로운 컴포넌트가 실행되면서 기존의 컴포넌트는 완전히 해제(Unmount)
되어버리기 때문에 <Counter />
가 갖고 있던 기존의 state
또한 파괴
반대로 타입이 바뀌지 않는다면 최대한 렌더링을 하지 않는 방향으로 최소한의 변경 사항만 업데이트
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
스타일만 바뀌고 있음을 알아낸다.
따라서 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는 기존 <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>
로 자식 노드가 서로 다르다고 인지한다.즉 <li>Duke</li>
와 <li>Villanova</li>
는 그대로기 때문에 두 자식 노드는 유지시켜도 된다는 것을 깨닫지 못하고 전부 버리고 새롭게 렌더링 해버린다
이는 굉장히 비효율적인 동작 방식
React는 이 문제를 해결하기 위해 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 트리를 구축해버리기 때문에 비효율적으로 동작하는 것이다.