✅배경지식 Check!
⚠ js, react에 대한 기본 지식이나 간단한 코드라도 개발해본 경험이 있어야 함.
⚠ DOM에 대해 알아야함.
🚩 브라우저 렌더링 과정에 대한 이해가 있으면 좋음.
Virtual DOM, 가상의 DOM 이라는 뜻인데 여기서 DOM이란것은 무엇일까?
일단 이 돔은 아니다.
아마 자바스크립트 기본 지식이 있는 사람이라면 DOM에 대해 한번쯤은 들어보고 알고있을것이다.
하지만, Virtual DOM에 대해 알아보기 전에 다시 한번 DOM에 대해 간단히 알아보도록 하자.(DOM Tree 등 DOM의 깊은 부분까지는 다루지 않을 것이다.)
MDN에서는 DOM를 아래와 같이 정의한다.
문서 객체 모델(The Document Object Model, 이하 DOM) 은 HTML, XML 문서의 프로그래밍 interface 이다.
프로그래밍 인터페이스라니, 갑자기 어렵게 느껴지는것 같다.
하지만, 이 말에 함축된 의미를 풀어쓰자면 다음과 같기에 어렵지 않을 것이다.
DOM은 웹 브라우저가 제공하는 DOM API를 통해 HTML 및 XML 문서의 구조, 콘텐츠, 스타일을 조작할 수 있기 때문에 DOM에서 제공하는 방법과 사양을 따르기만 하면, 파이썬에서도 자바스크립트에서도 프로그래밍 언어와 관계없이 DOM에 접근하고 조작할 수 있다.
여기서 API라는 용어가 낯설다면 아래 정의를 읽어보자.
API는 "Application Programming Interface"의 약자이다. 개발자가 다른 애플리케이션, 시스템 또는 장치와 상호 작용하는 소프트웨어 애플리케이션을 구축하는 데 사용하는 일련의 규칙 및 프로토콜이다. 즉, 소프트웨어 구성 요소가 서로 상호 작용하는 정의된 인터페이스이다.
한마디로 API는 어떤 기능을 제공하는 도구라고 볼 수 있다. 그러므로 위에서 DOM에 대해 풀어쓴것과 합치면
DOM은 웹 브라우저가 제공하는 DOM API를 통해 HTML 및 XML 문서의 구조, 콘텐츠, 스타일을 조작할 수 있는 객체
라고 할 수 있다.
그렇다면, DOM이 있다는 것을 알고 DOM의 정의도 알게 되었는데 Virtual DOM이란 것은 어쩌다 만들어진 것일까?
자바스크립트로 개발을 하다 보면 DOM 요소에 id, class, tag 선택자 등으로 접근하는 등 실제 DOM에 대해 우리는 많은 작업을 수행한다.
그렇지만 순수 자바스크립트가 아닌 리액트를 사용할 경우 말이 달라진다.
우리는 리액트를 사용해 프로그래밍 할 경우 순수 자바스크립트에서 처럼 직접적으로 DOM요소를 선택자를 통해 접근하거나 조작하지 않는다.
그럼에도 리액트 환경에서는 마치 DOM요소에 직접 접근이라도 한 것 처럼 우리가 원하는 대로 프로그래밍되어 브라우저에 렌더링된다.
어째서일까?
React에서는 기존 자바스크립트에서처럼 DOM을 개발자가 직접 조작하지 않는다.
import { createRoot } from 'react-dom';
import { useState } from 'react';
function App() {
const [color, setColor] = useState('red');
const handleClick = () => {
setColor(color === 'red' ? 'blue' : 'red');
};
return (
<div>
<button onClick={handleClick}>Change color</button>
<div style={{ width: '100px', height: '100px', backgroundColor: color }}></div>
</div>
);
}
const root = createRoot(document.getElementById('root'));
root.render(<App />);
리액트에서는 바닐라 자바스크립트에서처럼 id, class, tag 선택자 등을 사용해서 ui를 구성하고 이벤트를 부여하는것이 아니라 일반적으로 JSX 문법을 사용해 UI를 생성한다.
최상위 jsx 파일에서 ReactDOM.createRoot()(리액트 18버전 이전 버전에서는 ReactDOM.render())를 통해 Virtual DOM 트리를 구성한다.
그리고 react hooks를 이용하여 상태를 관리하거나 DOM요소를 참조하는 객체를 생성하는 등(useRef)의 작업을 수행할 수 있다.
이러한 일련의 과정은 우리가 리액트를 이용하여 개발하면서 흔히 마주하는 상황들이고 이 상황들 중 그 어느 부분에서도 직접적으로 DOM에 접근하고 조작하는 부분은 없다.
실제 DOM을 조작하는 것은 어떻게 보면 직관적이고 편리해보일 수 있지만 DOM을 직접 조작하면 속도가 느리고 비효율적일 수 있다고 한다.
DOM을 직접 수정하면 브라우저는 영향을 받는 요소를 다시 UI로 그리기 위해 많은 작업을 수행해야 하는데, 이는 변동이 잦은 SPA(Single Page Application) 의 UI를 자주 업데이트할 때 특히 문제가 될 수 있기 때문이다.
(지금은 이렇게 알고 넘어가지만, 이 부분에 대해서는 추후 Virtual DOM에 대해 알아본 후 다시 이야기해 볼 것이다.)
그렇기에 리액트에서는 Virtual DOM이라는 패턴을 도입했다.
리액트에서 실제 DOM과 Virtual DOM은 어떤 관계이며 Virtual DOM은 어떤 식으로 동작하고 있는 것일까?
DOM과 Virtual DOM은 무슨 관계일까?
Virtual DOM은 DOM을 본떠서 만든 객체이다. 그렇지만 완전히 동일한 복제품은 아니고 비유하자면
DOM을 다이어트 시킨(?) 상태가 Virtual DOM이다.
Virtual DOM은 실제 DOM과 달리 세부 레이아웃, 스타일정보나 이벤트 리스너들을 포함하지 않는다.
그렇기 때문에 실제 DOM에 비해 가볍고 빠르게 Virtual DOM 트리를 구축할 수 있다.
이 트리는 DOM 트리와 같이 루트로부터 가지가 뻗어나가는 형태이다. DOM의 복제품이니 당연하다.
또한 Virtual DOM은 자바스크립트 객체 형태로 자바스크립트 코드가 실행되는 웹 브라우저의 메모리에 저장되고 실제 DOM은 웹 브라우저의 메모리에 존재하는 실제 HTML 문서 객체를 의미한다.
이제 Virtual DOM의 동작에 대해 알아보도록 하자.
사실 Virtual DOM의 동작 과정은 매우 간결하고 명료하다.
React 컴포넌트의 상태가 변경되거나 props가 생성되거나 props가 업데이트 되는 등의 이유로 리렌더링이 발생하면 React는 새로운 가상 DOM 트리를 생성하고 이전 Virtual DOM과 비교하여 차이점이나 변경 사항을 찾는다. 그런 다음 React는 가상 DOM의 변경 사항을 실제 DOM에 반영하기 위해 Diffing 알고리즘을 이용해 필요한 부분만 업데이트하게 되는데 이 과정을 Reconciliation이라고 한다.
위 설명을 그림으로 표현하면 다음과 같다.
우선 리액트에선 state 변경, props 생성 및 수정등에 의해 리렌더링이 발생되면 리액트는 Virtual DOM 트리를 구축하게된다.
이러면 이전 Virtual DOM 트리와 Virtual DOM 트리가 다를것이다.
그러면 리액트의 Diffing 알고리즘이 그 차이점을 알 수 있게 된다.
Diffing 알고리즘이 이전에 생성한 Virtual DOM 트리를 보고 지금 Virtual DOM 트리와의 차이점을 계산한다.
그러면 리액트는 현재 기준의 Virtual DOM 트리 기반으로 실제 DOM 트리에 일괄적으로 반영하게 되는 것이다.
컴포넌트의 상태가 변경되거나 React Hook으로 인해 리렌더링이 발생하면 가상 DOM이 트리로 재구축되고, 이 새로운 가상 DOM을 실제 DOM이 아닌 기존 가상 DOM과 비교하여 변경된 내용을 확인하게 되는것이다.
이때 사용되는 알고리즘이 Diffing Algorithm(비교 알고리즘) 이다.
Diffing Algorithm(비교 알고리즘)
리액트는 두 개의 트리를 비교할 때 루트 엘리먼트부터 비교를 시작하며,
엘리먼트의 타입이 다른 경우
⇒ 루트 엘리먼트의 타입이 다르면 이전 Virtual DOM Tree는 버리고 완전히 새로운 Tree를 구축한다.
//기존
<div>
<Counter />
</div>
//변경
<span>
<Counter />
</span>
엘리먼트 타입이 같은 경우
⇒ 두 엘리먼트의 속성을 확인하여, 동일한 내역은 유지하고 변경된 속성들만 갱신한다.
<div style={{color: 'red', fontWeight: 'bold'}} /> //기존
<div style={{color: 'green', fontWeight: 'bold'}} /> //변경
자식 노드에 대한 재귀적 처리
⇒ 동시에 비교 대상인 두 리스트를 순회하고 차이점이 있으면 변경을 생성한다.
//기존
<ul>
<li>first</li>
<li>second</li>
</ul>
//변경
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
위 예제에서는 리스트 마지막에 <li>third</li>
를 추가해주면 된다. 그런데 이런 단순한 리스트가 아닌
//기존
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
//변경
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
위 코드처럼 순서가 뒤죽박죽인채로 추가되면 모든 자식 노드들을 버리고 새로 만들어야 할 것이다.
//기존
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
//변경
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
따라서 리액트에서는 Key 속성을 추가하여 기존 트리의 자식 리스트들과 현재 트리의 자식 리스트들이 일치하는지를 효율적으로 확인할 수 있게 된다.
여기서 언급한 key는 우리가 리액트로 개발하면서 map 함수로 태그를 반복적으로 생성할 때 key 속성을 추가하라는 경고 메시지 등으로 익숙하게 알고 있을 것이다.
리스트에 key 속성이 필요한 것은 이처럼 Diffing 알고리즘이 이전 Virtual DOM 트리와 현재 Virtual DOM 트리를 비교하기 위해서 반드시 필요하기 때문이다.
이것이 Diffing 알고리즘의 대략적인 내용이다.
자세한 것은 공식문서를 참조해보자.
그런데, useState, useEffect와 같은 훅은 리렌더링을 일으키는 것으로 알고 있지만 useRef 훅은 어떨까?
useRef를 통해 DOM요소를 참조하는 객체를 생성해서 사용하는 것은 리렌더링을 일으키지 않는다.
다만, 이 때 생성한 객체의 상태를 변경하는 등의 동작을 일으키면 리렌더링이 일어나므로 이때 Virtual DOM 트리가 재구축된다고 할 수 있다.
Batch Update(일괄 업데이트)
리액트는 일괄 업데이트 메커니즘을 따라 실제 DOM을 업데이트한다.
앞서 말한 부분에서는 이전의 Virtual DOM 과 현재의 Virtual DOM을 비교해서 변화한 부분 만을 판단해 DOM Tree를 재구축한다고 하였다.
리액트는 변경 사항이 반영되어 재구축된 Virtual DOM을 실제 DOM에 업데이트할때 리액트 컴포넌트의 상태가 바뀔 때마다 업데이트하는 것이 아니라 일괄적으로 업데이트함으로써 성능을 향상시킨다.
안돼…
브라우저의 뷰포트 내에서 각 노드들의 정확한 위치와 크기를 계산 및 배치하는 단계를 다시 거치는 리플로우(Reflow) 와 계산된 레이아웃을 바탕으로 실제 화면에 픽셀로 그리는 단계를 다시 거치는 리페인트(Repaint) 는 비용이 많이드는 과정들이다.
리액트는 실제 DOM이 UI를 다시 그릴때 일괄 업데이트된 내용을 반영하도록 효율적으로 보장한다.
즉, 여태 설명한 것을 쭉 한번 정리하자면
개발자는 리액트 컴포넌트의 리렌더링을 유발해서 virtual DOM 트리 구조를 새로 생성하게 트리거시킴으로써 Virtual DOM 구성에 간접적으로 영향을 줄 수는 있지만 직접적인 관여는 불가하고,
Virtual DOM의 트리 구조 생성 이후 리액트가 자체적으로 이전 Virtual DOM과 현재 Virtual DOM간의 차이점을 발견하는 "diffing" 알고리즘을 사용하여 Reconciliation을 수행한다.
Reconciliation가 완료되면 현재 Virtual DOM을 사용하여 새로운 실제 DOM 트리를 일괄 업데이트 하여 생성한 다음 렌더링을 위해 브라우저에 전달한다. 브라우저는 새로운 실제 DOM을 해석하고 그에 따라 표시되는 페이지를 업데이트하게 되는 것이다.
DOM 트리는 DOM요소에 변화가 생길 경우 루트부터 재구축하고 그에 따라 렌더 트리도 재구축 한다. 이로 인해 리플로우, 리페인팅이 일어나 UI를 다시 그려야 하므로 많은 비용이 드는 작업을 수행한다고 할 수 있다.
그러나 Virtual DOM 트리는 이전 Virtual DOM 트리와의 차이점을 파악하고 해당 부분만 수정한 Virtual DOM 트리를 구축하고 루트 엘리먼트의 타입이 다를 때만 트리를 루트부터 재구축한다.
더불어 실제 DOM 트리를 구축할때는 현재의 Virtual DOM 트리를 기반으로 일괄 업데이트하여 구축하기 때문에 리플로우, 리페인트 작업을 여러번 일으키지 않아 자원 소모가 적고 Virtual DOM 은 DOM보다 가볍기 때문에 빠르게 재구축이 가능하다는 점에서 유리하다고 볼 수 있다.
이렇듯 편리하고 완벽해보이는 Virtual DOM에게도 분명 한계점이 존재한다.
자바스크립트 객체로 브라우저 메모리 공간에 존재한다는 것은 Virtual DOM이 더욱 복잡하고 빈번한 변경이 일어나는 페이지일수록 크기가 커지면서 메모리 공간을 더 많이 차지한다는 의미이기도하다. 실제 DOM에 비해 훨씬 작은 크기이긴 하나 상황에 따라서는 Virtual DOM의 크기가 버거울수도 있다.
또한, Virtual DOM이 실제 DOM에 변경 사항을 반영함에 있어 철저한 계산의 결과로 동작하겠지만 그럼에도 약간의 오버헤드(리소스의 낭비)가 발생할 수 있다. 이러한 오버헤드는 비용 증가로 이어질 우려가 있다는 점도 한계로 지적된다.
이에 대한 자세한 내용은 Svelte의 창시자인 리치 해리스의 블로그 글을 참고하자.
✅ DOM : 웹 브라우저가 제공하는 DOM API를 통해 HTML 및 XML 문서의 구조, 콘텐츠, 스타일을 조작할 수 있는 객체
✅ Virtual DOM : UI의 이상적인 또는 “가상”적인 표현을 메모리에 저장하고 ReactDOM과 같은 라이브러리에 의해 “실제” DOM과 동기화 하는 프로그래밍 개념.
Virtual DOM의 동작
React 컴포넌트의 상태가 변경되거나 props가 생성되거나 props가 업데이트 되는 등의 이유로 리렌더링이 발생하면 React는 새로운 가상 DOM 트리를 생성하고 이전 Virtual DOM과 비교하여 차이점이나 변경 사항을 찾는다. 그런 다음 React는 가상 DOM의 변경 사항을 실제 DOM에 반영하기 위해 Diffing 알고리즘을 이용해 필요한 부분만 업데이트하게 되는데 이 과정을 Reconciliation이라고 한다.
Virtual DOM의 장점
Virtual DOM의 한계
Virtual DOM으로 인해 개발자는 많은 수고를 덜어낸 셈이다. 브라우저 렌더링의 과정과 더불어 리액트와 Virtual DOM에 대해 알게되면서 평소에 자주 겪던 현상에 대한 의문이 많이 해소된 것 같다. 간단하게 필요한 개념만을 정리했으나 추가적인 내용이 있다면 반영할 예정이다.
-끝
참고한 자료
DOM
https://developer.mozilla.org/ko/docs/Web/API/Document_Object_Model/Introduction
https://ko.javascript.info/dom-nodes
Virtual DOM
https://ko.reactjs.org/docs/faq-internals.html#gatsby-focus-wrapper
https://ko.reactjs.org/docs/rendering-elements.html
https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html
https://ko.reactjs.org/docs/reconciliation.html#gatsby-focus-wrapper
https://github.com/reactjs/react-basic
https://github.com/acdlite/react-fiber-architecture
https://ko.reactjs.org/docs/design-principles.html#gatsby-focus-wrapper
https://www.geeksforgeeks.org/reactjs-virtual-dom/
https://www.hyesungoh.xyz/whyVirtualDom
https://d2.naver.com/helloworld/9297403
https://meetup.nhncloud.com/posts/110
https://techblog.woowahan.com/8311/
https://overreacted.io/ko/react-as-a-ui-runtime/
https://velog.io/@yeonbot/React에서-key의-역할-컴포넌트를-다시그리는-과정
https://reactkungfu.com/2015/10/the-difference-between-virtual-dom-and-dom/
https://adhithiravi.medium.com/react-virtual-dom-explained-in-simple-english-fc2d0b277bc5
https://it-eldorado.tistory.com/87
https://svelte.dev/blog/virtual-dom-is-pure-overhead