React를 처음 공부할 때 Virtual DOM을 그 자체로 독립된 개념처럼 생각했고, React 엘리먼트는 단순히 JSX 문법이 변환된 결과물 정도로만 생각했습니다. 하지만 두 개념이 사실은 하나의 시스템을 구성하는 퍼즐 조각이라는 것을 깨닫고 막연했던 개념에서 명확한 원리로 이해할 수 있었습니다.
서로 따로 놀던 두 개념이 하나의 유기적인 시스템으로 연결되는 것을 이해하고 React의 동작 원리가 보이기 시작했습니다. 앞으로 코드를 짤 때 단순히 결과물만을 생각하는 것이 아니라, 그 뒤에 숨겨진 구조와 효율성까지 고려하는 개발자가 되어야하겠다고 다짐하는 계기가 되었습니다 :)
React는 Virtual DOM을 통해 효율적으로 UI를 업데이트합니다. 이 가상적인 개념은 실제 DOM을 직접 조작하는 것보다 훨씬 빠르고 효율적인 성능을 가능하게 합니다. Virtual DOM의 핵심에는 React Element라는 중요한 객체가 있습니다. 이 글에서는 이 둘의 관계를 심층적으로 다루고, React의 동작 원리를 명확히 설명해 드리겠습니다.
Virtual DOM (가상 DOM)이란?

Virtual DOM은 실제 DOM(Document Object Model)의 경량화된 인메모리(in-memory) 표현입니다. React는 UI 상태가 변경될 때마다 전체 UI를 Virtual DOM에 다시 렌더링합니다. 이 과정에서 이전 Virtual DOM 스냅샷과 새로운 스냅샷을 비교하는 'diffing(차이점 비교)' 알고리즘이 실행됩니다. 두 알고리즘에 대해서는 아래에 자세하게 적어두었습니다.
- Diffing: React는 이전과 이후의 Virtual DOM 트리를 순회하며 변경된 노드를 찾아냅니다. 예를 들어, div 태그의 텍스트가 변경되었다면, React는 정확히 해당 노드만 식별합니다.
- Reconciliation (재조정): 변경 사항이 모두 식별되면, React는 최소한의 변경(최적의 업데이트)만을 실제 DOM에 반영합니다. 이는 실제 DOM 조작이 비용이 많이 드는 작업이기 때문에, 이러한 최적화 과정이 React의 빠른 성능을 보장할 수 있습니다.
Virtual DOM은 마치 UI의 청사진과 같습니다. React는 이 청사진을 여러 번 수정하고 최종적으로 확정된 버전만 건축가(브라우저)에게 전달하여 실제 건물을(UI) 짓게 합니다.
Diffing과 Reconciliation: 두 트리의 비교와 실제 DOM 업데이트
1단계: Diffing (차이점 비교)

React는 이전 Virtual DOM 트리와 새로운 Virtual DOM 트리를 깊이 우선 탐색(Depth-First Search) 방식으로 순회하며 각 노드를 비교합니다. 이 과정을 Diffing 알고리즘이라 부릅니다.
- 루트 노드 비교: div 노드는 type과 props가 동일하므로, 자식 노드로 내려갑니다.
- 첫 번째 자식 비교: h1 노드 역시 type과 children이 동일하므로 변경 사항이 없습니다.
- 두 번째 자식 비교: p 노드의 type은 동일하지만, children (props.children) 속성 값이 'This is a paragraph.'에서 'Updated paragraph.'로 변경되었음을 감지합니다.
2단계: Reconciliation (재조정)
Diffing 과정에서 발견된 변경 사항을 실제 DOM에 적용하는 최적화된 과정을 Reconciliation이라고 합니다. React는 p 태그의 텍스트 내용만 변경하면 된다는 것을 파악하고, 불필요한 DOM 조작을 방지합니다.
- 실제 DOM 업데이트: React는 브라우저 API를 호출하여 <p> 태그의 innerHTML만 Updated paragraph.로 변경합니다. 전체 <p> 태그를 삭제하고 새로 생성하는 것이 아니라, 최소한의 변경만을 적용하는 것입니다.
- 결과: 단 하나의 DOM 조작만으로 UI가 업데이트됩니다. 이는 웹 브라우저가 전체 DOM 트리를 재계산하고 다시 그리는(reflow/repaint) 복잡한 작업을 피하게 해주어 성능을 극대화합니다.
React Element는 UI의 설계도 역할을 하고, 이 설계도들의 집합인 Virtual DOM이 React의 Diffing/Reconciliation 과정을 통해 실제 DOM 업데이트를 최적화하는 핵심적인 역할을 하는 것입니다. 이 두 알고리즘을 이용해 React는 빠르고 효율적인 사용자 인터페이스를 제공할 수 있습니다.
React Element란?
Virtual DOM 트리를 구성하는 각 노드들은 React Element 객체입니다. 이는 HTML의 <p>나 <img> 태그와는 다릅니다. React 엘리먼트는 가볍고 불변(immutable)적이며, "이 시점에 UI가 이렇게 생겨야 한다"는 것을 설명하는 역할을 합니다.
// React Element를 생성하는 예시
const element = <h1 className="greeting">Hello, world!</h1>;
// 위 JSX는 아래와 같은 순수 JavaScript 객체로 변환됩니다.
const elementObject = {
$$typeof: Symbol.for('react.element'),
type: 'h1',
key: null,
ref: null,
props: {
className: 'greeting',
children: 'Hello, world!'
},
_owner: null,
};
위 코드에서 볼 수 있듯이, React Element 객체는 type, props, key와 같은 속성들을 가집니다.
- type: h1, div와 같은 HTML 태그 문자열이거나, MyComponent와 같은 React 컴포넌트 함수 또는 클래스입니다.
- props: className, children 등 컴포넌트에 전달되는 속성들을 포함하는 객체입니다.
- key: 리스트 렌더링 시 고유성을 식별하기 위해 사용됩니다. (map() 함수를 사용할 때 key 속성 필수)
이러한 React Element 객체들이 트리 형태로 구성된 것이 바로 Virtual DOM입니다. 즉, Virtual DOM은 React Element 객체들로 이루어진 거대한 객체 트리입니다.
엘리먼트 렌더링하기: Virtual DOM의 시작
React 엘리먼트를 화면에 그리는 과정은 ReactDOM.createRoot()와 root.render() 함수를 통해 이루어집니다.
- 루트(Root) DOM 노드 설정: const root = ReactDOM.createRoot(document.getElementById('root')); 이 코드는 HTML 파일의 특정 DOM 노드를 React가 관리하는 루트 노드로 지정합니다. 이제 이 노드 안의 모든 UI는 React가 책임집니다.
- 엘리먼트 렌더링: const element = <h1>Hello, world</h1>; root.render(element); root.render() 함수가 호출되면, React는 전달받은 엘리먼트 객체를 기반으로 실제 DOM 노드를 생성하여 화면에 표시합니다.
이 과정은 마치 React 엘리먼트 객체라는 설계도를 브라우저에게 전달하여, 브라우저가 실제 DOM 트리라는 건물을 짓게 하는 것과 같습니다.
엘리먼트 업데이트하기: 불변성과 효율성
가장 중요한 개념은 React 엘리먼트가 불변(Immutable) 객체라는 점입니다. 한 번 생성된 엘리먼트는 변경할 수 없습니다. 그렇다면 UI를 업데이트하려면 어떻게 해야 할까요?
React는 업데이트가 필요할 때마다 root.render()를 다시 호출하여 새로운 엘리먼트 객체를 전달합니다.
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
root.render(element); // 새로운 엘리먼트를 매초 전달
}
setInterval(tick, 1000);
이 코드를 보면 매초 전체 UI를 다시 그리는 것처럼 보입니다. 하지만 React DOM은 매번 전체 DOM을 새로 만드는 비효율적인 작업을 하지 않습니다.
Diffing (변경 사항 비교)
React는 이전 엘리먼트 트리와 새로운 엘리먼트 트리를 비교합니다. 이 과정에서 내용이 바뀐 <h2> 태그만 변경되었음을 정확히 감지합니다. <h1> 태그와 <div> 태그는 변경되지 않았기 때문에 그대로 둡니다.
최소한의 DOM 업데이트
결과적으로 React는 매번 전체 UI를 다시 그리는 대신, <h2> 태그의 텍스트 노드만 업데이트합니다. 이처럼 필요한 부분만 효율적으로 업데이트하는 접근법 덕분에 React는 빠르고 부드러운 사용자 경험을 제공할 수 있습니다. 이것이 바로 Virtual DOM의 핵심 원리이자, React가 동적인 UI를 효율적으로 관리하는 방법입니다.
Component와 React Element의 차이 알아보기
공부하면서 컴포넌트(Component)와 엘리먼트(Element)의 차이에 대한 개념이 모호하게 느껴졌습니다. 둘 다 UI를 구성하는 핵심 개념이기 때문에 React를 능숙하게 다루기 위해서 관계를 명확히 정의해보겠습니다.
1) Component = UI의 설계도
컴포넌트는 JavaScript 함수나 클래스입니다. 화면에 보여줄 UI를 어떻게 만들지 정의하는 설계도 또는 청사진 역할을 합니다. 컴포넌트는 재사용 가능한 코드 조각으로, props라는 입력값을 받아서 UI를 렌더링합니다.
- 본질: 함수 또는 클래스
- 역할: UI를 정의하는 재사용 가능한 코드 조각
- 예시: function Welcome(props) { ... }
2) React Element = 설계도로 만들어진 객체 (Object)
React 엘리먼트는 화면에 렌더링될 내용을 기술하는 가벼운(lightweight), 불변(immutable)의 순수 JavaScript 객체입니다. 컴포넌트의 render 함수가 반환하는 결과물이 바로 이 React 엘리먼트입니다. 이는 마치 설계도(컴포넌트)를 바탕으로 만들어진 실제 건물(UI)에 대한 상세한 설명서와 같습니다.
- 본질: Plain JavaScript Object
- 역할: UI의 특정 부분을 묘사하는 객체
- 예시: <h1 className="greeting">Hello, world!</h1> JSX 코드는 아래와 같은 객체로 변환됩니다.
{
$$typeof: Symbol.for('react.element'),
type: 'h1',
key: null,
props: { className: 'greeting', children: 'Hello, world!' }
}
3) JSX를 통해 이해하기
흔히 JSX를 사용해 컴포넌트와 엘리먼트를 연결합니다. JSX는 마치 React.createElement() 함수를 호출하는 간결한 문법(Syntactic Sugar)과 같습니다.
// 1. 컴포넌트 정의 (설계도)
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
// 2. JSX를 사용해 엘리먼트 생성 (설명서)
const element = <Welcome name="Sara" />;
위 코드에서 <Welcome name="Sara" />는 Welcome이라는 컴포넌트를 사용해 React 엘리먼트를 만들고 있습니다. React는 이 코드
를 실행할 때 다음과 같이 동작합니다.
- <Welcome name="Sara" /> 코드를 만나면 React.createElement(Welcome, { name: 'Sara' }) 함수를 호출합니다.
- React.createElement는 Welcome 컴포넌트를 호출하고 { name: 'Sara' }를 props로 전달합니다.
- Welcome 컴포넌트는 내부에서 <h1>Hello, {props.name}</h1> JSX를 반환하고, 이는 다시 React.createElement('h1', ...) 함수 호출로 변환되어 최종적인 React 엘리먼트 객체를 반환합니다.
이러한 과정을 통해 React는 컴포넌트(설계도)를 엘리먼트(객체)로 변환하고, 그 엘리먼트 객체들을 기반으로 Virtual DOM을 구축하여 실제 DOM에 렌더링합니다.
| 컴포넌트 (Component) | React 엘리먼트 (React Element) | |
| 본질 | JavaScript 함수/클래스 | Plain JavaScript 객체 |
| 역할 | UI를 정의하는 템플릿 | UI를 묘사하는 불변 객체 |
| 속성 | props라는 인자를 받음 | type, props 등의 속성을 가짐 |
| 예시 | function MyButton() | <div>, <MyButton /> |
Virtual DOM = React Element들의 트리
결론적으로, Virtual DOM은 React Element 객체들로 이루어진 트리 구조입니다.
- 초기 렌더링: JSX를 사용하여 컴포넌트를 작성하면, React는 이를 React Element 객체 트리(Virtual DOM)로 변환합니다. 이후 이 Virtual DOM을 기반으로 실제 DOM을 생성하여 화면에 표시합니다.
- 상태 변경: 컴포넌트의 상태(state)나 속성(props)이 변경되면, React는 새로운 React Element 트리(새로운 Virtual DOM)를 생성합니다.
- Diffing: React는 이전 Virtual DOM과 새로운 Virtual DOM을 비교(diffing)하여 정확히 어떤 React Element가 변경되었는지 찾아냅니다.
- Reconciliation: 변경된 부분만 실제 DOM에 적용(reconciliation)합니다.
이러한 과정 덕분에 React는 불필요한 DOM 조작을 최소화하고, 높은 성능과 빠른 응답 속도를 유지할 수 있습니다. Virtual DOM과 React Element는 단순한 개념이 아니라, React가 가장 인기 있는 웹 프레임워크가 될 수 있었던 핵심적인 기술 원리입니다.
회고
React를 처음 공부할 때 Virtual DOM을 그 자체로 독립된 개념처럼 생각했고, React 엘리먼트는 단순히 JSX 문법이 변환된 결과물 정도로만 생각했습니다. 하지만 두 개념이 사실은 하나의 시스템을 구성하는 퍼즐 조각이라는 것을 깨닫고 막연했던 개념에서 명확한 원리로 이해할 수 있었습니다!
제가 알게 된 핵심은 다음과 같습니다.
- React 엘리먼트는 Virtual DOM을 구성하는 '객체'였습니다.
- Virtual DOM은 엘리먼트 객체들로 이루어진 '트리'였습니다.
Virtual DOM은 그저 추상적인 개념이 아니라, 제가 직접 작성하는 컴포넌트와 엘리먼트가 모여 만들어지는 구체적인 데이터 구조였다는 사실을 깨달았습니다. 엘리먼트가 불변 객체이기 때문에 React가 이전 Virtual DOM과 새로운 Virtual DOM을 효율적으로 비교할 수 있다는 diffing 원리를 비로소 완벽히 이해하게 되었습니다.
서로 따로 놀던 두 개념이 하나의 유기적인 시스템으로 연결되는 것을 이해하고 React의 동작 원리가 보이기 시작했습니다. 앞으로 코드를 짤 때 단순히 결과물만을 생각하는 것이 아니라, 그 뒤에 숨겨진 구조와 효율성까지 고려하는 개발자가 되어야하겠다고 다짐하는 계기가 되었습니다 :)
References
'📚 CS > React' 카테고리의 다른 글
| [React] useRef 알아보기 (0) | 2025.09.16 |
|---|---|
| [React] useEffect, useLayoutEffect 비교하기 (0) | 2025.09.14 |
| [React] 클래스형 컴포넌트 vs 함수형 컴포넌트 (0) | 2025.09.12 |
| [React] React Hooks: 함수형 컴포넌트 알아보기 (0) | 2025.09.11 |
| [React] React 렌더링 파이프라인 (0) | 2025.09.09 |