2025-10-07 13:32
- DOM은 웹 문서를 자바스크립트가 이해하고 조작할 수 있도록 객체 모델로 변환한 인터페이스다.
- HTML 문서의 모든 요소와 텍스트는 DOM에서 각각의 노드(Node)로 표현되며, 이들은 나무와 같은 계층 구조를 이룬다.
- 자바스크립트는 DOM API를 통해 동적으로 웹 페이지의 콘텐츠, 구조, 스타일을 변경하며 인터랙티브한 경험을 제공한다.
웹 개발의 핵심 DOM 완벽 정복 핸드북
웹 개발을 처음 접하는 사람이라면 누구나 DOM이라는 용어를 듣게 된다. 자바스크립트로 웹 페이지에 생동감을 불어넣으려면 DOM을 반드시 이해해야 한다. 하지만 많은 입문자가 DOM을 막연하고 어렵게 느낀다. 이 핸드북은 DOM이 왜 탄생했는지부터 그 구조와 사용법, 그리고 성능 최적화를 위한 심화 개념까지, DOM의 모든 것을 체계적으로 정리하여 당신을 DOM 전문가로 이끌어 줄 것이다.
1. 만들어진 이유 혼돈의 시대에서 질서를 찾다
DOM은 왜 필요했을까? 이를 이해하려면 1990년대 중반, 웹의 여명기로 거슬러 올라가야 한다. 당시 웹은 정적인 문서를 보여주는 수준에 머물러 있었다. 하지만 넷스케이프(Netscape)와 마이크로소프트(Microsoft)는 각자의 웹 브라우저에 동적인 기능을 추가하기 위해 경쟁적으로 스크립트 언어를 도입했다. 넷스케이프는 자바스크립트(JavaScript)를, 마이크로소프트는 J스크립트(JScript)를 내세웠다.
문제는 이 스크립트 언어들이 웹 페이지의 HTML 요소를 제어하는 방식이 제각각이었다는 점이다. 같은 기능을 구현하더라도 넷스케이프 내비게이터와 인터넷 익스플로러에서 작동하는 코드가 달랐다. 개발자들은 특정 브라우저에서만 작동하는 코드를 작성하거나, 브라우저를 감지하여 다른 코드를 실행하는 복잡한 작업을 감수해야 했다. 이는 웹 개발의 생산성을 심각하게 저해하는 ‘브라우저 전쟁’ 시대의 단면이었다.
이러한 혼란을 바로잡기 위해 월드 와이드 웹 컨소시엄(W3C)이 나섰다. W3C는 모든 브라우저에서 통일된 방법으로 웹 문서를 제어할 수 있는 표준 인터페이스의 필요성을 절감했고, 그 결과물로 문서 객체 모델(Document Object Model), 즉 DOM을 발표했다.
비유: DOM은 마치 여러 국가의 전기 콘센트 규격을 통일하는 ‘국제 표준 어댑터’와 같다. 이전에는 각 나라(브라우저)마다 다른 모양의 플러그(스크립트 코드)를 사용해야 했지만, 표준 어댑터(DOM)가 생기면서 하나의 전기 제품(웹 애플리케이션)을 전 세계 어디서든 사용할 수 있게 된 것과 같다.
DOM은 특정 프로그래밍 언어에 종속되지 않는 중립적인 인터페이스로 설계되었다. 즉, 자바스크립트뿐만 아니라 파이썬, 자바 등 다른 언어로도 웹 문서를 제어할 수 있는 길을 열어주었다. 하지만 웹 브라우저 환경에서는 자바스크립트가 거의 유일한 선택지이므로, 우리는 보통 ‘자바스크립트를 이용한 DOM 조작’을 이야기하게 된다.
결론적으로 DOM은 웹 문서를 프로그래밍적으로 제어하기 위한 표준화된 방법을 제공하기 위해 만들어졌다. 덕분에 개발자들은 브라우저 호환성 문제에서 벗어나 웹 페이지의 콘텐츠, 구조, 스타일에 동적으로 접근하고 수정하며 풍부한 사용자 경험을 만들어낼 수 있게 되었다.
2. DOM의 구조 세상을 나무로 표현하다
DOM은 웹 문서를 어떻게 바라볼까? DOM은 HTML 문서를 하나의 거대한 트리(Tree) 구조로 해석한다. 이 트리는 여러 개의 **노드(Node)**로 구성되며, 각 노드는 HTML 문서의 한 부분을 나타낸다.
가장 상위에는 document 노드가 존재하며, 이는 전체 문서를 대표하는 최상위 뿌리(root) 노드다. document 노드 아래에는 <html> 요소를 나타내는 노드가 있고, 그 아래에는 <head>와 <body> 노드가 자식으로 연결된다. 이런 식으로 HTML 문서의 모든 태그, 텍스트, 주석 등은 각각의 노드로 변환되어 부모-자식 관계로 엮이며 거대한 트리 구조를 형성한다.
노드의 종류
DOM 트리를 구성하는 노드는 여러 종류가 있지만, 가장 중요하고 자주 접하는 4가지 유형은 다음과 같다.
| 노드 유형 | 설명 | 예시 |
|---|---|---|
| 문서 노드 (Document Node) | DOM 트리의 최상위 노드로, 전체 문서를 나타낸다. document 객체로 접근할 수 있다. | document |
| 요소 노드 (Element Node) | HTML 태그를 나타내는 노드다. <html>, <body>, <div>, <h1> 등이 모두 요소 노드에 해당한다. | <p>, <a>, <img> |
| 텍스트 노드 (Text Node) | HTML 요소 안에 있는 텍_스트_를 나타내는 노드다. 요소 노드의 자식으로 존재한다. | <h1>안녕하세요</h1> 에서 ‘안녕하세요’ 부분 |
| 주석 노드 (Comment Node) | HTML 주석을 나타내는 노드다. | “ |
핵심은 요소 노드와 텍스트 노드의 관계다. 많은 사람이 <p>Hello, World!</p>라는 HTML 코드에서 <p> 요소가 “Hello, World!”라는 텍스트를 직접 포함한다고 생각한다. 하지만 DOM 구조에서는 다르다. <p>에 해당하는 요소 노드가 있고, 그 자식으로 “Hello, World!”에 해당하는 텍스트 노드가 존재하는 것이다. 이러한 구조적 이해는 DOM을 정확하게 조작하는 데 매우 중요하다.
비유: DOM 트리를 거대한 ‘가계도’라고 생각해보자.
document는 모든 가문의 시조이고,<html>은 그 직계 자손이다.<html>은<head>와<body>라는 두 자식을 낳았고,<body>는 다시<h1>,<div>,<p>등 여러 자식을 낳으며 가문을 이어간다. 각 구성원(노드)은 부모, 자식, 형제 관계를 가지며 명확한 계층 구조를 이룬다. 우리는 이 가계도를 보고 특정 구성원을 찾아내거나, 새로운 구성원을 입양시키거나, 기존 구성원의 정보를 바꿀 수 있다. 이것이 바로 DOM 조작의 본질이다.
3. DOM 사용법 자바스크립트로 웹 페이지에 생명 불어넣기
DOM의 진정한 힘은 자바스크립트를 통해 동적으로 조작할 때 발휘된다. 자바스크립트는 브라우저가 제공하는 DOM API(Application Programming Interface) 를 사용하여 DOM 트리의 노드에 접근하고, 수정하고, 추가하고, 삭제할 수 있다.
1) 원하는 요소 선택하기 (Traversing)
DOM을 조작하려면 먼저 원하는 노드(주로 요소 노드)를 찾아내야 한다.
-
document.getElementById('id'): 가장 빠르고 전통적인 방법. HTML 문서에서 유일한id속성을 사용하여 요소를 선택한다.const mainTitle = document.getElementById('main-title'); -
document.getElementsByTagName('tag'): 지정된 태그 이름을 가진 모든 요소를 HTMLCollection 형태로 반환한다. 배열과 비슷하지만 배열의 모든 메서드를 사용할 수는 없다.const allParagraphs = document.getElementsByTagName('p'); -
document.getElementsByClassName('class'): 지정된 클래스 이름을 가진 모든 요소를 HTMLCollection 형태로 반환한다.const highlightTexts = document.getElementsByClassName('highlight'); -
document.querySelector('selector'): CSS 선택자 문법을 사용하여 가장 먼저 일치하는 요소 하나를 반환한다. 매우 강력하고 유연하여 현대 웹 개발에서 가장 널리 쓰인다.const firstButton = document.querySelector('.btn-primary'); -
document.querySelectorAll('selector'): CSS 선택자 문법으로 일치하는 모든 요소를 NodeList 형태로 반환한다. NodeList는forEach메서드를 사용할 수 있어 편리하다.const allLinks = document.querySelectorAll('a'); allLinks.forEach(link => { console.log(link.href); });
2) 요소의 내용과 속성 변경하기 (Manipulation)
원하는 요소를 선택했다면, 이제 그 내용을 바꾸거나 속성을 변경할 수 있다.
element.innerHTML: 요소 안의 HTML 전체를 문자열로 가져오거나 새로 설정한다. 사용하기 편리하지만, 새로운 HTML을 파싱하는 과정이 필요하고 보안에 취약(XSS 공격)할 수 있어 주의해야 한다.const contentDiv = document.getElementById('content'); contentDiv.innerHTML = '<h2>새로운 제목</h2><p>새로운 내용입니다.</p>';element.textContent: 요소 안의 텍스트 내용만 가져오거나 설정한다. HTML 태그는 모두 제거되고 순수한 텍스트만 다룬다.innerHTML보다 빠르고 안전하다.const title = document.querySelector('h1'); title.textContent = '페이지 제목이 변경되었습니다!';element.style: 요소의 인라인 스타일을 변경한다. CSS 속성 이름은 카멜 케이스(camelCase)로 변환하여 사용해야 한다 (예:background-color→backgroundColor).const box = document.getElementById('box'); box.style.width = '200px'; box.style.backgroundColor = 'blue'; box.style.color = 'white';element.setAttribute('name', 'value')/element.getAttribute('name'): 요소의 특정 속성 값을 설정하거나 가져온다.const profileImage = document.querySelector('#profile-img'); profileImage.setAttribute('src', 'new-image.jpg'); console.log(profileImage.getAttribute('alt')); // '프로필 이미지'element.classList: 요소의 클래스를 손쉽게 제어할 수 있는 메서드(add,remove,toggle,contains)를 제공한다.const menu = document.getElementById('menu'); menu.classList.add('active'); // active 클래스 추가 menu.classList.remove('hidden'); // hidden 클래스 제거 menu.classList.toggle('collapsed'); // collapsed 클래스가 있으면 제거, 없으면 추가
3) 새로운 요소 만들고 추가하기 (Creation & Insertion)
DOM 트리에 없던 새로운 노드를 만들어 원하는 위치에 추가할 수도 있다.
document.createElement('tag'): 지정된 태그 이름의 새로운 요소 노드를 만든다. 이 단계에서는 메모리에만 존재하고, 아직 DOM 트리에 연결되지는 않았다.const newParagraph = document.createElement('p'); newParagraph.textContent = '새롭게 추가된 문단입니다.';parentNode.appendChild(childNode): 선택된parentNode의 마지막 자식으로childNode를 추가한다.const container = document.getElementById('container'); container.appendChild(newParagraph);parentNode.insertBefore(newNode, referenceNode):parentNode의 자식인referenceNode앞에newNode를 삽입한다.const firstItem = document.querySelector('li:first-child'); const newItem = document.createElement('li'); newItem.textContent = '새로운 아이템 0번'; const list = document.getElementById('my-list'); list.insertBefore(newItem, firstItem);
4) 요소 삭제하기 (Deletion)
parentNode.removeChild(childNode):parentNode에서 특정childNode를 제거한다.const list = document.getElementById('my-list'); const itemToRemove = document.getElementById('item-2'); list.removeChild(itemToRemove);element.remove(): 더 현대적이고 간편한 방법. 삭제하고 싶은 요소에서 직접 호출한다.const itemToRemove = document.getElementById('item-2'); itemToRemove.remove(); // 부모를 찾을 필요 없이 바로 제거 가능
4. 심화 내용 보이지 않는 세계와 성능 최적화
DOM 조작은 사용자에게 동적인 경험을 제공하는 강력한 도구지만, 무분별하게 사용하면 웹 페이지의 성능을 크게 저하시킬 수 있다. DOM의 이면에 있는 브라우저의 작동 방식을 이해하면 더 효율적인 코드를 작성할 수 있다.
1) 렌더링 엔진과 크리티컬 렌더링 경로
브라우저가 HTML, CSS, JavaScript를 화면에 그리는 과정은 **크리티컬 렌더링 경로(Critical Rendering Path)**를 따른다.
- DOM 트리 구축: 브라우저가 HTML을 파싱하여 DOM 트리를 만든다.
- CSSOM 트리 구축: CSS를 파싱하여 스타일 규칙을 담은 CSSOM(CSS Object Model) 트리를 만든다.
- 렌더 트리(Render Tree) 구축: DOM 트리와 CSSOM 트리를 결합하여 화면에 표시될 요소들로만 구성된 렌더 트리를 만든다 (
display: none;같은 요소는 제외됨). - 레이아웃(Layout): 렌더 트리의 각 노드가 화면의 어느 위치에 어떤 크기로 배치될지 계산한다. 이 과정을 **리플로우(Reflow)**라고도 한다.
- 페인트(Paint): 레이아웃 계산이 끝난 노드들을 화면에 실제로 그린다. 이 과정을 **리페인트(Repaint)**라고도 한다.
2) 리플로우와 리페인트: 성능 저하의 주범
DOM을 조작하여 요소의 크기, 위치, 내부 텍스트 등 기하학적 구조에 영향을 주는 변경을 가하면, 브라우저는 변경된 부분을 포함하여 그 주변, 심지어 페이지 전체의 레이아웃을 다시 계산해야 한다. 이것이 바로 리플로우다. 리플로우가 발생하면 필연적으로 해당 부분을 다시 그리는 리페인트가 뒤따른다.
리플로우는 CPU를 많이 사용하는 무거운 작업이다. 특히 반복문 안에서 여러 번 DOM을 변경하면 매번 리플로우가 발생하여 페이지가 버벅이는 현상을 유발할 수 있다.
리플로우를 유발하는 대표적인 작업:
- 요소 추가 또는 삭제
width,height,margin,padding등 크기 및 위치 관련 스타일 변경- 폰트 변경, 텍스트 내용 변경
- 창 크기 조절 (리사이즈)
성능을 최적화하려면 이 리플로우 발생을 최소화해야 한다.
3) DOM 성능 최적화 기법
-
변경 사항을 한번에 묶어서 처리하기: 여러 번에 걸쳐 DOM을 변경하는 대신, 변경할 내용을 변수에 저장해두었다가 마지막에 한 번만 DOM에 적용한다.
// 나쁜 예: 매번 리플로우 발생 const list = document.getElementById('my-list'); for (let i = 0; i < 10; i++) { const newItem = document.createElement('li'); newItem.textContent = `아이템 ${i}`; list.appendChild(newItem); // 반복할 때마다 DOM에 접근하여 리플로우 유발 } // 좋은 예: DocumentFragment를 사용하여 리플로우 최소화 const list = document.getElementById('my-list'); const fragment = document.createDocumentFragment(); // 메모리에만 존재하는 가상의 노드 for (let i = 0; i < 10; i++) { const newItem = document.createElement('li'); newItem.textContent = `아이템 ${i}`; fragment.appendChild(newItem); // 프래그먼트에 추가 (리플로우 발생 안 함) } list.appendChild(fragment); // 마지막에 한 번만 DOM에 추가 (리플로우 1회 발생) -
스타일 변경은 클래스로 제어하기: 여러 스타일 속성을 개별적으로 변경하면 여러 번의 리페인트가 발생할 수 있다. 대신, 변경할 스타일을 미리 정의된 CSS 클래스로 만들어두고
element.classList.add()를 통해 한 번에 적용하는 것이 효율적이다.
4) 가상돔(Virtual DOM)과 섀도우돔(Shadow DOM)
가상돔 (Virtual DOM) 리액트(React), 뷰(Vue) 같은 현대적인 자바스크립트 프레임워크는 실제 DOM을 직접 조작하는 대신 가상돔이라는 개념을 도입했다. 가상돔은 실제 DOM의 구조를 흉내 낸 자바스크립트 객체다.
- 데이터에 변경이 생기면, 프레임워크는 실제 DOM이 아닌 메모리상의 가상돔을 먼저 업데이트한다.
- 새로운 가상돔과 이전 상태의 가상돔을 비교하여(이 과정을 Diffing이라 함) 변경이 필요한 최소한의 부분만 찾아낸다.
- 찾아낸 변경 사항만 실제 DOM에 딱 한 번 적용(이 과정을 Reconciliation, 재조정이라 함)한다.
이 방식을 통해 불필요한 DOM 접근과 리플로우를 최소화하여 애플리케이션의 성능을 획기적으로 향상시킬 수 있다.
섀도우돔 (Shadow DOM) 섀도우돔은 웹 컴포넌트 기술의 핵심으로, 기존 DOM과 분리된 ‘그림자’ 같은 DOM 트리를 만드는 기능이다. 섀도우돔 내부에 정의된 HTML 구조, 스타일, 스크립트는 외부로부터 완전히 캡슐화된다.
이는 전역 CSS나 스크립트가 컴포넌트 내부에 영향을 주거나, 반대로 컴포넌트 내부의 스타일이 외부에 영향을 주는 것을 막아준다. <video> 태그의 재생 버튼이나 슬라이더 바가 바로 브라우저가 내부적으로 섀도우돔을 사용하여 구현한 예시다. 이를 통해 재사용 가능하고 독립적인 컴포넌트를 만들 수 있다.
결론
DOM은 정적인 HTML 문서를 살아 움직이는 인터랙티브 웹 페이지로 만들어주는 핵심적인 다리다. 그 탄생 배경에는 웹 표준을 향한 열망이 있었고, 그 구조는 세상을 논리적인 트리로 표현한다. 자바스크립트를 이용해 DOM을 조작하는 방법을 익히는 것은 모든 프론트엔드 개발자의 기본 소양이다.
나아가 리플로우와 리페인트의 원리를 이해하고, 가상돔과 같은 현대적인 개념을 파악함으로써 우리는 단순히 기능을 구현하는 것을 넘어 사용자에게 쾌적한 경험을 제공하는 고성능 웹 애플리케이션을 만들 수 있을 것이다. 이 핸드북이 당신의 DOM 정복 여정에 훌륭한 길잡이가 되기를 바란다.