리액트에서는 가상 돔을 사용한다. 가상 돔이란, 실제 DOM(Document Object Model)을 조작하는 방식이 아닌, 실제 DOM을 모방한 가상의 DOM을 구성해 원래 DOM과 비교하여 달라진 부분을 리렌더링 하는 방식으로 작동한다. 이 때 가상 돔을 잘 이해해야만 리액트의 상태(state)에 대해 잘 이해하고 다룰 수 있다.
왜 리액트는 실제 DOM을 조작하지 않고, 가상 돔을 사용하는 것일까? 자바스크립트의 모던한 방식(Vanilla Javascript)의 DOM을 조작하는 방식은 무거운 작동방식이다. 실제 DOM에는 브라우저가 화면을 그리는데 필요한 모든 정보가 들어있기 때문이다. 그래서 리액트는 깜빡거림 없이 부드러운 UX를 사용자에게 제공하고자 변경사항만 빠르게 파악하고 리렌더링 하기 위해 DOM을 만들어 비교하는 방식을 채택하였다.
리액트는 성능 향상을 위해 실제 렌더링된 UI를 내부적으로 자바스크립트 객체로 따로 관리한다. 직접 DOM 노드를 생성하거나 접근해서 변경을 하는 것이 자바스크립트 객체로 표현된 DOM 트리를 조작하는 것보다 훨씬 느리기 때문이다.
(자바스크립트 객체를 따로 관리하면서 생기는 메모리 사용량 증가의 단점이 존재한다.)
리액트는 실제 DOM의 UI를 가진 자바스크립트 객체를 메모리상에 가지고 있다. 가상 돔은 변화를 감지하면 재조정(Reconcilation)과정을 통하여 실제 DOM과 동기화 한다. 재조정 과정은 크게 3단계로 나뉜다.
- UI가 변경을 감지하면 UI를 Virtual DOM으로 렌더링한다. (실제 화면상 렌더링 되는 것이 아닌 비교를 위한 가상 렌더링)
- 현재 Virtual DOM과 이전 Virtual DOM을 비교해 차이를 계산한다.
- 변경된 부분을 실제 DOM에 반영한다.
리액트는 노드를 비교할 때 얕은 비교를 한다. 리액트의 얕은 비교는 같은 레벨에서만 일어난다.
- 숫자나 문자열, Boolean 같은 원시값을 가진 자료형은 값을 비교한다.
- 배열, 객체 등 참조값을 가진 자료형은 그 안의 값 혹은 attribute를 비교하지 않고, 그들의 레퍼런스(참조되는 위치)를 비교한다.
배열은 직접 수정하는 방식, push
, pop
, unshift()
와 같은 메서드로 배열을 수정한 뒤 setState에 담아주어도 같은 참조 위치를 가지고 있기 때문에 실제 값의 변화가 있다 하더라도 감지할 수 없다.
리액트에서 배열 값을 변경할 경우에는 객체와 마찬가지로 불변성을 지켜주어야 한다. 배열 혹은 객체의 원본을 수정하지 않고 상태 변경을 원하는 배열과 함수를 복사(깊은 복사를 말함, 얕은 복사는 해당 안됨)하고 사용해야 한다는 뜻이다.
그래서 원본을 수정하는 메서드인 push()
, pop()
과 같은 메서드를 사용하여 원본을 직접 수정하는 것은 리액트를 사용할 때는 지양해야 한다. assign()
메서드를 사용하거나, 전개구문을 사용해서 복사를 한 뒤 그 복사된 값을 수정하고 setState에 담아주면 변경을 감지 할 수 있다.
실제 DOM 조작은 비용이 높은 작업이다. 하지만 가상 돔은 메모리 상에서만 동작하며, 실제 DOM 조작을 최소화하여 성능을 향상시키게 된다. 변경된 부분만 실제 DOM에 적용함으로써 렌더링 성능을 향상시킨다.
가상 돔은 상태 변화를 추적하고, 적절한 타이밍에 변경 사항을 적용함으로써 렌더링 일관성을 유지하게 된다.
비교적 규모가 큰 애플리케이션에서 UI를 관리하기 위해 컴포넌트 기반 아키텍처를 사용하게 되는데, 가상 DOM은 이를 가장 효과적, 효율적으로 관리할 수 있게 해준다.
가상 돔은 브라우저 독립적인 방식으로 동작하므로, React Native와 같은 플랫폼에서도 활용될 수 있다.
다른 기술과 함께 사용할 수 있으며, 다양한 환경에서 렌더링 할 수 있도록 해준다.
가상 돔은 메모리 상에 DOM과 완전히 동일한 구조의 메모리를 유지하게 되는데, 따라서 추가적인 메모리를 사용하게 된다.
개발자가 애플리케이션의 뷰(View)와 상태(State)를 동기화하기 위해 추가적인 추상화 계층을 사용해야 한다. 이로 인해 애플리케이션의 복잡성이 증가할 수 있다.
모든 상황에 완벽하게 적합하지 않을 수 있다. 정말 간단한 UI나 작은 규모의 애플리케이션에서는 불필요한 오버헤드일 수 있다.