문서 객체 모델(DOM, Document Object Model)은 웹 문서를 위한 프로그래밍 인터페이스예요. 프로그램이 문서의 구조, 스타일, 내용을 변경할 수 있도록 페이지를 표현해 주는 역할을 하죠. DOM은 문서를 노드(nodes)와 객체(objects)로 표현하는데, 덕분에 프로그래밍 언어들이 웹 페이지와 상호작용할 수 있게 된답니다.
웹 페이지는 브라우저 창에 표시되는 문서이기도 하지만, 동시에 HTML 소스 그 자체이기도 해요. 이 두 가지 상황 모두 같은 문서지만, DOM 표현 방식 덕분에 우리가 마음대로 조작할 수 있는 거죠. 웹 페이지를 객체 지향적으로 표현한 것이기 때문에, JavaScript 같은 스크립트 언어로 수정이 가능해요.
예를 들어, 아래 코드처럼 DOM의 querySelectorAll 메서드를 사용하면 문서 안에 있는 모든 <p> 요소 리스트를 가져올 수 있다고 정의되어 있어요.
const paragraphs = document.querySelectorAll("p");
// paragraphs[0]는 첫 번째 <p> 요소예요.
// paragraphs[1]는 두 번째 <p> 요소고요. 이런 식으로 이어집니다.
alert(paragraphs[0].nodeName);
웹 페이지를 만들고 조작하는 데 사용할 수 있는 모든 속성(properties), 메서드(methods), 이벤트(events)는 객체로 정리되어 있어요. 예를 들어 문서 자체를 나타내는 document 객체나, HTML 테이블에 접근하기 위해 HTMLTableElement 인터페이스를 구현한 table 객체 같은 것들이 모두 객체랍니다.
DOM은 여러 API가 함께 작동하면서 만들어져요. 핵심(Core) DOM은 모든 문서와 그 안의 객체를 설명하는 엔티티를 정의하고요, 여기에 다른 API들이 새로운 기능들을 추가하면서 확장되는 방식이죠. 예를 들어, HTML DOM API는 핵심 DOM에 HTML 문서를 표현하는 기능을 더해주고, SVG API는 SVG 문서를 표현하는 기능을 더해준답니다.
DOM 트리는 HTML이나 XML 문서의 내용을 노드로 표현한 트리 구조를 말해요. 모든 HTML/XML 문서는 DOM 트리로 표현될 수 있죠. 예를 들어 아래와 같은 문서를 한번 볼까요?
<html lang="en">
<head>
<title>My Document</title>
</head>
<body>
<h1>Header</h1>
<p>Paragraph</p>
</body>
</html>
이 문서는 아래와 같은 DOM 트리 모양을 갖게 돼요.

위의 그림이 이 문서의 DOM 트리와 비슷해 보이지만, 실제 DOM 트리는 코드상의 공백(whitespace)까지 보존하기 때문에 완전히 똑같지는 않아요. 브라우저가 HTML 문서를 읽어 들일(parse) 때, 먼저 이 DOM 트리를 만들고 나서 화면에 보여주는 거랍니다.
방금 본 짧은 예제나 거의 모든 예제는 JavaScript로 되어 있어요. 즉, JavaScript로 작성되었지만, 문서와 요소에 접근하기 위해 DOM을 사용하는 거죠. DOM은 프로그래밍 언어가 아니에요. 하지만 DOM이 없다면 JavaScript는 웹 페이지, HTML 문서, SVG 문서나 그 구성 요소들에 대한 모델이나 개념을 전혀 갖지 못했을 거예요. 문서 전체, head, 문서 안의 테이블, 테이블 헤더, 셀 안의 텍스트 등 모든 요소가 그 문서의 DOM의 일부거든요. 이 모든 것들을 DOM과 JavaScript 같은 스크립트 언어를 통해 접근하고 조작할 수 있는 거예요.
DOM은 JavaScript 언어의 일부가 아니라, 웹사이트를 구축하는 데 쓰이는 Web API예요. JavaScript는 다른 환경에서도 쓰일 수 있거든요. 예를 들어 Node.js는 컴퓨터에서 JavaScript 프로그램을 실행하지만 다른 API 세트를 제공하고, DOM API는 Node.js 런타임의 핵심 부분은 아니랍니다.
DOM은 특정 프로그래밍 언어에 종속되지 않도록 설계되었어요. 그래서 문서의 구조적 표현을 일관된 하나의 API로 제공하죠. 대부분의 웹 개발자가 JavaScript를 통해서만 DOM을 사용하겠지만, 아래 Python 예제처럼 다른 언어로도 DOM을 구현할 수 있답니다.
# Python DOM 예제
import xml.dom.minidom as m
doc = m.parse(r"C:\Projects\Py\chap1.xml")
doc.nodeName # document 객체의 DOM 속성
p_list = doc.getElementsByTagName("para")
웹에서 JavaScript를 작성할 때 어떤 기술들이 쓰이는지 더 궁금하다면 JavaScript 기술 개요를 참고해 보세요.
DOM을 사용하기 위해 특별히 준비해야 할 건 없어요. 브라우저가 실행하는 프로그램인 '스크립트' 안에서 바로 API를 사용하면 되거든요.
<script> 요소 안에 직접 작성하든 외부 파일을 불러오든, 스크립트를 만들면 바로 document나](https://developer.mozilla.org/en-US/docs/Web/API/Document)나) window 객체의 API를 사용해 문서 자체나 페이지의 다양한 요소를 조작할 수 있어요. 아주 간단하게는 아래 예제처럼 console.log()를](https://developer.mozilla.org/en-US/docs/Web/API/console/log_static)를) 사용해 콘솔에 메시지를 띄우는 것부터 시작할 수 있죠.
<body onload="console.log('Welcome to my home page!');">
…
</body>
보통 페이지의 구조(HTML)와 DOM 조작(JavaScript)을 섞어 쓰는 건 권장되지 않기 때문에, 여기서는 JavaScript 부분을 따로 모아서 HTML과 분리해 보여드릴게요.
예를 들어, 아래 함수는 새로운 h1 요소를 만들고, 텍스트를 넣은 다음, 문서의 트리에 추가하는 코드예요.
<html lang="en">
<head> </head>
<body>
<script>
// 비어있는 HTML 페이지에 몇 가지 요소를 만듭니다.
const heading = document.createElement("h1");
const headingText = document.createTextNode("Big Head!");
heading.appendChild(headingText);
document.body.appendChild(heading);
</script>
</body>
</html>
아래는 DOM 명세(specification)에 정의된 모든 인터페이스 리스트예요.
AbortControllerAbortSignalAbstractRangeAttrCDATASectionCharacterDataCommentCustomEventDocumentDocumentFragmentDocumentTypeDOMError (더 이상 사용되지 않음. 새 웹사이트에는 쓰지 마세요.)DOMExceptionDOMImplementationDOMParserDOMTokenListElementEventEventTargetHTMLCollectionMutationObserverMutationRecordNamedNodeMapNodeNodeIteratorNodeListProcessingInstructionQuotaExceededErrorRangeShadowRootStaticRangeTextTreeWalkerXMLDocumentXPathEvaluatorXPathExpressionXPathResultXSLTProcessor이 가이드는 DOM 계층 구조를 조작하는 데 사용할 수 있는 객체와 실제 '요소'들에 대한 내용이에요. 가끔 이게 어떻게 작동하는지 헷갈릴 때가 있을 텐데요. 예를 들어, HTML form 요소를 나타내는 객체는 name 속성은 HTMLFormElement 인터페이스에서 가져오지만, className 속성은 HTMLElement 인터페이스에서 가져와요. 결국 우리가 원하는 속성은 그 form 객체 안에 다 들어있긴 하죠.
하지만 객체와 그 객체가 구현하는 인터페이스 사이의 관계는 꽤 복잡할 수 있어요. 그래서 이 섹션에서는 DOM 명세의 실제 인터페이스들과 그것들이 어떻게 제공되는지 간단히 설명해 보려고 해요.
많은 객체가 여러 개의 서로 다른 인터페이스를 구현해요. 예를 들어 table 객체는 createCaption이나 insertRow 같은 메서드를 가진 특화된 HTMLTableElement 인터페이스를 구현하고 있죠. 그런데 테이블도 결국 HTML 요소잖아요? 그래서 Element 인터페이스도 구현해요. 마지막으로, DOM의 관점에서 HTML 요소는 노드 트리를 구성하는 하나의 노드이기 때문에, Element가 상속받는 가장 기본적인 Node 인터페이스도 구현한답니다.
아래 예제처럼 table 객체의 참조를 가져오면, 여러분도 모르는 사이에 이 세 가지 인터페이스를 모두 섞어서 사용하게 될 거예요.
const table = document.getElementById("table");
const tableAttrs = table.attributes; // Node/Element 인터페이스 사용
for (const attr of tableAttrs) {
// HTMLTableElement 인터페이스: border 속성 조작
if (attr.nodeName.toLowerCase() === "border") {
table.border = "1";
}
}
// HTMLTableElement 인터페이스: summary 속성 조작
table.summary = "note: increased border";
여기서는 다양한 객체와 타입을 쉬운 용어로 설명하려고 노력했지만, API에서 주고받는 몇 가지 데이터 타입은 꼭 알아두는 게 좋아요.
참고: DOM을 사용하는 대부분의 코드가 HTML 문서를 조작하는 것이기 때문에, 엄밀히 말하면 모든 노드가 요소는 아니지만 DOM의 노드들을 흔히 요소(elements)라고 부르곤 해요.
다음 표는 이런 데이터 타입들을 짤막하게 설명해 줍니다.
| 데이터 타입 (Interface) | 설명 |
|---|---|
Document | 어떤 멤버가 document 타입의 객체를 반환할 때(예: 요소의 ownerDocument 속성이 해당 요소가 속한 문서를 반환할 때), 이 객체는 루트 document 객체 자체를 의미해요. |
Node | 문서 안에 있는 모든 객체는 일종의 노드예요. HTML 문서에서 객체는 요소 노드일 수도 있고, 텍스트 노드나 속성 노드일 수도 있답니다. |
Element | element 타입은 node를 기반으로 해요. DOM API의 멤버가 반환하는 요소나 element 타입의 노드를 말하죠. 예를 들어 document.createElement()가](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement)가) node에 대한 참조를 반환한다고 말하는 대신, 그냥 방금 DOM에 생성된 element를 반환한다고 말해요. element 객체는 Element 인터페이스뿐만 아니라 더 기본적인 Node 인터페이스도 구현하고 있어요. HTML 문서에서는 HTMLElement 인터페이스와 특정 요소 전용 인터페이스(예: <table>을 위한 HTMLTableElement)를 통해 더 강화된 기능을 제공받죠. |
Attr | createAttribute() 메서드 등으로 attribute가 반환될 때, 이건 속성을 위한 특별한(비록 작지만) 인터페이스를 노출하는 객체 참조예요. 속성도 요소와 마찬가지로 DOM의 노드지만, 그렇게 직접 노드로 다루는 일은 드물 거예요. |
자주 쓰이는 용어들도 기억해 두세요. 예를 들어 모든 Attr 노드를 attribute라고 부르거나, DOM 노드 배열을 nodeList라고 부르는 게 일반적이에요. 문서 곳곳에서 이런 용어들을 계속 만나게 될 거예요.
document와 window는 DOM 프로그래밍에서 가장 자주 쓰게 될 인터페이스예요. 쉽게 말해 window는 브라우저 같은 것을 나타내고, document는 문서 그 자체의 루트라고 생각하면 돼요. Element는 일반적인 Node 인터페이스를 상속받고, 이 두 인터페이스가 합쳐져서 개별 요소에서 사용할 수 있는 많은 메서드와 속성을 제공하는 거랍니다. 그리고 이전 섹션의 table 예제처럼, 요소가 담고 있는 데이터의 종류에 따라 전용 인터페이스를 가질 수도 있고요.
문서 객체 모델은 아주 많이 단순화되었어요. 이를 위해 이전 DOM 레벨 3 이하의 명세에 있던 다음 인터페이스들이 제거되었고, 이제는 웹 개발자가 사용할 수 없게 되었답니다.
DOMConfiguration, DOMErrorHandler, DOMImplementationList, DOMImplementationRegistry, DOMImplementationSource, DOMLocator, DOMObject, DOMSettableTokenList, DOMUserData, ElementTraversal, Entity, EntityReference, NameList, Notation, TypeInfo, UserDataHandlerHTML을 포함한 문서는 Document 인터페이스를 사용해 설명되는데, HTML 명세는 여기에 HTML 전용 기능들을 추가해서 확장해요. 특히 Element 인터페이스는 HTMLElement와](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement)와) 그 하위 클래스들로 강화되어 각각의 요소를 나타내게 됩니다.
HTML DOM API는 탭이나 창, CSS 스타일과 스타일시트, 브라우저 기록 같은 다양한 브라우저 기능에 접근할 수 있게 해 줘요. 이런 인터페이스들은 HTML DOM API 문서에서 더 자세히 다루고 있어요.
마찬가지로 SVG를 포함한 문서도 Document 인터페이스를 통해 설명되고, SVG 명세에 의해 확장돼요. Element 인터페이스가 SVGElement와](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)와) 그 하위 클래스들로 강화되죠. 더 자세한 내용은 SVG API 문서를 확인해 보세요.
이 예제는 <textarea> 하나와 두 개의 <button>이](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button)이) 들어있는 <div>를](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div)를) 사용해요. 첫 번째 버튼을 누르면 텍스트가 입력되고, 두 번째 버튼을 누르면 텍스트가 지워집니다.
Document.querySelector(): textarea와 버튼에 접근EventTarget.addEventListener(): 버튼 클릭 감지Node.textContent: 텍스트를 넣거나 지우기<div class="container">
<textarea class="story"></textarea>
<button id="set-text" type="button">텍스트 내용 넣기</button>
<button id="clear-text" type="button">텍스트 내용 지우기</button>
</div>
.container {
display: flex;
gap: 0.5rem;
flex-direction: column;
}
button {
width: 200px;
}
const story = document.querySelector(".story");
const setText = document.querySelector("#set-text");
setText.addEventListener("click", () => {
story.textContent = "어둡고 폭풍우 치는 밤이었어요...";
});
const clearText = document.querySelector("#clear-text");
clearText.addEventListener("click", () => {
story.textContent = "";
});
이번 예제는 버튼을 클릭하면 새로운 요소를 만들어 자식으로 추가하거나 제거하는 기능이에요.
Document.createElement: 요소 생성Node.appendChild(): 자식 추가Node.removeChild(): 자식 제거<div class="container">
<div class="parent">부모</div>
<button id="add-child" type="button">자식 추가</button>
<button id="remove-child" type="button">자식 제거</button>
</div>
.container {
display: flex;
gap: 0.5rem;
flex-direction: column;
}
button {
width: 100px;
}
div.parent {
border: 1px solid black;
padding: 5px;
width: 100px;
height: 100px;
}
div.child {
border: 1px solid red;
margin: 10px;
padding: 5px;
width: 80px;
height: 60px;
box-sizing: border-box;
}
const parent = document.querySelector(".parent");
const addChild = document.querySelector("#add-child");
addChild.addEventListener("click", () => {
// "부모"라는 텍스트 노드 외에 자식이 없을 때만 추가합니다.
if (parent.childNodes.length > 1) {
return;
}
const child = document.createElement("div");
child.classList.add("child");
child.textContent = "자식";
parent.appendChild(child);
});
const removeChild = document.querySelector("#remove-child");
removeChild.addEventListener("click", () => {
const child = document.querySelector(".child");
if (child) {
parent.removeChild(child);
}
});
아까 봤던 DOM 트리란 무엇일까요? 섹션의 문서 헤더를 바꾸고 단락을 하나 더 추가하고 싶다면 아래 스크립트가 제격이에요.
<html lang="en">
<head>
<title>My Document</title>
</head>
<body>
<input type="button" value="문서 변경하기" />
<h2>Header</h2>
<p>Paragraph</p>
</body>
</html>
document.querySelector("input").addEventListener("click", () => {
// document.getElementsByTagName("h2")는 문서 내 h2 요소들의 NodeList를 반환해요.
// 첫 번째 요소는 0번입니다.
const header = document.getElementsByTagName("h2").item(0);
// 헤더의 firstChild는 텍스트 노드예요.
header.firstChild.data = "동적인 문서로 변신!";
// 이제 첫 번째 단락에 접근해 볼게요.
const para = document.getElementsByTagName("p").item(0);
para.firstChild.data = "이게 첫 번째 단락입니다.";
// 두 번째 단락을 위한 새로운 텍스트 노드를 만듭니다.
const newText = document.createTextNode("이건 새로 추가된 두 번째 단락이에요.");
// 두 번째 단락이 될 새로운 요소를 만듭니다.
const newElement = document.createElement("p");
// 단락 안에 텍스트를 넣습니다.
newElement.appendChild(newText);
// 이 단락을 문서 끝(body)에 붙입니다.
para.parentNode.appendChild(newElement);
});
위에서 봤던 트리를 완전히 JavaScript만으로도 만들 수 있어요.
const root = document.createElement("html");
root.lang = "en";
const head = document.createElement("head");
const title = document.createElement("title");
title.appendChild(document.createTextNode("My Document"));
head.appendChild(title);
const body = document.createElement("body");
const header = document.createElement("h1");
header.appendChild(document.createTextNode("Header"));
const paragraph = document.createElement("p");
paragraph.appendChild(document.createTextNode("Paragraph"));
body.appendChild(header);
body.appendChild(paragraph);
root.appendChild(head);
root.appendChild(body);
이 예제는 DOM에서 이벤트가 어떻게 발생하고 처리되는지 보여줍니다. 테이블의 윗줄에 클릭 리스너를 달고, stopEvent 함수에서 셀 내용을 바꾼 뒤 event.stopPropagation을](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation)을) 호출해요. 이 덕분에 이벤트가 상위 요소인 t-daddy 테이블까지 전달(bubble)되지 않게 된답니다.
<table id="t-daddy">
<tbody>
<tr id="tbl1">
<td id="c1">하나</td>
</tr>
<tr>
<td id="c2">둘</td>
</tr>
</tbody>
</table>
#t-daddy {
border: 1px solid red;
}
#c1 {
background-color: pink;
}
function stopEvent(event) {
const c2 = document.getElementById("c2");
c2.textContent = "안녕!";
// 이 코드가 t-daddy가 클릭 이벤트를 받는 걸 막아줍니다.
event.stopPropagation();
console.log("이벤트 전파 중단!");
}
const elem = document.getElementById("tbl1");
elem.addEventListener("click", stopEvent);
document.getElementById("t-daddy").addEventListener("click", () => {
console.log("t-daddy가 클릭되었습니다."); // 위에서 막았기 때문에 실행 안 됨!
});
마지막으로, onload 이벤트 객체의 모든 속성과 값을 테이블로 보여주는 예제예요. for...in 루프를 사용해 객체를 순회하는 유용한 기법을 보여주죠.
<h1>DOM <span id="eventType"></span> 이벤트 객체 속성들</h1>
table {
border-collapse: collapse;
}
thead {
font-weight: bold;
}
td {
padding: 2px 10px;
}
.odd {
background-color: #efdfef;
}
.even {
background-color: white;
}
function showEventProperties(e) {
function addCell(row, text) {
const cell = row.insertCell(-1);
cell.appendChild(document.createTextNode(text));
}
const event = e || window.event;
document.getElementById("eventType").textContent = event.type;
const table = document.createElement("table");
const thead = table.createTHead();
let row = thead.insertRow(-1);
const labelList = ["#", "Property", "Value"];
for (let i = 0; i < labelList.length; i++) {
addCell(row, labelList[i]);
}
const tbody = document.createElement("tbody");
table.appendChild(tbody);
for (const p in event) {
row = tbody.insertRow(-1);
row.className = row.rowIndex % 2 ? "odd" : "even";
addCell(row, row.rowIndex);
addCell(row, p);
addCell(row, event[p]);
}
document.body.appendChild(table);
}
showEventProperties(event);