DOM과 Virtual DOM의 구조 및 동작 원리
DOM 이란?
DOM(Document Object Model)은 HTML, XML 문서의 프로그래밍 인터페이스 이다. 웹 페이지의 객체 지향 표현으로, JavaScript와 같은 스크립팅 언어를 통해 문서 구조, 스타일, 내용을 변경할 수 있게 해준다.
HTML 파싱과 DOM 트리 생성 과정
브라우저가 HTML 문서를 처리하는 과정은 다음과 같다.
- 바이트 → 문자: 서버로부터 받은 HTML 파일의 바이트를 문자로 변환
- 토큰화: HTML 태그를 토큰으로 변환
- 노드 생성: 토큰을 분석하여 노드(DOM 요소)로 변환
- DOM 트리 구축: 노드들을 계층적 트리 구조로 조립
html
├── head
│ ├── title
│ └── meta
└── body
├── div
│ └── p
└── script
이러한 트리 구조는 부모-자식 관계를 형성하며, 각 노드는 특정 유형(요소, 텍스트, 주석 등)을 가진다.
Tree의 구조적 특성과 접근 비용
DOM 트리는 계층적 구조를 가지며, 이는 데이터 접근 및 조작에 영향을 미친다.
시간 복잡도
- 노드 접근: O(1) (직접 참조 시)
- 노드 검색: O(n) (선형 검색 시)
- 트리 순회: O(n) (모든 노드 방문 시)
공간 복잡도
- DOM 트리는 웹 페이지의 모든 요소를 저장하므로 O(n)공간을 차지
DOM 트리가 커질수록 조작과 탐색의 비용도 증가한다. 특히 복잡한 웹 애플리케이션에서는 수천 개의 노드가 존재할 수 있어, 효율적인 DOM 조작이 중요함
DOM 조작의 비용
DOM 조작은 브라우저에 상당한 부하를 줄 수 있다. 이는 DOM API 호출이 브라우저의 렌더링 파이프라인을 다시 실행시키기 때문이다.
렌더링 파이프라인
DOM 조작 후 브라우저는 다음 단계를 거쳐 화면을 다시 그린다.
-
스타일 계산(Style): 변경된 DOM에 CSS 규칙을 적용
-
레이아웃(Layout/Reflow): 요소의 크기와 위치를 계산
-
페인트(Paint): 픽셀을 화면에 그림
-
컴포지팅(Compositing): 여러 레이어를 결합
비용이 높은 DOM 조작
특히 다음 DOM 조작은 높은 비용을 발생시킨다.
- 레이아웃 촉발 속성 변경:
width
,height
,margin
,padding
,display
등 - DOM 구조 변경: 요소 추가/제거, 위치 변경
- 스타일 대량 변경: 클래스 전환, 인라인 스타일 변경
- 스크롤 위치 변경: 강제 스크롤 위치 변경
예를 들어, 다음 코드는 성능 저하를 일으킬 수 있다.
// 안좋은 예: 반복문 내 DOM 조작으로 강제 레이아웃/리플로우 발생
for (let i = 0; i < 1000; i++) {
document.getElementById("myDiv").style.width = i + "px";
console.log(document.getElementById("myDiv").offsetWidth); // 강제 레이아웃 발생
}
Virtual DOM 개념
Virtual DOM은 실제 DOM의 가벼운 JavaScript 표현이다. UI의 이상적인 상태를 메모리에 저장하고, ReactDOM과 같은 라이브러리를 통해 실제 DOM과 동기화한다.
React와 Virtual DOM
React는 Virtual DOM을 가장 대중적으로 활용하는 라이브러리이다. React의 Virtual DOM 작동 방식은 다음고 같다.
- 상태 변화: 컴포넌트의 상태(state)가 변경됨
- 새로운 Virtual DOM 생성: React는 새로운 Virtual DOM 트리를 생성
- Diffing(비교): 기존 Virtual DOM과 새로운 Virtual DOM을 비교
- Reconciliation(재조정): 변경된 부분만 실제 DOM에 반영
Virtual DOM의 이점
- Batch 업데이트: 여러 DOM 변경을 하나의 일괄 처리로 최적화
- 불필요한 리렌더링 방지: 변경된 부분만 업데이트
- 선언적 프로그래밍: 개발자는 UI 상태만 정의하고, DOM 업데이트는 라이브러리가 처리
- 크로스 플랫폼: DOM 추상화로 다양한 렌더링 환경(웹, 모바일)을 지원
Diffing 알고리즘
React의 Diffing 알고리즘은 두 트리를 비교하는 일반적인 알고리즘 O(n³)을 O(n)으로 최적화하며, 이는 두 가지 가정을 기반으로 한다.
- 다른 타입의 두 요소는 다른 트리를 생성
- 개발자는 key 속성으로 여러 렌더링에서 안정적인 요소를 표시할 수 있음
React의 Diffing 알고리즘은 다음 순서로 작동한다.
- 루트 요소부터 비교: 루트 요소 타입이 다르면 전체 트리를 재구성
- 같은 타입의 DOM 요소 비교: 속성만 업데이트하고 자식 요소를 재귀적으로 처리
- 같은 타입의 컴포넌트 요소 비교: 인스턴스는 유지하고 props만 업데이트
- 자식 목록 비교: key를 사용하여 효율적으로 비교
// 예시: React 컴포넌트와 Virtual DOM
function TodoItem({ text, completed }) {
return (
<li className={completed ? "completed" : ""}>
{text}
</li>
);
}
// React는 이 JSX를 다음과 같은 Virtual DOM 객체로 변환
{
type: "li",
props: {
className: completed ? "completed" : "",
children: text
}
}
Reconciliation(재조정) 과정
Reconciliation은 Virtual DOM의 변경사항을 실제 DOM에 적용하는 과정이다.
- 변경 집합(mutation set) 생성: diffing 결과를 바탕으로 필요한 DOM 변경 목록 작성
- 일괄 적용: 모든 변경을 한 번에 적용하여 레이아웃 계산을 최소화
- 효율적 업데이트: 필요한 최소한의 DOM 조작만 수행
React 16부터는 Fiber 아키텍처를 도입하여 이 과정을 더욱 개선하였다.
Virtual DOM 최적화 전략
Keyed Diff vs Unkeyed Diff
리스트 렌더링에서 key 속성은 중요한 최적화 요소이다.
Unkeyed Diff(key 없음)
- 위치 기반으로 요소를 비교
- 리스트 중간에 요소가 추가/제거되면 그 이후의 모든 요소가 리렌더링됨
// Unkeyed 예시 - 비효율적
<ul>
{items.map((item) => (
<li>{item.text}</li>
))}
</ul>
Keyed Diff(key 있음)
- 고유 식별자로 요소를 추적
- 변경된 요소만 정확히 업데이트
// Keyed 예시 - 효율적
<ul>
{items.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
Batching
React는 여러 상태 업데이트를 Batch로 처리하여 불필요한 리렌더링을 방지한다.
Batch: 변경된 모든 엘리먼트들을 일괄적으로 DOM에 반영
// React 17 이전: 이벤트 핸들러 내에서만 자동 배치
function handleClick() {
setCount((c) => c + 1); // 배치의 일부
setFlag((f) => !f); // 배치의 일부
// 이 두 상태 업데이트는 하나의 렌더링으로 처리됨
}
// React 18: 자동 배치 확장
setTimeout(() => {
setCount((c) => c + 1); // 배치의 일부
setFlag((f) => !f); // 배치의 일부
// React 18에서는 비동기 코드에서도 배치 처리
}, 1000);
React 18의 createRoot API는 모든 상태 업데이트에 자동 Batch를 적용한다.
Fiber 아키텍처
React 16부터 도입된 Fiber 아키텍처는 렌더링 작업을 작은 단위로 분할하여 중요한 작업(애니메이션, 사용자 입력)을 방해하지 않고 렌더링 할 수 있게 한다.
- 작업 분할: 렌더링을 더 작은 단위로 나눔
- 우선순위 지정: 중요한 업데이트(사용자 입력)에 높은 우선순위를 부여
- 중단 및 재개: 렌더링을 중단하고 나중에 다시 시작할 수 있음
- 오류 경계: 컴포넌트 트리의 일부에서 발생한 오류가 전체 앱에 영향을 미치지 않도록 함
// React 18의 동시성 기능 예시
function App() {
const [isPending, startTransition] = useTransition();
function handleClick() {
// 낮은 우선순위로 상태 업데이트 표시
startTransition(() => {
setCount((c) => c + 1);
});
}
return (
<div>
{isPending ? <Spinner /> : <ExpensiveComponent count={count} />}
<button onClick={handleClick}>Update</button>
</div>
);
}
Fiber는 단일 스레드 javaScript 환경에서 동시성을 구현하는 React의 핵심 아키텍처이다.
Memoization 전략
React는 불필요한 리렌더링을 방지하기 위한 메모이제이션 기능을 제공한다.
React.memo
: 컴포넌트 결과를 메모이제이션useMemo
: 계산 결과를 메모이제이션useCallback
: 함수 정의를 메모이제이션
// 컴포넌트 메모이제이션
const MemoizedComponent = React.memo(function MyComponent(props) {
// props가 변경되지 않으면 리렌더링 건너뜀
return <div>{props.name}</div>;
});
// 값 메모이제이션
function Component({ data }) {
// data가 변경될 때만 계산 수행
const processedData = useMemo(() => {
return expensiveCalculation(data);
}, [data]);
return <div>{processedData}</div>;
}
결론
DOM과 Virtual DOM은 웹 애플리케이션 개발의 핵심 개념이다. DOM은 웹 문서의 프로그래밍 인터페이스를 제공하지만, 직접적인 조작은 비용이 높은 렌더링 작업을 발생시킬 수 있다.
Virtual DOM은 이러한 문제를 추상화 계층을 통해 해결하여, 효율적인 UI 업데이트와 선언적 프로그래밍 모델을 제공한다.
특히 React와 같은 라이브러리의 diffing 알고리즘, Reconciliation 과정, Fiber 아키텍처는 복잡한 웹 애플리케이션의 성능 최적화에 크게 기여한다. 개발자는 Virtual DOM의 동작 원리와 최적화 전략을 이해함으로써 더 효율적이고 반응이 빠른 웹 애플리케이션을 구축할 수 있다.