DOM API 개념 및 Virtual DOM과 차이점

ByEunwoo
javascript

DOM 과 DOM API를 알아보기 전에 전반적인 브라우저의 렌더링 과정을 먼저 알아보자.

브라우저의 렌더링 과정

Browser Rendering

  • 1️⃣ HTML 파싱 및 DOM 트리 생성
    • 브라우저가 HTML을 읽고, DOM 트리를 생성
  • 2️⃣ CSS 파싱 및 CSSOM 트리 생성
    • CSS를 읽고, CSSOM을 생성
  • 3️⃣ 렌더 트리(Render Tree) 생성
    • DOM과 CSSOM을 결합하여 렌더 트리를 구성
  • 4️⃣ Reflow (Layout)
    • 렌더 트리의 각 노드들이 브라우저의 Viewport 내에서 어느 위치에 어떤 크기로 배치돼야 하는지에 대한 정보를 계산
    • Layout 단계를 통해 %, vh, vw 등과 같은 상대적인 속성이 px 단위로 변환
  • 5️⃣ Repaint (Paint)
    • 렌더 트리의 각 노드들을 모니터에 실제 픽셀로 그리는 단계
    • 요소의 스타일(색상, 그림자, 글꼴 등)을 적용하여 화면에 그림
  • 6️⃣ 합성 (Compositing) 및 화면에 표시
    • 레이어를 합성하여 화면에 표시

React에서의 렌더링 과정

React는 Vanilla JS와 동일한 렌더링 과정을 따르지만, Virtual DOM을 활용하여 성능을 최적화한다.

  • 컴포넌트 렌더링 (Virtual DOM 생성)
    • JSX 코드가 React.createElement()로 변환되어 Virtual DOM을 생성.
  • 렌더 트리 생성 (하지만 실제 DOM에는 적용되지 않음)
    • Virtual DOM에서 변경 사항을 먼저 적용하고, 실제 DOM에는 영향을 주지 않음.
  • Diffing 알고리즘을 통해 변경 사항 비교
    • React는 이전 Virtual DOM과 새로운 Virtual DOM을 비교하여 변경된 부분만 추적.
  • 필요한 부분만 실제 DOM 업데이트
    • 브라우저가 최소한의 Reflow와 Repaint로 업데이트하도록 최적화.

DOM (Document Object Model) 이란?

브라우저는 웹 문서를 로드한 후, 파싱하여 DOM을 생성한다.
DOM이란 Document Object Model 문서 객체 모델로, HTML로 작성된 여러 요소들에 자바스크립트가 접근할 수 있도록 브라우저가 변환시킨 객체이다.

쉽게 말해 브라우저가 우리가 작성한 HTML을 자바스크립트가 이해하고 조작할 수 있게 객체로 변환 한 것이다.

웹 브라우저는 이 HTML 문서를 불러 온 다음 이를 아래 사진과 같이 상하 관계를 한 눈에 볼 수 있는 트리 구조로 나타내는데, 이를 DOM Tree 라고 부른다.
DOM API

DOM Tree에서 이 아이템 하나 하나를 노드(Node)라고 부르며, 이 노드들은 전부 하나의 객체로 이루어져 있고, 웹 브라우저는 이 DOM Tree를 통해 웹 요소들을 웹 페이지에 나타낼 수 있다.

이러한 DOM은 자바스크립트가 자신에게 접근해 DOM을 조작하고 수정할 수 있는 방법을 제공한다.

이 방법을 DOM API 라고 부르며, 자바스크립트는 DOM이 제공하는 DOM API를 통해 웹 요소들을 수정하고 조작할 수 있다.

DOM API란?

DOM이란 HTML로 작성된 여러 요소들에 자바스크립트가 접근할 수 있도록 브라우저가 변환시킨 객체이고, 자바스크립트는 이러한 DOM을 통해 HTML로 짜여진 요소들을 생성하고 수정하고 삭제할 수 있다.
또한 DOM은 자바스크립트가 자신에게 접근해, DOM을 조작하고 수정할 수 있는 방법인 DOM API를 제공하기 때문에 자바스크립트는 이를 활용해 웹 요소들을 생성하고 수정하고 삭제할 수 있다.

DOM API를 이용해 HTML 요소들에 접근하기 위해서는 DOM Tree를 참고 해야하는데, 이 DOM Tree를 자세하게 살펴보도록 하자.

아래에 있는 왼쪽의 HTML 코드를 DOM Tree로 나타낸 것을 보면,
이 DOM Tree는 크게 노랑색으로 표시되어있는 document 문서노드, 초록색으로 표시되어있는 요소노드, 분홍색으로 표시되어있는 어트리뷰트 노드, 마지막으로 파랑색으로 표시되어있는 텍스트 노드로 나타낼 수 있다.

DOM API

우리는 DOM API를 이용해 요소에 접근하기 위해, 해당 요소들을 찾을 때 문서, 요소, 어트리뷰트, 텍스트 순서로 각각의 노드에 접근해야하고, 이 문서 노드는 트리의 가장 위쪽에 위치하기 때문에 어떤 요소에 접근하든지 항상 이 문서 노드를 거쳐서 다른 요소들에 접근 해야한다.
자바스크립트가 이 DOM API를 사용해 웹 페이지를 조작하기 위해서는 먼저, 조작하고자하는 요소를 찾아 해당 요소를 선택한 다음, 선택된 요소를 조작해야 한다.

그럼, 제일 먼저 특정 요소를 찾고 선택하는 DOM API에 대해 알아보자.

DOM API로 요소 찾고 선택하기

index.html에 들어가 body 태그 안에 다음과 코드가 있다라고 하자.

<body>
  <div class="today-info">
    <div class="date" id="date">03.10.월요일</div>
    <div class="clock" id="clock">13:15</div>
  </div>
  <script src="src/index.js"></script>
</body>

document.getElementById(id)

특정 요소를 찾을 때에는, 가장 먼저 문서노드 document에 접근해야하기 때문에, document를 작성해줘야한다.
getElementById는 말 그대로 특정 요소를 id값으로 요소를 가져온다 라는 의미이고, 실제로 특정 요소 객체를 반환한다.
getElementById는 만약 동일한 id를 갖고있는 요소가 여러 개 있을 경우, 가장 위에 있는 첫 번째 요소만 반환하는 API 이다.

실제로

console.log(document.getElementById('date'));

를 하게 되면 아래와 같이 해당 id를 갖고 있는 요소 객체를 반환한다.

document.querySelector(cssSelector)

getElementById 처럼 단 하나의 요소를 반환하는 또 다른 API 가 있다. 바로 querySelector 이다.
querySelector는 CSS 선택자를 사용하여 문서 내에서 일치하는 첫 번째 요소를 반환하는 기능을 한다.
querySelector 는 id 뿐만 아니라 class, tag, attribute 등 다양한 선택자를 사용할 수 있다.

selector로는 CSS 선택자(예: "#id", ".class", "tag", "[attribute]" 등) 이 될 수 있다.

id로 요소 선택하기

const title = document.querySelector('#title');

class로 요소 선택하기

const firstItem = document.querySelector('.item');

tag 이름으로 요소 선택하기

const paragraph = document.querySelector('p');

이후에도 getElementsByClassName, getElementsByTagName, getElementsByName 등 다양한 API가 있지만, querySelector를 사용하면 더 간편하게 요소를 선택할 수 있다.
하지만 getElementById, querySelector은 단 하나의 요소만을 반환하기 때문에 여러 개의 요소를 선택하고 싶다면 querySelectorAll 등을 사용해야한다.

DOM API로 요소 조작하기

DOM API를 사용하면 선택한 요소를 조작할 수 있다. 대표적인 속성 및 메서드는 다음과 같다.

.id 와 .className

  • .id: 요소의 ID 값을 변경할 수 있다.
  • .className: 요소의 클래스 값을 변경할 수 있다.
document.querySelector('#title').id = 'newTitle';
document.querySelector('.item').className = 'newClass';

createElement , appendChild

  • 새로운 HTML 요소를 동적으로 생성할 때 createElement를 사용한다.
  • 생성한 요소를 부모 요소에 추가할 때 appendChild를 사용한다.
const button = document.createElement('button');
button.textContent = '클릭하세요';
button.classList.add('btn');
button.addEventListener('click', () => alert('버튼이 클릭됨'));
document.body.appendChild(button);

위의 코드에서 button 요소를 생성 한 후 'btn' 이라는 Class를 추가하고, 버튼을 클릭하면 '버튼이 클릭됨' 이라는 경고창을 띄우는 이벤트를 추가한 후, body 요소에 추가한다.
appendChild 메서드는 부모 요소의 마지막 자식 요소로 추가한다.

textContent

  • 요소 내부의 순수한 텍스트 콘텐츠를 가져오거나 설정할 수 있다.
  • HTML 태그를 해석하지 않고 문자 그대로 출력한다.
const element = document.querySelector('#title');
console.log(element.textContent); // 요소 안의 텍스트 출력
element.textContent = '새로운 제목'; // 텍스트 변경

innerHTML

  • 요소 내부의 HTML을 가져오거나 설정할 수 있다.
  • 문자열로 전달된 HTML을 실제 요소로 해석하여 삽입한다.

innerHTML을 사용하면 HTML 태그가 포함된 문자열을 삽입할 수 있음.

export default function Header(container) {
  container.innerHTML += `
    <h1 class="gnb__title text-title">점심 뭐 먹지</h1>
    <button
      id="gnb-button"
      type="button"
      class="gnb__button"
      aria-label="음식점 추가"
    >
      <img src="./add-button.png" alt="음식점 추가" />
    </button>
  `;
}

🚨innerHTML 사용 시 주의점 - 보안 위험(XSS 공격)

  • innerHTML을 사용하여 사용자 입력을 직접 삽입하면 악성 스크립트가 실행될 위험이 있음.
const userInput = "<script>alert('해킹!');</script>";
document.querySelector('#content').innerHTML = userInput;

❌ 위 코드를 실행하면 경고창이 나타나면서 XSS(크로스 사이트 스크립팅) 공격이 발생할 수 있다.

해결 방법

    1. textContent를 사용하여 HTML 태그를 해석하지 못하도록 방지할 수 있음.
document.querySelector('#content').textContent = userInput;
    1. DOM 요소를 직접 생성하여 추가하는 방법을 활용.
const div = document.createElement('div');
div.textContent = userInput;
document.querySelector('#content').appendChild(div);

addEventListener

  • document 객체의 addEventListener 메서드를 사용하면 특정 요소에 이벤트를 추가할 수 있다.

React에서 DOM API를 직접 사용하지 않고 Virtual DOM을 사용하는 이유

Virtual DOM(VDOM)이란, 실제 DOM을 직접 변경하는 것이 아니라 메모리 내에서 가상의 DOM을 만들어 관리하는 방식이다.
하지만 React에서는 아래와 같은 이유로 DOM API가 아닌 Virtual DOM을 사용한다.

  • DOM 조작 비용 절감
    브라우저의 DOM 조작은 성능 비용이 크다.
    Virtual DOM은 변경 사항을 한 번에 묶어 최소한의 연산으로 적용할 수 있다.

  • 빠른 업데이트 및 최적화
    React는 여러 개의 변경 사항을 Batching(묶음 처리) 하여 한 번의 업데이트로 적용.
    필요한 부분만 업데이트하여 성능 최적화를 할 수 있음.

  • 컴포넌트 기반 개발 방식
    React는 컴포넌트 단위로 UI를 관리하므로, Virtual DOM을 사용하면 각 컴포넌트의 변경 사항만 효율적으로 감지할 수 있음.

비교 항목Virtual DOM 사용 (React)직접 DOM API 사용
성능변경된 부분만 업데이트 → 빠름모든 변경 사항을 즉시 반영 → 성능 부담
코드 유지보수선언적 방식 → 가독성 높음명령형 방식 → 복잡해짐
UI 업데이트상태(State) 기반 자동 업데이트직접 수동으로 업데이트 필요
DOM 변경 방식변경 사항 비교 후 최소 업데이트직접 조작

React가 Virtual DOM을 주로 사용하지만, 특정 상황에서는 직접 DOM을 조작해야 하는 경우도 있다.

useRef를 이용한 직접적인 DOM 접근

(1) useRef를 이용한 직접적인 DOM 접근

import { useRef, useEffect } from 'react';
 
function InputFocus() {
  const inputRef = useRef(null);
 
  useEffect(() => {
    // 컴포넌트가 마운트될 때 input 요소에 자동으로 포커스를 줌
    inputRef.current.focus();
  }, []);
 
  return <input ref={inputRef} placeholder='자동 포커스 입력창' />;
}
 
export default InputFocus;

inputRef.current는 해당 input 요소의 **실제 DOM 객체(HTMLInputElement)**를 가리킨다.
focus() 메서드는 브라우저의 기본 DOM API로, 특정 요소에 포커스를 설정하는 역할을 한다.
즉, React의 Virtual DOM이 아니라 실제 브라우저의 DOM을 직접 조작하는 코드이다.

React에서의 onKeyDown 이벤트 핸들러 사용 (React 방식)

React는 특정 요소에 onKeyDown 이벤트를 직접 부여할 수 있다.
이 방법을 사용하면 window.addEventListener 없이 React의 Virtual DOM에서 키 입력을 직접 처리할 수 있다.

function InputKeyLogger() {
  const handleKeyDown = (event) => {
    if (event.key === 'Escape') {
      console.log('ESC 키가 눌렸습니다!');
    }
  };
 
  return <input type='text' onKeyDown={handleKeyDown} placeholder='ESC 키를 눌러보세요' />;
}
 
export default InputKeyLogger;
  • 특정 <input> 요소에서만 키 입력 감지 가능
  • window.addEventListener 없이 Virtual DOM에서 이벤트 관리
  • React의 이벤트 위임 방식과 일치하여 성능 최적화

(2) 이벤트 리스너를 직접 추가

React의 onClick을 활용하는 것이 일반적이지만, window 객체와 같은 글로벌 이벤트를 사용할 때는 DOM API가 필요하다.

import { useEffect } from 'react';
 
function KeyPressLogger() {
  useEffect(() => {
    const handleKeyPress = (event) => {
      console.log(`입력된 키: ${event.key}`);
    };
 
    window.addEventListener('keydown', handleKeyPress);
 
    return () => {
      window.removeEventListener('keydown', handleKeyPress);
    };
  }, []);
 
  return <div>키보드를 눌러보세요!</div>;
}
 
export default KeyPressLogger;

컴포넌트가 마운트될 때 addEventListener를 추가하고, 언마운트될 때 제거하여 메모리 누수를 방지한다.


정리하기 : DOM API vs Virtual DOM

비교 항목DOM APIVirtual DOM
변경 방식 직접 조작 (document.createElement(), appendChild() 등)변경 사항을 Virtual DOM에서 계산 후 반영
성능 문제 여러 번 조작하면 리플로우 & 리페인트 빈번 발생 변경된 부분만 찾아서 최소한의 업데이트 수행
재사용성 상태(state) 관리가 어려움상태 기반 UI 업데이트 가능 요
코드 유지보수 코드가 길고 복잡해질 가능성 높음 선언형 방식으로 더 직관적인 코드 작성 가능
프레임워크 사용 여부 순수 바닐라 JS로 개발 React, Vue 등의 라이브러리 필요

성능 측면에서의 차이 (리플로우 & 리페인트)

DOM API에서 성능 문제가 발생하는 이유

DOM을 직접 조작하면 브라우저는 다음 두 가지 과정을 수행해야 한다.

  • 리플로우(Reflow): 레이아웃이 변경될 때 전체적인 문서 구조를 다시 계산하는 과정.
  • 리페인트(Repaint): 변경된 요소를 다시 그리는 과정.

DOM을 수정할 때마다 렌더 트리 생성부터 Reflow, Repaint의 과정을 다시 수행해야 한다.
DOM 자체의 수정은 빠르지만, 브라우저가 수행해야 하는 이후의 과정이 느리다.
즉, 성능 저하의 주요 원인은 DOM을 수정할 때 발생하는 Reflow, Repaint 과정에 있다.
Reflow가 빈번하게 발생하는 경우 브라우저에서는 성능 저하가 발생하며, 웹 페이지의 DOM이 복잡하게 구성되어 있고 CSS가 많이 적용된 사이트일 수록 더욱 심해진다.

비효율적인 DOM 조작 예제

const list = document.getElementById('list');
for (let i = 0; i < 100; i++) {
  const newItem = document.createElement('li');
  newItem.textContent = `Item ${i}`;
  list.appendChild(newItem); // 매번 DOM을 업데이트하여 성능 저하 발생
}

위 코드는 100번의 DOM 변경이 발생하여 브라우저가 여러 번 리플로우 & 리페인트를 수행하게 된다.

Virtual DOM에서 성능 최적화 방식

Virtual DOM을 활용하면 브라우저의 직접적인 렌더링을 최소화할 수 있다.

  • 변경 사항을 Virtual DOM에서 먼저 적용.
  • 이전 Virtual DOM과 비교하여 바뀐 부분만 감지(Diffing).
  • 변경된 부분만 실제 DOM에 업데이트.

React의 Virtual DOM 활용 예제

function App() {
  const [items, setItems] = useState([]);
 
  const addItem = () => {
    setItems([...items, `Item ${items.length}`]); // 상태 변화 → 자동 렌더링 최적화
  };
 
  return (
    <div>
      <ul>
        {items.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
      <button onClick={addItem}>Add Item</button>
    </div>
  );
}
  • 상태(state) 변경만으로 자동으로 변경 사항을 감지하여 업데이트.
  • React는 useState를 통해 Virtual DOM을 변경하고, 필요할 때만 실제 DOM을 업데이트.
Posted injavascript
Written byEunwoo