가상 DOM 이해하기 및 Svelte/SolidJS가 이를 사용하지 않는 이유
Wenhao Wang
Dev Intern · Leapcell

소개
프론트엔드 개발 환경은 차세대 JavaScript 프레임워크에 의해 극적으로 재편되었습니다. 수년 동안 React와 Vue는 가상 DOM의 혁신적인 사용으로 대화를 지배하며 효율성과 개발자 친화적인 추상화를 약속했습니다. 가상 DOM은 UI 업데이트 관리를 위한 모범 사례로 널리 받아들여지면서 거의 어디에나 존재하는 최적화 기술이 되었습니다. 그러나 프론트엔드 생태계가 계속 진화함에 따라 Svelte와 SolidJS와 같은 새로운 플레이어들이 이 확립된 패러다임에 도전하고 있습니다. 그들은 가상 DOM이 독창적이긴 하지만, 그 자체의 복잡성과 오버헤드를 야기한다고 주장하며, 더 뛰어난 성능과 단순성을 목표로 하는 대안적인 전략을 제안합니다. 이 논의는 단순히 학문적인 것이 아닙니다. 오늘날과 미래에 우리가 웹 애플리케이션을 구축하고 최적화하는 방식에 중대한 영향을 미칩니다. 가상 DOM이 실제로 무엇인지, 그리고 이 새로운 프레임워크들이 왜 그것 없이 더 뛰어난 것을 달성할 수 있다고 믿는지 알아보겠습니다.
핵심 개념
대안을 탐구하기 전에 관련된 기본 기술을 이해하는 것이 중요합니다.
DOM (Document Object Model)
DOM은 웹 문서에 대한 프로그래밍 인터페이스입니다. 페이지 구조를 트리 형태로 표현하며, 각 노드는 문서의 일부(예: HTML 요소, 속성 또는 텍스트 노드)를 나타내는 객체입니다. DOM과의 상호 작용, 예를 들어 요소를 추가, 제거 또는 업데이트하는 것은 사용자 화면에 변경 사항을 직접 반영합니다. DOM을 직접 조작하는 것은 특히 많은 변경이 빠르게 발생할 때 느릴 수 있습니다. 브라우저에 레이아웃 재계산 및 다시 그리기를 자주 트리거하기 때문입니다.
가상 DOM
가상 DOM은 실제 DOM의 경량화된 메모리 내 표현입니다. 애플리케이션의 상태가 변경되면 새로운 가상 DOM 트리가 생성됩니다. 이 새로운 트리는 "diffing"이라는 프로세스에서 이전 트리와 비교됩니다. diffing 알고리즘은 실제 DOM을 업데이트하는 데 필요한 최소한의 변경 세트를 식별합니다. 이러한 변경 사항은 배치되어 단일의 최적화된 작업으로 실제 DOM에 적용됩니다. 이것은 직접적인 DOM 조작을 최소화하여 성능 향상을 목표로 합니다.
카운터를 표시하는 간단한 컴포넌트를 생각해 보세요.
// React 유사 의사 코드 function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }
setCount가 호출되면 React는 다음을 수행합니다:
Counter컴포넌트를 다시 렌더링하여 새로운 가상 DOM 트리를 생성합니다.- 이 새로운 트리를 이전 트리와 비교합니다(diff).
<p>태그 내의 텍스트 내용만 변경되었음을 식별합니다.- 실제 DOM에서 해당 특정 텍스트 노드만 업데이트합니다.
이를 통해 브라우저는 전체 <div> 또는 <button> 요소를 다시 렌더링할 필요가 없어 계산을 절약할 수 있습니다.
가상 DOM이 불필요할 수 있는 이유
가상 DOM은 직접적이고 최적화되지 않은 DOM 조작에 비해 상당한 개선을 제공하지만, Svelte와 SolidJS와 같은 프레임워크는 그것이 자체 비용을 가진 추상화이며, 더 직접적인 컴파일러 기반 또는 세분화된 접근 방식이 더 뛰어난 결과를 달성할 수 있다고 주장합니다.
Svelte: 컴파일러 접근 방식
Svelte는 근본적으로 다른 접근 방식을 취합니다. 그것은 런타임에 해석하는 대신, 빌드 단계 동안 컴포넌트 코드를 매우 최적화된 명령형 JavaScript로 변환하는 컴파일러입니다. 이것은 런타임에 가상 DOM이 없다는 것을 의미합니다. Svelte 컴포넌트는 상태 변경 시 DOM을 직접 조작합니다.
동일한 카운터에 대한 Svelte 예제를 살펴보겠습니다.
<!-- Counter.svelte --> <script> let count = 0; function increment() { count += 1; } </script> <div> <p>Count: {count}</p> <button on:click={increment}>Increment</button> </div>
Svelte가 이 컴포넌트를 컴파일할 때, 본질적으로 다음과 같이 보이는 JavaScript 코드를 생성합니다(개념적).
// 생성된 Svelte 출력 (개념적) function create_fragment(ctx) { let p, t0, t1, t2, button; return { c() { // 요소 생성 p = element('p'); t0 = text('Count: '); t1 = text(/*count*/ ctx[0]); t2 = space(); button = element('button'); button.textContent = 'Increment'; listener(button, 'click', /*increment*/ ctx[1]); }, m(target, anchor) { // 요소 마운트 insert(target, p, anchor); append(p, t0); append(p, t1); insert(target, t2, anchor); insert(target, button, anchor); }, p(ctx, [dirty]) { // 요소 업데이트 if (dirty & /*count*/ 1) { // count가 변경된 경우 set_data(t1, /*count*/ ctx[0]); // 직접 텍스트 노드 업데이트 } }, d(detaching) { // 요소 삭제 if (detaching) { detach(p); detach(t2); detach(button); } del_listener(button, 'click', /*increment*/ ctx[1]); } }; }
count가 변경될 때, Svelte의 생성된 코드는 새로운 트리를 생성하고, diff하고, 패치를 적용할 필요 없이 직접 특정 텍스트 노드(t1)를 업데이트합니다. "diffing"은 Svelte가 어떤 상태 변수에 어떤 DOM의 어떤 부분이 의존하는지 정확히 알고 있기 때문에 컴파일 타임에 발생합니다. 이것은 가상 DOM 재조정의 런타임 오버헤드를 제거합니다.
SolidJS: 가상 DOM 없는 세분화된 반응성
SolidJS는 Knockout.js 또는 MobX와 유사한 반응형 프로그래밍 패러다임에서 영감을 얻지만, 이를 JSX에 적용합니다. JSX 템플릿을 실제 DOM 노드로 컴파일하고 상태 변수를 "시그널"로 래핑합니다. 시그널이 변경될 때, SolidJS는 해당 시그널에 의존하는 DOM(또는 다른 계산)의 어떤 부분이 변경되었는지 정확히 알고 해당 부분만 업데이트합니다. 반응형 프리미티브에서 DOM 요소로 일대일 데이터 바인딩을 직접 생성하여 가상 DOM을 완전히 피합니다.
SolidJS에서 카운터를 다시 생각해 봅시다.
// SolidJS import { createSignal } from 'solid-js'; function Counter() { const [count, setCount] = createSignal(0); return ( <div> <p>Count: {count()}</p> {/* count()는 시그널의 값을 읽습니다 */} <button onClick={() => setCount(count() + 1)}>Increment</button> </div> ); }
setCount(count() + 1)이 호출되면 count 시그널의 값이 업데이트됩니다. SolidJS는 <p> 태그의 텍스트 내용 내 count() 호출이 이 시그널에 의존한다는 것을 알고 있습니다. 그런 다음 DOM에서 해당 텍스트 노드만 직접 업데이트합니다. Svelte와 마찬가지로 중간 가상 DOM 트리가 없습니다. SolidJS의 JSX에 대한 컴파일 단계는 시그널 변경에 의해 직접 트리거되는 일련의 DOM 작업을 효과적으로 생성합니다. 이 "세분화된 반응성"은 반드시 필요한 업데이트만 발생함을 의미하며, 이는 매우 효율적인 DOM 조작으로 이어집니다.
핵심 차이점은 React/Vue가 실제 DOM 업데이트를 최소화하기 위해 가상 DOM을 사용하는 반면, Svelte와 SolidJS는 가상 DOM을 완전히 제거함으로써 한 단계 더 나아간다는 것입니다. Svelte는 컴파일 타임 최적화를 통해 직접 DOM을 조작하는 JavaScript를 생성하여 이를 달성합니다. SolidJS는 상태 변경을 특정 DOM 노드에 직접 매핑하는 반응형 그래프를 통해 이를 달성하며, 역시 직접 DOM 작업으로 컴파일됩니다.
결론
가상 DOM은 비효율적인 DOM 조작의 문제에 대한 훌륭한 해결책이었으며 프론트엔드 개발에 혁명을 일으켰습니다. 그러나 프레임워크가 발전함에 따라 우리는 설득력 있는 대안들이 등장하는 것을 보고 있습니다. Svelte와 SolidJS는 런타임에서 컴파일 타임(Svelte)으로 작업을 옮기거나 매우 최적화된 세분화된 반응형 연결(SolidJS)을 설정함으로써 가상 DOM의 오버헤드와 재조정 프로세스를 완전히 우회할 수 있음을 보여줍니다. 이는 더 작은 번들 크기, 더 빠른 런타임 성능, 그리고 종종 개발자에게 더 간단한 정신 모델로 이어집니다. 이 프레임워크들은 가상 DOM이 우리에게 유용했지만, 빠르고 효율적인 웹 애플리케이션을 구축하는 유일한 길이나 궁극적인 길이 아님을 강조합니다. 프론트엔드 프레임워크의 미래는 브라우저의 네이티브 기능과 상호 작용하는 더욱 직접적이고 최적화된 방법에 있을 가능성이 높습니다.

