사실 예전의 나는 Virtual DOM을 그냥 '성능이 좋아서 쓰는 거 아닌가?'라는 식으로 막연하게만 생각했었다. 꽤 오랫동안 React를 사용하면서도 왜 Virtual DOM을 사용해야 하는지에 대한 깊은 이해 없이 그냥 사용하고 있었던 것 같다.
하지만 항해 2주차에서 코치님들의 발제자료를 보고, 직접 virtual DOM을 구현해보면서 그 동안 잘못 알고 있었다는 점을 깨닫게 되었다. 이 글에서는 그동안 잘못 이해하고 있었던 Virtual DOM에 대해, 그리고 왜 우리가 Virtual DOM을 사용해야 하는지에 대해 정리해보려고 한다.
1.Virtual DOM이 없던 시절..
Virtual DOM이 왜 필요했는지 이해하려면, 예전에 바닐라 자바스크립트로 어떻게 개발했는지를 보면 금방 깨닫게 된다. 프레임워크의 도움 없이 DOM을 관리할 때는 보통 `html template literal`과 `innerHTML`을 사용했는데, 상태가 변경될 때 마다 전체 HTML을 다시 생성하고 `innerHTML`로 교체하는 방식이다.
Virtual DOM은 대규모 DOM 조작이 필요한 경우 이점이 있지만, 작은 규모의 앱에서는 오히려 오버헤드가 될 수 있다.
👍 Virtual DOM이 성능적으로 효과적일 때
대규모 리스트나 복잡한 UI 업데이트
대규모 리스트를 직접 DOM으로 조작하면 하나하나 개별적으로 처리해야 한다:
// js의 경우 -> 1000개 아이템 업데이트 시 - 각각 개별 DOM 조작
for (let i = 0; i < 1000; i++) {
document.getElementById(`item-${i}`).textContent = newData[i];
// 각 조작마다 레이아웃 재계산 발생 가능
}
Virtual DOM의 배치 처리 방식
Virtual DOM은 모든 변경사항을 한 번에 비교하고 배치 처리한다:
// VirtualDOM은 모든 변경사항을 한 번에 비교하고 배치 처리
const oldVTree = [...]; // 이전 가상 DOM 트리
const newVTree = [...]; // 새로운 가상 DOM 트리
// 한 번의 diff 알고리즘으로 모든 변경사항 파악
const patches = diff(oldVTree, newVTree);
// 실제 DOM에 한 번에 적용
applyPatches(realDOM, patches);
최소한의 DOM 조작을 위한 Reconciliation 알고리즘
O(n³) → O(n) 복잡도로 최적화된 알고리즘을 사용한다:
// O(n³) → O(n) 복잡도로 최적화
function reconcileChildren(oldChildren, newChildren) {
// 1. 키 기반 매칭으로 이동/삽입/삭제 최소화
// 2. 동일한 타입의 컴포넌트만 비교
// 3. 변경된 속성만 업데이트
const operations = [];
// 최소한의 DOM 조작만 수행
for (let i = 0; i < newChildren.length; i++) {
const oldChild = oldChildren[i];
const newChild = newChildren[i];
if (shouldUpdate(oldChild, newChild)) {
operations.push({ type: 'UPDATE', node: oldChild, props: newChild.props });
}
}
return operations;
}
또한 React의 경우 key index나 React.memo 등으로 추가적인 최적화가 가능하다.
👎 Virtual DOM이 성능적으로 별로일 때
Virtual DOM은 실제 DOM을 JS객체로 가지고 있는 것이니 만큼, 메모리 사용량과 초기 로딩 시간으로 인해 손해를 볼 수 있다.
Virtual DOM, 변경된 Virtual DOM, 그리고 실제 DOM까지 3가지의 DOM이 메모리에 상주
각 컴포넌트마다 인스턴스 메타데이터를 가지고 있어야 함
초기 로딩 시간 이슈
런타임 라이브러리 로드
또한 최초 렌더링 시 Virtual DOM 트리를 구성해야 한다.
JSX → Virtual DOM 변환
초기 트리 구성
실제 DOM 생성
이벤트 시스템 초기화
⇒ 따라서 간단한 경우에는 직접적인 DOM 조작이 더 효율적이다.
대부분의 애니메이션
Virtual DOM은 변경사항을 모아서 한번에 처리(batching)하는 것이 최대 장점인데, 애니메이션은 매 프레임마다 연속적으로 변경해야 하기 때문에 Virtual DOM의 장점을 활용하기 어렵다.
그리고 중요한 부분인데, 최신 브라우저의 렌더링 엔진은 매우 최적화되어 있다.
웹 컴포넌트와 Shadow DOM의 등장으로 네이티브 수준의 캡슐화가 가능해졌으며
브라우저 네이티브 API의 성능이 지속적으로 개선되고 있다.
고로 처음 Virtual DOM이 등장했을 때면 몰라도, 이제는 성능 때문에 Virtual DOM을 사용해야 한다는 것은 갈수록 틀린 말이 되어가고 있는 것이다.
3. 그럼 왜 Virtual DOM을 써야 할까?
아니 그러면 우리가 도대체 왜 Virtual DOM을 사용해야 하는 것일까? 성능도 더 이상 좋다고 하기 어려운데?! 라고 하기에는 도저히 포기할 수 없는 강점이 있다. 바로 선언형으로 코딩이 가능하다는 점이다.
예를 들어, 간단하게 클릭하면 토글되는 todo item을 react로 구현한다고 해보자.
// 명령형 (바닐라 JS)
function updateTodoStatus(id, completed) {
const element = document.getElementById(`todo-${id}`);
const checkbox = element.querySelector('input[type="checkbox"]');
const text = element.querySelector('span');
checkbox.checked = completed;
if (completed) {
element.classList.add('completed');
text.style.textDecoration = 'line-through';
} else {
element.classList.remove('completed');
text.style.textDecoration = 'none';
}
}
이렇게 큰 차이가 나는 이유는 React에서는 그냥 상태만 바꾸면 UI가 알아서 동기화되기 때문(정확히는, React에서 Virtual DOM을 비교해서 알아서 동기화 처리를 해주기 때문)이다. 개발자는 "DOM을 어떻게 건드릴지" 고민할 필요 없이 "UI가 어떻게 보여야 하는지"만 생각하면 된다.
📝 결론
이렇게 Virtual DOM에 대해 공부하면서 느낀 점들을 정리해보면:
기술의 가치는 단순한 성능 수치에만 있는 게 아니다
개발자 경험과 생산성도 엄청 중요한 요소다
프로젝트 상황을 고려해서 기술을 선택해야 한다
Virtual DOM은 이제 더 이상 "빠르게 DOM 조작하기 위한 기술"이 아니다. 복잡한 UI 상태 관리를 쉽게 해주고, 개발자가 비즈니스 로직에 집중할 수 있게 해주는 도구에 가깝다.
결국 좋은 개발자가 되려면 기술의 표면만 보지 말고, 그 기술이 풀려고 하는 근본적인 문제가 뭔지를 이해하는 게 중요한 것 같다. Virtual DOM도 그런 관점에서 바라봐야 제대로 이해할 수 있을 것 같다.
+α. React VS Svelte
하지만 Virtual DOM을 쓰지 않고도 훌륭한 DX를 제공하는 프레임워크도 있다. 바로 Svelte다. Svelte를 만든 사람인 Rich Harris는 대놓고 공식 홈페이지에 "Virtual DOM is pure overhead"라는 글을 올려두고 있다.
Svelte는 Virtual DOM을 아예 안 쓴다. 대신 컴파일할 때 최적화된 코드를 미리 만들어버리는 방식을 사용한다.
// Svelte가 생성하는 최적화된 코드 (개념적 예시)
function update_count(new_count) {
if (count !== new_count) {
count = new_count;
button_text.textContent = `클릭 수: ${count}`;
}
}
결과적으로 React가 JSX를 가지고 Virtual DOM을 만들고, 이걸로 컴포넌트 인스턴스를 생성하고, 다시 이벤트를 붙이는 동안 Svelte는 그냥 컴파일 시점에서 바로 최적화된 JS로 변환한 다음 실제 DOM을 생성하고 이벤트 리스너를 직접 붙여버린다.
와! 그러면 이제 Svelte의 시대가 오는구나!!
하지만 안타깝게도 그런 일은 없지 않을까?
이미 확고한 강자로 자리잡은 React도 계속해서 발전해나가고 있으며, Svelte도 Svelte 5에서 React의 state 문법을 받아들이며 갈수록 수렴진화에 가까운 모습을 보이고 있다. 거기에다가 이미 많은 문서와 거대한 커뮤니티, 다양한 라이브러리등 생태계 구축이 완료되어있는 상황이다.
물론 Virtual DOM을 사용하지 않고도 더 간결한 문법을 보여주고, 뛰어난 성능과 작은 번들 사이즈 등의 장점은 분명히 매력적이다. 그렇기에 나 역시 많은 데이터를 차트로 랜더링해야하는 프로젝트에서 Svelte를 사용했었으니까.
하지만 생태계의 성숙도와 기존 투자 대비 마이그레이션 비용을 고려했을 때, Svelte는 새로운 프로젝트나 특정 성능이 중요한 영역에서의 대안으로서 의미를 가질 것 같다.