-
리액트를 경험한 지 어언 3주 차, 가장 신기했던 것 하나를 알아보고자 합니다.
리액트를 사용하기 전에는 요소를 추가, 제거, 업데이트할 때 개발자 도구를 확인해 보면 전체 태그가 변경되는 것을 확인할 수 있었습니다.
그런데, 리액트를 사용하니 변경된 부분만을 갱신하는 게 굉장히 신기했습니다.
그래서 어떻게 변경된 부분만 쏙쏙 찾아서 업데이트해주는지 알아보았습니다.
가상 돔 (Virtual DOM)
먼저, 가상 돔이 뭔지 알아야 합니다.
DOM은 Document Object Model의 약자로, 직역하면 문서 객체 모형입니다.
풀어서 말하면 HTML 등의 문서의 각 항목들을 트리구조로 만든 객체 모델을 말합니다.
그럼 Virtual DOM은 실제 돔이 아닌 DOM을 추상화시킨 것이라는 것을 유추할 수 있습니다.
가상 돔은 실제 DOM의 가벼운 복사본으로, 실제로 렌더링 되지는 않습니다.
UI의 상태 변화가 발생하면 먼저 가상 돔이 업데이트되고, 이전 가상 돔과 현재 가상 돔을 비교하여 변경된 부분을 찾습니다.
변경된 부분을 찾으면 해당 부분만을 실제 돔에 반영함으로써 최소한의 조작으로 업데이트할 수 있게 합니다.
만약, 가상 돔을 사용하지 않는다면 트리 구조로 이루어진 돔에서 요소를 직접 추가, 제거, 수정해야 하고 불필요한 리렌더링이 발생할 수 있어 브라우저에 큰 부하를 줄 수 있습니다.
재조정 (Reconciliation)
그렇다면, 가상 돔에서 실제 돔으로 어떻게 반영을 할 수 있을까요?
하나의 트리를 가지고 다른 트리로 변환하기 위한 가장 횽류적인 알고리즘도 O(n3)의 복잡도를 가집니다.
리액트는 이를 해결하기 위해 아래 두 가지 가정을 기반으로 O(n) 복잡도의 휴리스틱 알고리즘을 구현했다고 합니다.
1. 서로 다른 타입의 두 요소는 서로 다른 트리를 만들어낸다.
2. 개발자가 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.
비교 알고리즘 (Diffing Algorithm)
먼저, 1번 가정에 대한 것을 알아봅시다.
두 개의 트리를 비교할 때, 먼저 루트(root) 요소부터 비교합니다.
<div> <Counter /> </div> <span> <Counter /> </span>위 코드의 두 요소의 루트는 div와 span으로 서로 다른 타입입니다.
그럼 리액트는 이전 트리를 버리고 새로운 트리를 구축합니다.
결과적으로 이전 Counter는 사라지고 연관된 모든 state도 사라진 뒤, 새로 다시 마운트 됩니다.
<div className="before" title="stuff" /> <div className="after" title="stuff" /> <div style={{color: 'red', fontWeight: 'bold'}} /> <div style={{color: 'green', fontWeight: 'bold'}} />만약 요소의 타입이 같을 경우는 변경된 속성들만 수정합니다.
위의 두 요소의 경우에는 className만 수정하고, 아래의 두 요소의 경우에는 style의 color 속성만 수정합니다.
이 경우에는 인스턴스는 같게 유지되기 때문에 렌더링 간 state는 유지됩니다.
자식에 대한 재귀적 처리
루트를 비교하고 난 뒤, 루트의 자식 요소들에 대해서 재귀적으로 처리합니다.
<ul> <li>first</li> <li>second</li> </ul> <ul> <li>first</li> <li>second</li> <li>third</li> </ul>그렇다면, 위와 같은 상황에서는 문제없이 첫 번째와 두 번째 li 태그를 비교하고, 마지막 <li>third</li>를 돔에 추가할 수 있을 것입니다.
하지만 아래와 같은 상황에서는 문제가 생깁니다.
<ul> <li>Duke</li> <li>Villanova</li> </ul> <ul> <li>Connecticut</li> <li>Duke</li> <li>Villanova</li> </ul>새로운 요소를 맨 앞에 추가하는 경우 두 트리를 비교할 때 첫 번째 요소가 다르기 때문에 <li>Connecticut</li>요소를 추가한 뒤, 나머지 자식들도 새로 마운트 할 것입니다.
이는 리액트가 BFS 또는 그와 유사한 방법으로 트리를 탐색하기 때문에 나타나는 문제라고 생각합니다.
이러한 문제를 해결하기 위해, 리액트는 key 속성을 지원합니다.
그리고 이 key 속성이 2번 가정에 해당하는 것입니다.
<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>아까 전과 같은 상황에서 key 속성을 추가하여 주었습니다.
key 속성을 통해 리액트는 '2014' key를 가진 요소가 추가되었고, '2015'와 '2016' key를 가진 속성은 이동만 하면 된다는 것을 확인할 수 있습니다.
여담

map 함수를 사용하여 컴포넌트를 반복해서 렌더링할 때 위와 같은 에러를 자주 봤었습니다.
그때는 이유를 잘 모르고, 에러를 해결하기 위해 key 속성을 추가하여 주기만 하였습니다.
하지만, 이제는 반복해서 렌더링 한 컴포넌트들에 변동이 일어났을 때, 효율적으로 리렌더링 하기 위해 key를 사용한다는 것을 알았습니다.
후기
알고 지낸지 이제 3주밖에 안된 리액트이지만, 조금씩 친해지고 있는 느낌이 들고 있습니다.
그리고 오늘은 가장 궁금했던 부분을 학습해서 조금 더 가까워졌다고 생각합니다.
아직 모르는 리액트의 장점이 많이 있겠지만, 하나하나 알아가면서 잘 사용할 수 있으면 좋겠다고 생각합니다.
참고