DOM
HTML 문서는 브라우저에 의해 파싱되어, 각 요소를 자바스크립트 객체로 변환한 DOM(Document Object Model) 트리 형태로 브라우저에 저장됩니다.
이 DOM 트리는 브라우저의 렌더링 엔진에 의해 관리되며, 이를 기반으로 웹 페이지가 렌더링됩니다.
가상 돔(Virtual DOM)이 왜 등장했는가?
가상 DOM의 등장 배경을 이해하기 위해서는 CSR(Client Side Rendering)과 SSR(Server Side Rendering)이라는 렌더링 방식을 먼저 이해해야 합니다.
CSR vs SSR
SSR (Server Side Rendering)
사용자의 요청에 따라, 웹(프론트) 서버가 최종 완성된 HTML을 생성하여 클라이언트에 전송합니다.
각 페이지 요청마다 서버에서 HTML을 생성하여 전송하기 때문에, 페이지 전환 시 완전히 새로운 HTML 파일이 로드됩니다.
사용자는 서버에서 제공한 완성된 HTML을 이용해 DOM을 생성하고, 브라우저는 이를 렌더링하여 페이지를 표시합니다.
CSR (Client Side Rendering)
기존의 SSR과 달리, 모든 페이지를 서버에서 제공하는 대신, 한 번의 초기 로딩 후 JavaScript를 사용하여 동적으로 페이지를 구성합니다.
페이지 요청마다 서버 부하가 발생하는 문제를 해결하기 위해 등장한 방식이 SPA(Single Page Application)입니다.
SPA는 한 번의 페이지 로딩 후, JavaScript로 DOM을 조작하고 필요한 데이터만 비동기적으로 불러와 화면을 갱신합니다.
💡 Virtual DOM은 SPA(Single Page Application)와 같은 동적인 웹 애플리케이션에서 빈번한 DOM 업데이트로 인한 성능 저하를 해결하기 위해 등장했습니다.
SPA는 한 번의 페이지 로딩 이후 JavaScript를 이용해 동적으로 페이지를 조작하므로, DOM 업데이트가 자주 발생합니다.
Virtual DOM은 이러한 잦은 DOM 업데이트를 메모리 내에서 관리하고, 실제 DOM에 최소한의 변경만 반영하여 성능을 개선합니다.
DOM 조작이 많아지면?
DOM 조작 100번 vs DOM 조작 1번
DOM 업데이트는 브라우저가 요소를 다시 렌더링(Repaint)하고, 레이아웃을 재계산(Reflow)하는 과정으로 이어집니다.
예제
<script>
// Good
function onClickGoodCase() {
const $ul = document.getElementById("ul");
let list = "";
for (let i = 0; i < 100; i++) {
list += `<li>${i}</li>`;
}
$ul.innerHTML = list; // DOM 업데이트를 한 번만 수행
console.timeEnd("Good Case Timer");
}
// Bad
function onClickBadCase() {
console.time("Bad Case Timer");
const $ul = document.getElementById("ul");
for (let i = 0; i < 100; i++) {
$ul.innerHTML += `<li>${i}</li>`; // DOM을 매번 업데이트 (비효율적)
}
console.timeEnd("Bad Case Timer");
}
</script>
100개의 리스트를 생성할 때, DOM을 한 번만 업데이트하는 방식이 매번 DOM을 업데이트하는 방식보다 약 65.58배 더 빠른 성능을 보였습니다.
이로써 DOM 수정 -> 업데이트가 많아질수록 렌더링이 느려지게 됨을 알 수 있습니다.
근데 서비스가 커질수록 이렇게 동시에 발생한 업데이트를 모아서 한 번에 DOM을 수정하는 게 점점 힘들어집니다.
React에서의 Virtual DOM
어떻게 실제 DOM에 최소한의 변경만 반영하는지 React 라이브러리를 활용해서 알아보겠습니다.
리액트의 공식문서에서는 아래와 같이 Virtual DOM을 정의하고 있습니다.
한마디로 표현하면 메모리에 저장되는 DOM(자바스크립트) 트리의 복사본
음.. 그럼 DOM과 Virtual DOM의 차이가 뭔가요?
가상 DOM은 실제 DOM의 정보를 모두 가지고 있지만, 브라우저의 문서에 직접적으로 접근할 수 없습니다.
따라서 Virtual DOM을 구성하는 노드들에는 DOM 조작 API가 포함되어 있지 않습니다.
아래는 실제 DOM에서 사용 가능한 대표적인 DOM 조작 메서드들이며, 가상 DOM에는 이러한 메서드들이 존재하지 않습니다.
appendChild() | 새로운 자식 노드를 추가 | element.appendChild(newNode) |
removeChild() | 자식 노드를 제거 | element.removeChild(node) |
setAttribute() | 요소의 속성 값을 설정 | element.setAttribute('class', 'test') |
getAttribute() | 요소의 속성 값을 가져오기 | element.getAttribute('class') |
따라서 Virtual DOM은 자바스크립트 객체로, 브라우저의 실제 DOM을 직접 수정하지 않고 컴퓨터 메모리 상에서만 작동하기 때문에, 실제 DOM에 비해 성능 비용이 훨씬 적습니다.
Virtual DOM의 구성 요소
JSX는 HTML과 JavaScript를 결합한 문법으로, React에서 UI를 선언적으로 작성하기 위해 사용됩니다.
const message = <h1>안녕!</h1>;
JSX는 Babel을 통해 JavaScript 코드로 변환됩니다:
const message = React.createElement("h1", null, "안녕!");
React.createElement는 최종적으로 JavaScript 객체를 반환합니다.
const message = {
type: "h1",
props: {
children: "안녕!"
}
};
리액트는 실제 DOM을 직접 수정하지 않고, 최종적으로 만들어지는 React Element라는 객체를 활용하여 가상 DOM을 만들고 조작합니다.
따라서 리액트 컴포넌트 내에서 작성하는 모든 코드는 React의 Virtual DOM을 먼저 수정하는 방식입니다.
Virtual DOM의 동작 방식
1. 업데이트 발생 : 리액트 컴포넌트의 상태나 속성이 변경되면 새로운 React Element가 생성된다.
2. Virtual DOM 업데이트 : 변경된 내용들을 기존 Virtual DOM과 비교(diffing)한다.
3. 변경 사항 계산 : 변경이 필요한 최소한의 요소를 파악한다.
4. 실제 DOM 업데이트 : 파악된 변경사항만 실제 DOM에 반영한다.
Diffing algorithm
React는 Virtual DOM을 업데이트할 때 두 개의 Virtual DOM을 사용합니다. 하나는 업데이트 이전의 Virtual DOM이고, 다른 하나는 업데이트 후 변경 사항이 반영된 Virtual DOM입니다.
React는 이 두 버전을 비교하는 재조정(Reconciliation) 과정을 거쳐, 실제 DOM에 최소한의 변경 사항만 반영합니다.
Diffing Algorithm은 React가 두 Virtual DOM을 비교할 때 사용하는 핵심 알고리즘입니다.
두 Virtual DOM의 비교 동작 방식
const message = {
type: "h1",
props: {
children: "안녕!"
}
};
1. 비교한 엘리먼트의 type이 다른 경우
가상 돔 트리를 완전히 새로 재생성한다.
2. 비교한 엘리먼트의 type이 같은 경우
두 엘리먼트의 속성을 비교하여, 변경된 속성들만 갱신한다.
예를 들어,
<div className="before" title="stuff" />
<div className="after" title="stuff" />
이 두 엘리먼트를 비교하면, React는 가상 DOM 노드 상에 className만 수정합니다.
=> 이렇게 바뀐 부분만 파악한 후에 Batch Update를 수행하여 실제 DOM에 한번에 적용시켜준다.
Virtual DOM을 실제 DOM에 적용하기
React 라이브러리를 사용할 때, 가상 DOM을 실제 DOM에 적용하기 위해 react-dom이라는 라이브러리를 함께 사용해야 합니다.
react-dom은 브라우저와 상호작용하는 역할을 하며 가상 DOM을 실제 DOM에 마운트 시키는 중요한 역할을 하기 때문입니다.
// 예제 코드 React 18 기준
import React from 'react';
import ReactDOM from 'react-dom/client';
const App = () => {
return <h1>Hello, React!</h1>;
};
// 실제 DOM의 id가 root인 요소에 가상 DOM을 렌더링
const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);
root.render(<App />);
마무리
React의 Virtual DOM은 SPA 환경에서 빈번한 DOM 업데이트로 인한 성능 저하를 해결하는 혁신적인 방식입니다.
그러나 모든 프론트엔드 프레임워크가 동일한 방식을 사용하는 것은 아닙니다.
리액트의 Virtual DOM도 항상 최고의 선택은 아니며, DOM 조작이 적은 정적 페이지나 간단한 UI에서는 diffing 과정이 오히려 성능 저하를 일으킬 수 있다는 한계가 존재합니다.
따라서, 다양한 렌더링 방식을 이해하고 공부하는 것이 프론트엔드 프레임워크들의 등장 배경이라고 생각하며, 각 프레임워크의 특성과 성능을 신중히 고려하여 적절한 도구를 선택하는 것이 중요할 것 같습니다!
React | Virtual DOM | 메모리상의 가상 DOM, diffing 후 최소 업데이트 |
Vue.js | Virtual DOM | React와 유사한 Virtual DOM, 반응형 시스템 |
Svelte | 컴파일 단계 DOM 업데이트 | 컴파일 단계에서 직접 DOM 업데이트, Virtual DOM 없음 |
Angular | Incremental DOM (증분 DOM) | 변경된 노드만 직접 업데이트 |
참고
https://ko.legacy.reactjs.org/docs/reconciliation.html
https://developer.mozilla.org/ko/docs/Web/API/Document_Object_Model/Introduction
'👨💻 Programming > WEB' 카테고리의 다른 글
Stateless vs Stateful (0) | 2025.02.24 |
---|---|
모듈 번들러란? Webpack으로 알아보는 번들링 과정 (0) | 2025.01.20 |
브라우저 화면 렌더링 원리 – DOM, CSSOM부터 Display까지 총정리 (1) | 2024.08.09 |