자바스크립트로 구현하는 Virtual DOM: 렌더링 성능을 극대화하는 최신 기법 2025

콘텐츠 대표 이미지 - 자바스크립트로 구현하는 Virtual DOM: 렌더링 성능을 극대화하는 최신 기법 2025

 

 

🚀 자바스크립트 성능 최적화의 핵심, Virtual DOM을 파헤쳐보자! 💻

2025년 최신 트렌드와 함께 Virtual DOM의 모든 것

🌟 Virtual DOM이 뭐길래 다들 난리야?

안녕! 오늘은 자바스크립트 개발자라면 꼭 알아야 할 Virtual DOM에 대해 함께 알아볼 거야. 2025년 현재, 프론트엔드 개발에서 빼놓을 수 없는 이 기술이 왜 그렇게 중요한지, 어떻게 구현하는지 친구처럼 쉽게 설명해줄게! 🤓

요즘 웹 개발 트렌드를 보면 사용자 경험(UX)이 정말 중요해졌어. 사용자들은 버벅거리는 웹사이트를 참지 못하지. 그래서 렌더링 성능이 프론트엔드 개발의 핵심 과제가 됐어. 이런 상황에서 Virtual DOM은 마치 슈퍼히어로처럼 등장했지! 🦸‍♂️

재능넷 같은 복잡한 웹 플랫폼을 운영하다 보면 사용자 인터페이스의 반응성이 얼마나 중요한지 절실히 느끼게 돼. 수많은 재능 거래가 이루어지는 플랫폼에서 화면이 버벅거린다면? 아마 사용자들은 금방 떠나버릴 거야. 그래서 오늘 이 글이 더욱 의미가 있을 거라 생각해! 📱

🧩 DOM이 뭐길래? 기본부터 알고 가자!

Virtual DOM을 이해하기 전에, 먼저 DOM이 뭔지 알아야 해. DOM(Document Object Model)은 HTML 문서를 트리 구조로 표현한 것이야. 브라우저가 웹 페이지를 어떻게 그려야 할지 이해하는 방식이지.

HTML DOM HEAD BODY 기타 요소 DIV SPAN

위 그림처럼 DOM은 트리 구조로 되어 있어. HTML 문서가 브라우저에 로드되면, 브라우저는 이 문서를 파싱해서 DOM 트리를 만들지. 그리고 이 DOM 트리를 기반으로 화면에 요소들을 그려. 🌳

그런데 문제는 DOM 조작이 비싸다는 거야! DOM을 직접 조작하면 브라우저는 레이아웃을 다시 계산하고, 페인팅을 다시 하는 등의 작업을 수행해. 이런 과정을 '리플로우(reflow)'와 '리페인트(repaint)'라고 하는데, 이게 자주 일어나면 성능이 크게 저하돼. 특히 복잡한 UI를 가진 SPA(Single Page Application)에서는 치명적이지. 😱

🔮 Virtual DOM의 등장: 구세주인가?

여기서 Virtual DOM이 등장해! Virtual DOM은 실제 DOM의 가벼운 복사본이야. 자바스크립트 객체로 메모리에 존재하지. 이 가상의 DOM을 조작하는 건 실제 DOM을 조작하는 것보다 훨씬 빠르고 효율적이야. 왜냐하면 자바스크립트 객체를 조작하는 건 브라우저가 화면을 다시 그리는 것보다 훨씬 가볍거든. 🚀

상태 변경 Virtual DOM 생성 차이점 계산 실제 DOM 업데이트 (최소한의 변경만) 이전 Virtual DOM 화면 렌더링 Virtual DOM - 메모리에 존재 - JS 객체로 구현 - 조작 비용 저렴 Real DOM - 브라우저에 존재 - HTML 요소로 구현 - 조작 비용 비쌈 VS

Virtual DOM의 작동 방식은 간단해:

  1. 상태가 변경되면 전체 UI를 Virtual DOM에 렌더링해.
  2. 이전 Virtual DOM과 새로운 Virtual DOM을 비교해.
  3. 실제로 변경된 부분만 실제 DOM에 적용해.

이렇게 하면 불필요한 DOM 조작을 최소화할 수 있어서 성능이 크게 향상돼! 특히 React, Vue 같은 프레임워크들이 이 개념을 도입해서 큰 성공을 거뒀지. 2025년 현재는 거의 모든 최신 프론트엔드 프레임워크가 이 개념을 활용하고 있어. 🌈

재능넷처럼 다양한 컴포넌트와 상호작용이 많은 웹사이트에서는 Virtual DOM의 이점이 더욱 두드러져. 사용자가 재능을 검색하고, 필터링하고, 메시지를 주고받는 모든 과정에서 UI가 부드럽게 업데이트되니까! 💼

🛠️ 직접 만들어보자! Virtual DOM 구현하기

이제 자바스크립트로 간단한 Virtual DOM을 직접 구현해볼 거야. 물론 React나 Vue처럼 완벽하진 않겠지만, 핵심 개념을 이해하는 데 큰 도움이 될 거야! 🧪

1. Virtual DOM 노드 만들기

먼저 Virtual DOM의 기본 단위인 VNode(Virtual Node)를 만들어보자:


// Virtual DOM 노드 클래스
class VNode {
  constructor(tagName, props, children) {
    this.tagName = tagName;    // HTML 태그 이름 (div, span 등)
    this.props = props || {};  // 속성들 (id, class, style 등)
    this.children = children || []; // 자식 노드들
  }
}

// Virtual DOM 노드 생성 함수
function h(tagName, props, ...children) {
  return new VNode(
    tagName,
    props,
    children.flat().map(child => 
      typeof child === 'string' || typeof child === 'number'
        ? new VNode('text', { textContent: child })
        : child
    )
  );
}
    

위 코드에서 VNode 클래스는 Virtual DOM의 각 노드를 표현해. 그리고 h() 함수는 노드를 쉽게 생성할 수 있게 도와주는 헬퍼 함수야. 이름이 h인 이유는 "hyperscript"의 약자로, HTML을 생성하는 스크립트라는 의미야. 😉

2. Virtual DOM을 실제 DOM으로 변환하기

이제 Virtual DOM 노드를 실제 DOM 요소로 변환하는 함수를 만들어보자:


// Virtual DOM을 실제 DOM으로 변환하는 함수
function createElement(vnode) {
  // 텍스트 노드 처리
  if (vnode.tagName === 'text') {
    return document.createTextNode(vnode.props.textContent);
  }
  
  // 일반 요소 생성
  const element = document.createElement(vnode.tagName);
  
  // 속성 설정
  for (const [key, value] of Object.entries(vnode.props)) {
    if (key === 'style' && typeof value === 'object') {
      Object.assign(element.style, value);
    } else if (key.startsWith('on') && typeof value === 'function') {
      // 이벤트 리스너 처리
      const eventName = key.slice(2).toLowerCase();
      element.addEventListener(eventName, value);
    } else {
      // 일반 속성 처리
      element.setAttribute(key, value);
    }
  }
  
  // 자식 요소 처리
  for (const child of vnode.children) {
    element.appendChild(createElement(child));
  }
  
  return element;
}
    

이 함수는 재귀적으로 Virtual DOM 트리를 순회하면서 각 노드를 실제 DOM 요소로 변환해. 텍스트 노드, 속성, 이벤트 리스너, 자식 요소 등을 모두 처리하지. 🔄

3. DOM 차이점 비교 알고리즘 (Diffing)

Virtual DOM의 핵심은 이전 상태와 새로운 상태의 차이점만 실제 DOM에 적용하는 거야. 이를 위한 간단한 diffing 알고리즘을 구현해보자:


// 두 Virtual DOM 트리의 차이점을 찾아 실제 DOM에 적용하는 함수
function updateElement(parent, newNode, oldNode, index = 0) {
  // 1. 새 노드가 없는 경우 (요소 삭제)
  if (!newNode && oldNode) {
    parent.removeChild(parent.childNodes[index]);
    return;
  }
  
  // 2. 이전 노드가 없는 경우 (새 요소 추가)
  if (newNode && !oldNode) {
    parent.appendChild(createElement(newNode));
    return;
  }
  
  // 3. 두 노드의 타입이 다른 경우 (요소 교체)
  if (newNode.tagName !== oldNode.tagName) {
    parent.replaceChild(
      createElement(newNode),
      parent.childNodes[index]
    );
    return;
  }
  
  // 4. 텍스트 노드인 경우 텍스트 내용만 업데이트
  if (newNode.tagName === 'text' && oldNode.tagName === 'text') {
    if (newNode.props.textContent !== oldNode.props.textContent) {
      parent.childNodes[index].nodeValue = newNode.props.textContent;
    }
    return;
  }
  
  // 5. 속성 업데이트
  updateProps(
    parent.childNodes[index],
    newNode.props,
    oldNode.props
  );
  
  // 6. 자식 노드 재귀적으로 비교
  const newLength = newNode.children.length;
  const oldLength = oldNode.children.length;
  const maxLength = Math.max(newLength, oldLength);
  
  for (let i = 0; i < maxLength; i++) {
    updateElement(
      parent.childNodes[index],
      newNode.children[i],
      oldNode.children[i],
      i
    );
  }
}

// 요소의 속성 업데이트 함수
function updateProps(element, newProps, oldProps = {}) {
  // 이전 속성 중 새로운 속성에 없는 것들 제거
  for (const [key, value] of Object.entries(oldProps)) {
    if (!(key in newProps)) {
      if (key.startsWith('on')) {
        const eventName = key.slice(2).toLowerCase();
        element.removeEventListener(eventName, value);
      } else {
        element.removeAttribute(key);
      }
    }
  }
  
  // 새로운 속성 설정 또는 업데이트
  for (const [key, value] of Object.entries(newProps)) {
    if (oldProps[key] !== value) {
      if (key === 'style' && typeof value === 'object') {
        // 스타일 객체 처리
        Object.assign(element.style, value);
      } else if (key.startsWith('on') && typeof value === 'function') {
        // 이벤트 리스너 처리
        const eventName = key.slice(2).toLowerCase();
        if (oldProps[key]) {
          element.removeEventListener(eventName, oldProps[key]);
        }
        element.addEventListener(eventName, value);
      } else {
        // 일반 속성 처리
        element.setAttribute(key, value);
      }
    }
  }
}
    

위 코드는 두 Virtual DOM 트리를 비교하고 실제 DOM을 효율적으로 업데이트하는 알고리즘이야. 다음과 같은 경우를 처리해:

  1. 요소 삭제: 새 노드가 없는 경우
  2. 요소 추가: 이전 노드가 없는 경우
  3. 요소 교체: 노드 타입이 다른 경우
  4. 텍스트 업데이트: 텍스트 노드인 경우
  5. 속성 업데이트: 요소의 속성이 변경된 경우
  6. 자식 노드 비교: 재귀적으로 자식 노드들을 비교

이 알고리즘은 React나 Vue의 실제 구현보다는 단순하지만, Virtual DOM의 핵심 개념을 잘 보여주고 있어. 🧠

4. 간단한 예제로 사용해보기

이제 우리가 만든 Virtual DOM 라이브러리를 사용해서 간단한 카운터 앱을 만들어보자:


// 초기 상태
let count = 0;

// Virtual DOM 트리 생성 함수
function createVApp(count) {
  return h('div', { id: 'app' }, [
    h('h1', {}, `카운터 앱: ${count}`),
    h('button', { 
      onclick: () => renderApp(count + 1),
      style: { marginRight: '10px' }
    }, '+'),
    h('button', { 
      onclick: () => renderApp(count - 1) 
    }, '-')
  ]);
}

// 이전 Virtual DOM 트리 저장 변수
let oldVApp = null;
const rootElement = document.getElementById('root');

// 앱 렌더링 함수
function renderApp(newCount) {
  count = newCount;
  const newVApp = createVApp(count);
  
  // 최초 렌더링인 경우
  if (!oldVApp) {
    rootElement.appendChild(createElement(newVApp));
  } else {
    // 차이점 비교 후 업데이트
    updateElement(rootElement, newVApp, oldVApp);
  }
  
  // 현재 상태를 이전 상태로 저장
  oldVApp = newVApp;
}

// 초기 렌더링
renderApp(count);
    

이 예제는 +/- 버튼으로 카운터를 증가/감소시키는 간단한 앱이야. 버튼을 클릭할 때마다 새로운 Virtual DOM 트리가 생성되고, 이전 트리와 비교해서 변경된 부분만 실제 DOM에 적용돼. 🔢

실행 결과 예시:

카운터 앱: 0

🚀 성능 최적화: Virtual DOM을 더 빠르게!

지금까지 기본적인 Virtual DOM을 구현해봤어. 하지만 실제 프로덕션 환경에서는 더 많은 최적화가 필요해. 2025년 현재 가장 많이 사용되는 최적화 기법들을 알아보자! 💨

1. 키(Key) 사용하기

리스트 렌더링에서 키는 정말 중요해! 키를 사용하면 Virtual DOM이 요소의 변경, 추가, 삭제를 더 효율적으로 파악할 수 있어. 우리 구현에도 이 기능을 추가해보자:


// VNode 클래스에 key 속성 추가
class VNode {
  constructor(tagName, props, children) {
    this.tagName = tagName;
    this.props = props || {};
    this.key = props.key; // 키 속성 추가
    this.children = children || [];
  }
}

// updateElement 함수 수정 (키 기반 비교 추가)
function updateElement(parent, newNode, oldNode, index = 0) {
  // ... 기존 코드 ...
  
  // 키가 있는 경우 키 기반으로 자식 노드 비교
  if (newNode.children.some(child => child.key != null)) {
    updateChildrenWithKeys(
      parent.childNodes[index],
      newNode.children,
      oldNode.children
    );
  } else {
    // 기존 방식대로 인덱스 기반 비교
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    const maxLength = Math.max(newLength, oldLength);
    
    for (let i = 0; i < maxLength; i++) {
      updateElement(
        parent.childNodes[index],
        newNode.children[i],
        oldNode.children[i],
        i
      );
    }
  }
}

// 키 기반 자식 노드 비교 함수
function updateChildrenWithKeys(parent, newChildren, oldChildren) {
  // 이전 자식 노드들을 키로 맵핑
  const oldKeyMap = {};
  oldChildren.forEach((child, i) => {
    if (child.key != null) {
      oldKeyMap[child.key] = { node: child, index: i };
    }
  });
  
  let lastIndex = 0;
  
  // 새 자식 노드들을 순회하며 처리
  newChildren.forEach((newChild, i) => {
    const key = newChild.key;
    
    if (key == null) {
      // 키가 없는 경우 일반적인 방식으로 처리
      updateElement(parent, newChild, oldChildren[i], i);
      return;
    }
    
    const oldChildInfo = oldKeyMap[key];
    
    if (oldChildInfo) {
      // 키가 일치하는 이전 노드가 있는 경우
      const oldChild = oldChildInfo.node;
      const oldIndex = oldChildInfo.index;
      
      // 노드 업데이트
      updateElement(parent, newChild, oldChild, oldIndex);
      
      // 노드 위치 이동이 필요한 경우
      if (oldIndex < lastIndex) {
        const currentNode = parent.childNodes[oldIndex];
        parent.insertBefore(currentNode, parent.childNodes[i]);
      } else {
        lastIndex = oldIndex;
      }
    } else {
      // 새로운 키인 경우 노드 추가
      const newElement = createElement(newChild);
      
      if (i < parent.childNodes.length) {
        parent.insertBefore(newElement, parent.childNodes[i]);
      } else {
        parent.appendChild(newElement);
      }
    }
  });
  
  // 새 자식 노드에 없는 이전 키를 가진 노드 제거
  for (const key in oldKeyMap) {
    if (!newChildren.some(child => child.key === key)) {
      const oldIndex = oldKeyMap[key].index;
      parent.removeChild(parent.childNodes[oldIndex]);
    }
  }
}
    

이제 리스트 렌더링에서 키를 사용할 수 있어! 예를 들어, 할 일 목록을 렌더링할 때:


function createTodoList(todos) {
  return h('ul', {}, 
    todos.map(todo => 
      h('li', { key: todo.id }, todo.text)
    )
  );
}
    

이렇게 하면 할 일 항목이 추가, 삭제, 순서 변경될 때 Virtual DOM이 훨씬 효율적으로 실제 DOM을 업데이트할 수 있어. 🔑

2. 메모이제이션(Memoization) 활용하기

동일한 입력에 대해 동일한 Virtual DOM 트리를 반환하는 컴포넌트는 메모이제이션을 통해 불필요한 재계산을 방지할 수 있어:


// 메모이제이션 함수
function memoize(fn) {
  const cache = new Map();
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      return cache.get(key);
    }
    
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

// 메모이제이션된 컴포넌트
const MemoizedUserProfile = memoize((user) => {
  return h('div', { class: 'user-profile' }, [
    h('img', { src: user.avatar, alt: user.name }),
    h('h3', {}, user.name),
    h('p', {}, user.bio)
  ]);
});

// 사용 예시
function renderUserList(users) {
  return h('div', { class: 'user-list' },
    users.map(user => MemoizedUserProfile(user))
  );
}
    

이 기법은 React의 React.memo()나 Vue의 computed 속성과 유사한 개념이야. 입력이 변경되지 않으면 이전에 계산된 결과를 재사용해서 성능을 크게 향상시킬 수 있지! 🧮

3. 지연 렌더링(Lazy Rendering) 구현하기

화면에 보이지 않는 요소는 렌더링을 지연시켜 초기 로딩 성능을 개선할 수 있어:


// 지연 렌더링 컴포넌트
function LazyComponent(renderFn, loadCondition) {
  let rendered = null;
  
  return function() {
    // 로딩 조건이 충족되면 실제 컴포넌트 렌더링
    if (loadCondition()) {
      if (!rendered) {
        rendered = renderFn();
      }
      return rendered;
    }
    
    // 로딩 조건이 충족되지 않으면 플레이스홀더 반환
    return h('div', { class: 'lazy-placeholder' }, 'Loading...');
  };
}

// 사용 예시: 뷰포트에 들어왔을 때만 렌더링
const HeavyComponent = LazyComponent(
  () => h('div', {}, [
    // 복잡한 컴포넌트 내용
    h('h2', {}, '복잡한 차트'),
    // ... 많은 데이터를 표시하는 컴포넌트
  ]),
  () => {
    // 요소가 뷰포트에 있는지 확인하는 로직
    const element = document.querySelector('.lazy-placeholder');
    if (!element) return false;
    
    const rect = element.getBoundingClientRect();
    return (
      rect.top >= 0 &&
      rect.left >= 0 &&
      rect.bottom <= window.innerHeight &&
      rect.right <= window.innerWidth
    );
  }
);
    

이 패턴은 무한 스크롤이나 대규모 데이터 표시에 매우 유용해. 사용자가 볼 수 없는 콘텐츠는 렌더링하지 않아 초기 로딩 시간과 메모리 사용량을 크게 줄일 수 있지! 📜

4. 배치 업데이트(Batch Updates) 구현하기

여러 상태 변경을 한 번에 처리해 불필요한 렌더링을 방지할 수 있어:


// 배치 업데이트 관리자
const BatchUpdateManager = {
  updates: [],
  scheduled: false,
  
  // 업데이트 예약
  schedule(updateFn) {
    this.updates.push(updateFn);
    
    if (!this.scheduled) {
      this.scheduled = true;
      
      // 다음 프레임에 일괄 처리
      requestAnimationFrame(() => {
        this.processUpdates();
      });
    }
  },
  
  // 예약된 모든 업데이트 처리
  processUpdates() {
    const updates = this.updates;
    this.updates = [];
    this.scheduled = false;
    
    // 모든 상태 업데이트 적용
    updates.forEach(update => update());
    
    // 한 번만 렌더링
    renderApp(count);
  }
};

// 사용 예시
function incrementCount() {
  BatchUpdateManager.schedule(() => {
    count++;
  });
}

function decrementCount() {
  BatchUpdateManager.schedule(() => {
    count--;
  });
}

// 여러 업데이트를 동시에 적용
function resetAndDouble() {
  BatchUpdateManager.schedule(() => {
    count = 0;
  });
  
  BatchUpdateManager.schedule(() => {
    count = count * 2;
  });
  
  // 두 업데이트가 한 번의 렌더링으로 처리됨
}
    

이 기법은 React의 상태 업데이트 배치 처리와 유사해. 여러 상태 변경을 한 번의 렌더링 사이클로 처리해서 성능을 크게 향상시킬 수 있어! 📊

🌍 실전 예제: 간단한 TODO 앱 만들기

이제 우리가 구현한 Virtual DOM 라이브러리를 사용해서 간단한 TODO 앱을 만들어보자! 이 예제를 통해 Virtual DOM이 실제로 어떻게 동작하는지 더 잘 이해할 수 있을 거야. 📝


// 앱 상태
const state = {
  todos: [
    { id: 1, text: 'Virtual DOM 공부하기', completed: false },
    { id: 2, text: '자바스크립트 심화 학습', completed: true },
    { id: 3, text: '재능넷에 프로젝트 올리기', completed: false }
  ],
  newTodoText: '',
  filter: 'all' // 'all', 'active', 'completed'
};

// 액션 처리 함수들
const actions = {
  addTodo() {
    if (state.newTodoText.trim()) {
      state.todos.push({
        id: Date.now(),
        text: state.newTodoText,
        completed: false
      });
      state.newTodoText = '';
      renderApp();
    }
  },
  
  removeTodo(id) {
    state.todos = state.todos.filter(todo => todo.id !== id);
    renderApp();
  },
  
  toggleTodo(id) {
    state.todos = state.todos.map(todo => 
      todo.id === id 
        ? { ...todo, completed: !todo.completed } 
        : todo
    );
    renderApp();
  },
  
  updateNewTodoText(text) {
    state.newTodoText = text;
    renderApp();
  },
  
  setFilter(filter) {
    state.filter = filter;
    renderApp();
  },
  
  clearCompleted() {
    state.todos = state.todos.filter(todo => !todo.completed);
    renderApp();
  }
};

// 필터링된 할일 목록 가져오기
function getFilteredTodos() {
  switch (state.filter) {
    case 'active':
      return state.todos.filter(todo => !todo.completed);
    case 'completed':
      return state.todos.filter(todo => todo.completed);
    default:
      return state.todos;
  }
}

// 앱 컴포넌트
function TodoApp() {
  const filteredTodos = getFilteredTodos();
  
  return h('div', { class: 'todo-app' }, [
    h('h1', {}, 'Virtual DOM Todo App'),
    
    // 새 할일 입력 폼
    h('div', { class: 'add-todo' }, [
      h('input', { 
        type: 'text',
        value: state.newTodoText,
        placeholder: '할 일을 입력하세요',
        oninput: e => actions.updateNewTodoText(e.target.value),
        onkeypress: e => e.key === 'Enter' && actions.addTodo()
      }),
      h('button', { 
        onclick: actions.addTodo 
      }, '추가')
    ]),
    
    // 필터 버튼들
    h('div', { class: 'filters' }, [
      h('button', { 
        class: state.filter === 'all' ? 'active' : '',
        onclick: () => actions.setFilter('all') 
      }, '전체'),
      h('button', { 
        class: state.filter === 'active' ? 'active' : '',
        onclick: () => actions.setFilter('active') 
      }, '미완료'),
      h('button', { 
        class: state.filter === 'completed' ? 'active' : '',
        onclick: () => actions.setFilter('completed') 
      }, '완료'),
      h('button', { 
        onclick: actions.clearCompleted 
      }, '완료 항목 삭제')
    ]),
    
    // 할일 목록
    h('ul', { class: 'todo-list' }, 
      filteredTodos.map(todo => 
        h('li', { 
          key: todo.id,
          class: todo.completed ? 'completed' : '' 
        }, [
          h('input', { 
            type: 'checkbox',
            checked: todo.completed,
            onchange: () => actions.toggleTodo(todo.id)
          }),
          h('span', {}, todo.text),
          h('button', { 
            class: 'delete',
            onclick: () => actions.removeTodo(todo.id) 
          }, '삭제')
        ])
      )
    ),
    
    // 통계
    h('div', { class: 'stats' }, [
      h('span', {}, `총 ${state.todos.length}개 항목 중 `),
      h('span', {}, `${state.todos.filter(t => t.completed).length}개 완료`)
    ])
  ]);
}

// 이전 Virtual DOM 트리 저장 변수
let oldVApp = null;
const rootElement = document.getElementById('app');

// 앱 렌더링 함수
function renderApp() {
  const newVApp = TodoApp();
  
  // 최초 렌더링인 경우
  if (!oldVApp) {
    rootElement.appendChild(createElement(newVApp));
  } else {
    // 차이점 비교 후 업데이트
    updateElement(rootElement, newVApp, oldVApp);
  }
  
  // 현재 상태를 이전 상태로 저장
  oldVApp = newVApp;
}

// 초기 렌더링
renderApp();
    

이 TODO 앱은 우리가 구현한 Virtual DOM 라이브러리를 사용해 다음 기능을 제공해:

  1. 할 일 추가, 삭제, 완료 상태 토글
  2. 필터링 (전체/미완료/완료)
  3. 완료된 항목 일괄 삭제
  4. 통계 표시

Virtual DOM의 핵심 장점은 이런 상호작용이 많은 앱에서 빛을 발해. 예를 들어, 할 일 항목을 토글하면 해당 항목만 업데이트되고, 필터를 변경하면 목록만 다시 렌더링돼. 전체 DOM을 다시 그리는 것보다 훨씬 효율적이지! 🎯

Virtual DOM Todo App 할 일을 입력하세요 추가 전체 미완료 완료 완료 항목 삭제 Virtual DOM 공부하기 삭제 자바스크립트 심화 학습 삭제 재능넷에 프로젝트 올리기 삭제 총 3개 항목 중 1개 완료

🔍 프레임워크 비교: React, Vue, 그리고 우리의 구현

우리가 직접 구현한 Virtual DOM 라이브러리는 기본적인 개념을 이해하는 데 좋지만, React나 Vue 같은 프레임워크들은 훨씬 더 정교하고 최적화되어 있어. 이 섹션에서는 우리의 구현과 인기 있는 프레임워크들을 비교해볼게! 🧐

1. React의 Virtual DOM

React는 Virtual DOM을 가장 대중화한 프레임워크야. 2025년 현재 React 19버전에서는 다음과 같은 발전된 기능들을 제공해:

  1. Fiber 아키텍처: 렌더링 작업을 작은 단위로 나누어 중단하고 재개할 수 있게 해서 메인 스레드 차단을 방지해.
  2. 동시성 모드(Concurrent Mode): 여러 UI 업데이트를 동시에 준비하고 우선순위에 따라 렌더링할 수 있어.
  3. 서버 컴포넌트(Server Components): 일부 컴포넌트를 서버에서 렌더링해 클라이언트로 전송함으로써 번들 크기와 로딩 시간을 줄여.
  4. 자동 배치 업데이트: 여러 상태 업데이트를 자동으로 배치 처리해 불필요한 렌더링을 방지해.

React 코드 예시:


// React 컴포넌트 예시 (2025년 최신 문법)
function TodoApp() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Virtual DOM 공부하기', completed: false },
    { id: 2, text: '자바스크립트 심화 학습', completed: true }
  ]);
  
  const addTodo = (text) => {
    setTodos([...todos, {
      id: Date.now(),
      text,
      completed: false
    }]);
  };
  
  return (
    <div classname="todo-app">
      <h1>React Todo App</h1>
      <todoinput onadd="{addTodo}"></todoinput>
      <todolist todos="{todos}" ontoggle="{(id)"> {
          setTodos(todos.map(todo => 
            todo.id === id 
              ? { ...todo, completed: !todo.completed } 
              : todo
          ));
        }} 
      />
    </todolist></div>
  );
}
    

2. Vue의 Virtual DOM

Vue는 React보다 더 세밀한 반응성 시스템을 가지고 있어. 2025년 현재 Vue 4에서는 다음과 같은 특징이 있어:

  1. 세밀한 반응성(Fine-grained Reactivity): 컴포넌트 수준이 아닌 변수 수준에서 반응성을 제공해.
  2. 컴파일 타임 최적화: 템플릿을 분석해 정적인 부분과 동적인 부분을 구분하고, 변경이 필요한 부분만 업데이트해.
  3. Proxy 기반 반응성: ES6 Proxy를 사용해 객체의 변경을 효율적으로 감지해.
  4. 정적 호이스팅: 변경되지 않는 노드를 한 번만 생성하고 재사용해 메모리와 성능을 최적화해.

Vue 코드 예시:


<!-- Vue 컴포넌트 예시 (2025년 최신 문법) -->
<script setup>
import { ref } from 'vue';

const todos = ref([
  { id: 1, text: 'Virtual DOM 공부하기', completed: false },
  { id: 2, text: '자바스크립트 심화 학습', completed: true }
]);

function addTodo(text) {
  todos.value.push({
    id: Date.now(),
    text,
    completed: false
  });
}

function toggleTodo(id) {
  const todo = todos.value.find(todo => todo.id === id);
  if (todo) {
    todo.completed = !todo.completed;
  }
}
</script>

<template>
  <div class="todo-app">
    <h1>Vue Todo App</h1>
    <todoinput></todoinput>
    <todolist :todos="todos"></todolist>
  </div>
</template>
    

3. 우리 구현과의 차이점

우리가 구현한 Virtual DOM 라이브러리와 React, Vue의 주요 차이점은 다음과 같아:

특징 우리 구현 React Vue 렌더링 알고리즘 단순 재귀적 비교 Fiber 아키텍처 (작업 분할 & 우선순위) 컴파일 타임 최적화 (정적/동적 분석) 반응성 시스템 수동 상태 관리 단방향 데이터 흐름 (useState, useReducer) 세밀한 반응성 (Proxy 기반) 성능 최적화 기본적인 키 기반 비교 수동 메모이제이션 useMemo, useCallback React.memo, Suspense computed, watchEffect v-once, v-memo 개발자 경험 기본적인 API 풍부한 생태계 강력한 개발자 도구 직관적인 API 상세한 컴파일 경고 확장성 제한적 훅 시스템 서버 컴포넌트 컴포지션 API 플러그인 시스템

이 비교를 통해 알 수 있듯이, 우리의 구현은 기본적인 개념을 이해하는 데는 좋지만 실제 프로덕션 환경에서는 React나 Vue 같은 검증된 프레임워크를 사용하는 것이 더 좋아. 그래도 Virtual DOM의 내부 작동 방식을 이해하면 이런 프레임워크를 더 효율적으로 사용할 수 있지! 🧠

🎯 결론: Virtual DOM의 힘을 활용하자!

지금까지 Virtual DOM의 개념부터 직접 구현, 최적화 기법, 그리고 미래 트렌드까지 살펴봤어. 이제 Virtual DOM이 왜 현대 웹 개발에서 중요한지 이해했을 거야! 🧠

Virtual DOM은 단순한 기술적 트릭이 아니라 사용자 경험을 향상시키는 강력한 도구야. 복잡한 웹 애플리케이션에서 성능을 유지하면서도 개발자 경험을 향상시키는 데 큰 역할을 하고 있지.

직접 구현해봄으로써 내부 작동 방식을 이해하게 되었고, 이는 React나 Vue 같은 프레임워크를 더 효과적으로 사용하는 데 도움이 될 거야. 특히 성능 최적화가 필요한 상황에서 Virtual DOM의 원리를 이해하고 있다면 더 나은 결정을 내릴 수 있을 거야. 🚀

재능넷처럼 다양한 기능과 상호작용이 필요한 웹 플랫폼을 개발할 때, Virtual DOM의 개념을 잘 활용한다면 사용자들에게 더 부드럽고 반응성 좋은 경험을 제공할 수 있을 거야. 결국 좋은 사용자 경험은 플랫폼의 성공으로 이어지니까! 💼

앞으로도 Virtual DOM 기술은 계속 발전할 거야. 컴파일 타임 최적화, WebAssembly 활용, AI 기반 예측적 렌더링 등 새로운 기술들이 등장하면서 웹 개발의 패러다임도 계속 변화할 거야. 이런 변화에 발맞춰 계속 공부하고 발전해 나가자! 🌱

이 글이 Virtual DOM에 대한 이해를 높이는 데 도움이 되었길 바라! 혹시 더 궁금한 점이 있거나 프로젝트에 도움이 필요하다면, 재능넷에서 전문가들의 도움을 받아보는 것도 좋은 방법이야. 다양한 개발자들이 자신의 재능을 공유하고 있으니까! 👨‍💻👩‍💻

함께 더 나은 웹을 만들어 나가자! 화이팅! 💪

🌟 Virtual DOM이 뭐길래 다들 난리야?

안녕! 오늘은 자바스크립트 개발자라면 꼭 알아야 할 Virtual DOM에 대해 함께 알아볼 거야. 2025년 현재, 프론트엔드 개발에서 빼놓을 수 없는 이 기술이 왜 그렇게 중요한지, 어떻게 구현하는지 친구처럼 쉽게 설명해줄게! 🤓

요즘 웹 개발 트렌드를 보면 사용자 경험(UX)이 정말 중요해졌어. 사용자들은 버벅거리는 웹사이트를 참지 못하지. 그래서 렌더링 성능이 프론트엔드 개발의 핵심 과제가 됐어. 이런 상황에서 Virtual DOM은 마치 슈퍼히어로처럼 등장했지! 🦸‍♂️

재능넷 같은 복잡한 웹 플랫폼을 운영하다 보면 사용자 인터페이스의 반응성이 얼마나 중요한지 절실히 느끼게 돼. 수많은 재능 거래가 이루어지는 플랫폼에서 화면이 버벅거린다면? 아마 사용자들은 금방 떠나버릴 거야. 그래서 오늘 이 글이 더욱 의미가 있을 거라 생각해! 📱

🧩 DOM이 뭐길래? 기본부터 알고 가자!

Virtual DOM을 이해하기 전에, 먼저 DOM이 뭔지 알아야 해. DOM(Document Object Model)은 HTML 문서를 트리 구조로 표현한 것이야. 브라우저가 웹 페이지를 어떻게 그려야 할지 이해하는 방식이지.

HTML DOM HEAD BODY 기타 요소 DIV SPAN

위 그림처럼 DOM은 트리 구조로 되어 있어. HTML 문서가 브라우저에 로드되면, 브라우저는 이 문서를 파싱해서 DOM 트리를 만들지. 그리고 이 DOM 트리를 기반으로 화면에 요소들을 그려. 🌳

그런데 문제는 DOM 조작이 비싸다는 거야! DOM을 직접 조작하면 브라우저는 레이아웃을 다시 계산하고, 페인팅을 다시 하는 등의 작업을 수행해. 이런 과정을 '리플로우(reflow)'와 '리페인트(repaint)'라고 하는데, 이게 자주 일어나면 성능이 크게 저하돼. 특히 복잡한 UI를 가진 SPA(Single Page Application)에서는 치명적이지. 😱

🔮 Virtual DOM의 등장: 구세주인가?

여기서 Virtual DOM이 등장해! Virtual DOM은 실제 DOM의 가벼운 복사본이야. 자바스크립트 객체로 메모리에 존재하지. 이 가상의 DOM을 조작하는 건 실제 DOM을 조작하는 것보다 훨씬 빠르고 효율적이야. 왜냐하면 자바스크립트 객체를 조작하는 건 브라우저가 화면을 다시 그리는 것보다 훨씬 가볍거든. 🚀

상태 변경 Virtual DOM 생성 차이점 계산 실제 DOM 업데이트 (최소한의 변경만) 이전 Virtual DOM 화면 렌더링 Virtual DOM - 메모리에 존재 - JS 객체로 구현 - 조작 비용 저렴 Real DOM - 브라우저에 존재 - HTML 요소로 구현 - 조작 비용 비쌈 VS

Virtual DOM의 작동 방식은 간단해:

  1. 상태가 변경되면 전체 UI를 Virtual DOM에 렌더링해.
  2. 이전 Virtual DOM과 새로운 Virtual DOM을 비교해.
  3. 실제로 변경된 부분만 실제 DOM에 적용해.

이렇게 하면 불필요한 DOM 조작을 최소화할 수 있어서 성능이 크게 향상돼! 특히 React, Vue 같은 프레임워크들이 이 개념을 도입해서 큰 성공을 거뒀지. 2025년 현재는 거의 모든 최신 프론트엔드 프레임워크가 이 개념을 활용하고 있어. 🌈

재능넷처럼 다양한 컴포넌트와 상호작용이 많은 웹사이트에서는 Virtual DOM의 이점이 더욱 두드러져. 사용자가 재능을 검색하고, 필터링하고, 메시지를 주고받는 모든 과정에서 UI가 부드럽게 업데이트되니까! 💼

🛠️ 직접 만들어보자! Virtual DOM 구현하기

이제 자바스크립트로 간단한 Virtual DOM을 직접 구현해볼 거야. 물론 React나 Vue처럼 완벽하진 않겠지만, 핵심 개념을 이해하는 데 큰 도움이 될 거야! 🧪

1. Virtual DOM 노드 만들기

먼저 Virtual DOM의 기본 단위인 VNode(Virtual Node)를 만들어보자:


// Virtual DOM 노드 클래스
class VNode {
  constructor(tagName, props, children) {
    this.tagName = tagName;    // HTML 태그 이름 (div, span 등)
    this.props = props || {};  // 속성들 (id, class, style 등)
    this.children = children || []; // 자식 노드들
  }
}

// Virtual DOM 노드 생성 함수
function h(tagName, props, ...children) {
  return new VNode(
    tagName,
    props,
    children.flat().map(child => 
      typeof child === 'string' || typeof child === 'number'
        ? new VNode('text', { textContent: child })
        : child
    )
  );
}
    

위 코드에서 VNode 클래스는 Virtual DOM의 각 노드를 표현해. 그리고 h() 함수는 노드를 쉽게 생성할 수 있게 도와주는 헬퍼 함수야. 이름이 h인 이유는 "hyperscript"의 약자로, HTML을 생성하는 스크립트라는 의미야. 😉

2. Virtual DOM을 실제 DOM으로 변환하기

이제 Virtual DOM 노드를 실제 DOM 요소로 변환하는 함수를 만들어보자:


// Virtual DOM을 실제 DOM으로 변환하는 함수
function createElement(vnode) {
  // 텍스트 노드 처리
  if (vnode.tagName === 'text') {
    return document.createTextNode(vnode.props.textContent);
  }
  
  // 일반 요소 생성
  const element = document.createElement(vnode.tagName);
  
  // 속성 설정
  for (const [key, value] of Object.entries(vnode.props)) {
    if (key === 'style' && typeof value === 'object') {
      Object.assign(element.style, value);
    } else if (key.startsWith('on') && typeof value === 'function') {
      // 이벤트 리스너 처리
      const eventName = key.slice(2).toLowerCase();
      element.addEventListener(eventName, value);
    } else {
      // 일반 속성 처리
      element.setAttribute(key, value);
    }
  }
  
  // 자식 요소 처리
  for (const child of vnode.children) {
    element.appendChild(createElement(child));
  }
  
  return element;
}
    

이 함수는 재귀적으로 Virtual DOM 트리를 순회하면서 각 노드를 실제 DOM 요소로 변환해. 텍스트 노드, 속성, 이벤트 리스너, 자식 요소 등을 모두 처리하지. 🔄

3. DOM 차이점 비교 알고리즘 (Diffing)

Virtual DOM의 핵심은 이전 상태와 새로운 상태의 차이점만 실제 DOM에 적용하는 거야. 이를 위한 간단한 diffing 알고리즘을 구현해보자:


// 두 Virtual DOM 트리의 차이점을 찾아 실제 DOM에 적용하는 함수
function updateElement(parent, newNode, oldNode, index = 0) {
  // 1. 새 노드가 없는 경우 (요소 삭제)
  if (!newNode && oldNode) {
    parent.removeChild(parent.childNodes[index]);
    return;
  }
  
  // 2. 이전 노드가 없는 경우 (새 요소 추가)
  if (newNode && !oldNode) {
    parent.appendChild(createElement(newNode));
    return;
  }
  
  // 3. 두 노드의 타입이 다른 경우 (요소 교체)
  if (newNode.tagName !== oldNode.tagName) {
    parent.replaceChild(
      createElement(newNode),
      parent.childNodes[index]
    );
    return;
  }
  
  // 4. 텍스트 노드인 경우 텍스트 내용만 업데이트
  if (newNode.tagName === 'text' && oldNode.tagName === 'text') {
    if (newNode.props.textContent !== oldNode.props.textContent) {
      parent.childNodes[index].nodeValue = newNode.props.textContent;
    }
    return;
  }
  
  // 5. 속성 업데이트
  updateProps(
    parent.childNodes[index],
    newNode.props,
    oldNode.props
  );
  
  // 6. 자식 노드 재귀적으로 비교
  const newLength = newNode.children.length;
  const oldLength = oldNode.children.length;
  const maxLength = Math.max(newLength, oldLength);
  
  for (let i = 0; i < maxLength; i++) {
    updateElement(
      parent.childNodes[index],
      newNode.children[i],
      oldNode.children[i],
      i
    );
  }
}

// 요소의 속성 업데이트 함수
function updateProps(element, newProps, oldProps = {}) {
  // 이전 속성 중 새로운 속성에 없는 것들 제거
  for (const [key, value] of Object.entries(oldProps)) {
    if (!(key in newProps)) {
      if (key.startsWith('on')) {
        const eventName = key.slice(2).toLowerCase();
        element.removeEventListener(eventName, value);
      } else {
        element.removeAttribute(key);
      }
    }
  }
  
  // 새로운 속성 설정 또는 업데이트
  for (const [key, value] of Object.entries(newProps)) {
    if (oldProps[key] !== value) {
      if (key === 'style' && typeof value === 'object') {
        // 스타일 객체 처리
        Object.assign(element.style, value);
      } else if (key.startsWith('on') && typeof value === 'function') {
        // 이벤트 리스너 처리
        const eventName = key.slice(2).toLowerCase();
        if (oldProps[key]) {
          element.removeEventListener(eventName, oldProps[key]);
        }
        element.addEventListener(eventName, value);
      } else {
        // 일반 속성 처리
        element.setAttribute(key, value);
      }
    }
  }
}
    

위 코드는 두 Virtual DOM 트리를 비교하고 실제 DOM을 효율적으로 업데이트하는 알고리즘이야. 다음과 같은 경우를 처리해:

  1. 요소 삭제: 새 노드가 없는 경우
  2. 요소 추가: 이전 노드가 없는 경우
  3. 요소 교체: 노드 타입이 다른 경우
  4. 텍스트 업데이트: 텍스트 노드인 경우
  5. 속성 업데이트: 요소의 속성이 변경된 경우
  6. 자식 노드 비교: 재귀적으로 자식 노드들을 비교

이 알고리즘은 React나 Vue의 실제 구현보다는 단순하지만, Virtual DOM의 핵심 개념을 잘 보여주고 있어. 🧠

4. 간단한 예제로 사용해보기

이제 우리가 만든 Virtual DOM 라이브러리를 사용해서 간단한 카운터 앱을 만들어보자:


// 초기 상태
let count = 0;

// Virtual DOM 트리 생성 함수
function createVApp(count) {
  return h('div', { id: 'app' }, [
    h('h1', {}, `카운터 앱: ${count}`),
    h('button', { 
      onclick: () => renderApp(count + 1),
      style: { marginRight: '10px' }
    }, '+'),
    h('button', { 
      onclick: () => renderApp(count - 1) 
    }, '-')
  ]);
}

// 이전 Virtual DOM 트리 저장 변수
let oldVApp = null;
const rootElement = document.getElementById('root');

// 앱 렌더링 함수
function renderApp(newCount) {
  count = newCount;
  const newVApp = createVApp(count);
  
  // 최초 렌더링인 경우
  if (!oldVApp) {
    rootElement.appendChild(createElement(newVApp));
  } else {
    // 차이점 비교 후 업데이트
    updateElement(rootElement, newVApp, oldVApp);
  }
  
  // 현재 상태를 이전 상태로 저장
  oldVApp = newVApp;
}

// 초기 렌더링
renderApp(count);
    

이 예제는 +/- 버튼으로 카운터를 증가/감소시키는 간단한 앱이야. 버튼을 클릭할 때마다 새로운 Virtual DOM 트리가 생성되고, 이전 트리와 비교해서 변경된 부분만 실제 DOM에 적용돼. 🔢

실행 결과 예시:

카운터 앱: 0