Document Object Model
웹 개발을 한다면 반드시라고 해도 좋을 정도로 만나게 되는 DOM! 오늘은 이 친구에 대해서 알아보자.
DOM(Document Object Model) 은 HTML, XML 문서를 객체로 표현한 계층적 구조를 말하며, 개발자가 문서의 내용과 구조를 동적으로 접근하고 조작할 수 있도록 하는 표준화된 프로그래밍 인터페이스이다. 웹 브라우저는 HTML 문서를 읽고, 이를 기반으로 DOM 트리를 생성하며, DOM은 웹 페이지의 시각적 출력과 사용자 상호작용을 연결하는 중요한 역할을 한다.
DOM은 계층적 트리 구조로 구성되며, 문서의 각 부분은 트리의 노드로 표현된다.
DOM에서 모든 요소(HTML 태그), 텍스트, 속성은 각각의 노드로 표현된다.
HTML 태그를 나타내는 DOM의 기본 단위다. 각 요소는 자식 노드와 속성을 포함할 수 있다.
<div id="example">Hello, DOM!</div>
div
: 요소 노드.id="example"
: 속성 노드."Hello, DOM!"
: 텍스트 노드.요소가 가진 추가적인 정보를 나타냄. JavaScript를 사용하여 속성을 읽거나 수정할 수 있다.
const element = document.getElementById("example");
console.log(element.getAttribute("id")); // "example"
element.setAttribute("class", "highlight");
다음의 HTML 문서를 DOM 트리로 표현하면:
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
</head>
<body>
<h1>Hello World</h1>
<p>This is an example paragraph.</p>
</body>
</html>
Document
├── html
├── head
│ └── title: "Example"
├── body
├── h1: "Hello World"
└── p: "This is an example paragraph."
DOM은 HTML, CSS, JavaScript를 상호작용하도록 연결하는 다리 역할을 한다. DOM은 이러한 세 요소를 연결하여 웹 페이지의 동적이고 상호작용적인 특성을 가능하게 한다.
HTML 문서는 웹 페이지의 기본 구조와 내용을 정의하며, 브라우저는 이를 기반으로 DOM 트리를 생성한다. DOM은 HTML 문서를 조작할 수 있도록 인터페이스를 제공한다.
<p id="demo">Original Text</p>
<script>
const paragraph = document.getElementById("demo");
paragraph.textContent = "Modified Text"; // DOM을 통해 텍스트 변경
</script>
CSS는 DOM의 요소에 스타일을 적용한다. DOM을 사용하여 CSS 클래스나 인라인 스타일을 동적으로 변경할 수 있다.
<div id="box" style="width:100px; height:100px; background-color:red;"></div>
<script>
const box = document.getElementById("box");
box.style.backgroundColor = "blue"; // CSS 스타일 변경
</script>
JavaScript는 DOM을 조작하는 주요 도구다. JavaScript는 DOM을 통해 페이지의 콘텐츠를 동적으로 수정하거나 이벤트를 처리할 수 있다.
<button id="btn">Click Me</button>
<script>
const button = document.getElementById("btn");
button.addEventListener("click", () => {
alert("Button Clicked!");
});
</script>
DOM 트리는 브라우저가 HTML 문서를 읽고 해석한 결과로 생성된다. 이 과정은 웹 페이지가 화면에 렌더링되는 핵심 단계다.
브라우저는 서버에서 HTML 문서를 수신한 뒤 이를 파싱(Parsing) 하여 DOM 트리를 생성한다.
<html>
, <body>
등)으로 분리한다.HTML 문서를 기반으로 DOM 트리를 구축한다.
브라우저는 DOM 트리와 CSSOM(CSS Object Model)을 결합하여 렌더 트리를 생성한 후, 이를 화면에 그린다. 이 과정은 브라우저가 웹 페이지의 구조와 콘텐츠를 이해하고 화면에 렌더링할 준비를 완료하는 중요한 단계다.
예시
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
</head>
<body>
<h1>Hello, DOM!</h1>
</body>
</html>
Document
└── html
├── head
│ └── title: "Example"
└── body
└── h1: "Hello, DOM!"
DOM은 트리 구조로 이루어져 있으며, JavaScript를 사용해 각 노드를 탐색하고 조작할 수 있다.
DOM 노드에 접근하기 위해 브라우저는 다양한 메서드를 제공한다.
const element = document.getElementById("example");
console.log(element.textContent);
const heading = document.querySelector("h1");
const paragraphs = document.querySelectorAll("p");
parentNode
: 부모 노드childNodes
: 자식 노드 목록nextSibling
/ previousSibling
: 형제 노드const parent = element.parentNode;
const children = parent.childNodes;
DOM 노드를 동적으로 수정하여 페이지의 콘텐츠와 구조를 변경할 수 있다. DOM 탐색과 조작은 웹 페이지의 동적 콘텐츠를 구성하고 사용자 상호작용을 처리하는 핵심 도구다.
element.textContent = "Updated Text"; // 텍스트 내용 변경
element.innerHTML = "<strong>Bold Text</strong>"; // HTML 내용 변경
const newElement = document.createElement("p");
newElement.textContent = "New Paragraph";
document.body.appendChild(newElement);
element.remove(); // 요소 삭제
DOM 이벤트는 사용자의 행동(클릭, 입력 등)에 응답하기 위한 메커니즘이다. 또한 DOM 이벤트는 웹 페이지에서 사용자와 상호작용하는 핵심 기술로, 적절히 활용하면 동적인 사용자 경험을 구현할 수 있다.
이벤트(Event)는 사용자의 특정 행동(예: 클릭, 키 입력) 또는 시스템 상태의 변화를 나타낸다. DOM에서는 이러한 이벤트가 발생했을 때 이벤트 핸들러(Event Handler) 를 통해 동작을 정의한다.
const button = document.getElementById("btn");
button.addEventListener("click", () => {
alert("Button clicked!");
});
DOM 이벤트는 특정 요소에서 발생한 후, DOM 트리를 따라 흐른다.
<div id="parent">
<button id="child">Click Me</button>
</div>
<script>
document.getElementById("parent").addEventListener("click", () => {
console.log("Parent clicked!");
});
document.getElementById("child").addEventListener("click", () => {
console.log("Child clicked!");
});
</script>
이벤트 위임은 부모 요소에 이벤트 핸들러를 등록하여, 자식 요소의 이벤트를 처리하는 기법이다.
장점: 많은 자식 요소에 각각 이벤트를 등록할 필요가 없어 성능이 향상된다.
예시
document.getElementById("parent").addEventListener("click", (event) => {
if (event.target.tagName === "BUTTON") {
console.log("Button clicked!");
}
});
DOM 조작의 첫 단계는 문서의 특정 요소를 선택하는 것이다. JavaScript는 다양한 메서드를 제공하여 DOM 요소를 선택할 수 있다. 이 메서드들은 필요에 따라 사용되며, querySelector
는 특히 유연성과 간결성 때문에 선호된다고 한다.
getElementById
는 요소의 고유 id
를 기반으로 선택한다.
장점: 빠르고 간단하며, id
는 문서에서 유일하므로 단일 요소를 정확히 선택할 수 있다.
예시
const element = document.getElementById("example");
console.log(element.textContent); // 해당 요소의 텍스트 출력
CSS 선택자를 사용하여 요소를 선택한다.
querySelector: 첫 번째로 일치하는 요소를 반환
querySelectorAll: 모든 일치하는 요소를 NodeList로 반환
예시
const heading = document.querySelector("h1"); // 첫 번째 <h1> 선택
const items = document.querySelectorAll(".item"); // 클래스가 'item'인 모든 요소 선택
items.forEach((item) => console.log(item.textContent)); // 각 요소의 텍스트 출력
getElementsByClassName: 특정 클래스를 가진 모든 요소를 선택
getElementsByTagName: 특정 태그 이름을 가진 모든 요소를 선택
예시
const paragraphs = document.getElementsByTagName("p"); // 모든 <p> 요소 선택
console.log(paragraphs[0].textContent); // 첫 번째 <p> 요소의 텍스트 출력
DOM 요소를 동적으로 추가, 수정, 삭제하면 사용자 경험을 개선하는 인터랙티브한 웹 페이지를 구현할 수 있다. DOM 요소 추가, 수정, 삭제는 웹 페이지의 내용을 동적으로 변화시켜, 사용자의 상호작용에 즉각적으로 응답할 수 있게 한다.
createElement: 새로운 요소를 생성
appendChild / append: 새 요소를 부모 요소에 추가
예시
const newElement = document.createElement("p"); // <p> 요소 생성
newElement.textContent = "This is a new paragraph."; // 텍스트 추가
document.body.appendChild(newElement); // <body>에 추가
const referenceNode = document.querySelector("h1"); // 참조 노드
document.body.insertBefore(newElement, referenceNode); // <h1> 앞에 추가
textContent
: 텍스트만 수정innerHTML
: HTML 콘텐츠를 수정const element = document.getElementById("example");
element.textContent = "Updated Text"; // 텍스트 수정
element.innerHTML = "<strong>Bold Text</strong>"; // HTML 수정
setAttribute
: 속성을 추가하거나 수정removeAttribute
: 속성을 제거element.setAttribute("class", "highlight"); // 클래스 추가
element.removeAttribute("id"); // ID 제거
remove: 요소를 삭제
removeChild: 부모 요소에서 특정 자식 요소를 제거
예시
const parent = document.getElementById("parent");
const child = document.getElementById("child");
parent.removeChild(child); // 부모 요소에서 자식 요소 제거
CSS 클래스와 스타일 속성을 조작하면 DOM 요소의 외형과 레이아웃을 동적으로 변경할 수 있다. CSS 클래스와 스타일 변경은 동적 UI와 사용자 경험 개선의 중요한 요소다. 특히 classList
는 반복적으로 사용되는 스타일 변경 작업을 효율적으로 처리할 수 있도록 한다.
JavaScript는 classList
API를 사용해 CSS 클래스를 추가, 제거, 토글할 수 있다.
const element = document.getElementById("box");
element.classList.add("highlight"); // 클래스 추가
element.classList.remove("highlight"); // 클래스 제거
element.classList.toggle("active"); // 클래스 추가 또는 제거 (토글)
console.log(element.classList.contains("active")); // 클래스 포함 여부 확인
style
속성을 사용하면 CSS 속성을 직접 설정할 수 있다.
const element = document.getElementById("box");
element.style.backgroundColor = "blue"; // 배경색 변경
element.style.border = "2px solid black"; // 테두리 추가
CSS 변수는 JavaScript를 통해 동적으로 조작할 수 있다.
const root = document.documentElement; // :root 선택
root.style.setProperty("--main-color", "green"); // CSS 변수 수정
DOM 이벤트는 사용자 상호작용(클릭, 입력, 마우스 이동 등)에 반응하기 위해 사용된다. 이벤트 리스너는 특정 요소에 이벤트를 감지하고 처리하기 위한 코드를 정의하며, 이벤트 핸들러는 이벤트가 발생했을 때 실행되는 함수를 의미한다. 특히 이벤트 리스너는 웹 페이지에서 동적 상호작용을 구현하는 기본 도구이며, 다양한 이벤트 유형에 반응할 수 있도록 유연하게 설계된다.
JavaScript에서 addEventListener
메서드를 사용하여 이벤트 리스너를 추가할 수 있다.
element.addEventListener(eventType, handler, options);
eventType
: 감지할 이벤트 유형(예: "click"
, "keydown"
)
handler
: 이벤트가 발생했을 때 실행될 함수
options
: 이벤트 동작을 제어하는 옵션 객체(예: capture: true
)
예시: 버튼 클릭 이벤트 처리
const button = document.getElementById("myButton");
button.addEventListener("click", () => {
alert("Button was clicked!");
});
removeEventListener
를 사용하여 이벤트 리스너를 제거할 수 있다.
function handleClick() {
console.log("Button clicked!");
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick); // 리스너 제거
once: true
옵션을 사용하면 이벤트가 한 번만 실행되도록 설정할 수 있다.
button.addEventListener(
"click",
() => {
console.log("This will only run once.");
},
{ once: true }
);
DOM 이벤트는 특정 요소에서 발생한 뒤 트리 구조를 따라 전파된다. 이 과정을 이해하면 이벤트 처리의 효율성을 높일 수 있다.
이벤트 흐름은 세 가지 단계로 나뉜다:
<div id="parent">
<button id="child">Click Me</button>
</div>
이벤트가 하위 요소에서 발생한 후 상위 요소로 전파되는 과정이다.
document.getElementById("parent").addEventListener("click", () => {
console.log("Parent clicked!");
});
document.getElementById("child").addEventListener("click", () => {
console.log("Child clicked!");
});
이벤트가 최상위 요소에서 시작하여 하위 요소로 전파되는 과정이다. 캡처링은 기본적으로 비활성화되어 있지만, addEventListener
의 capture: true
옵션으로 활성화할 수 있다.
document.getElementById("parent").addEventListener(
"click",
() => {
console.log("Parent captured!");
},
{ capture: true }
);
stopPropagation
을 사용하여 이벤트가 더 이상 상위 노드로 전파되지 않도록 할 수 있다.
document.getElementById("child").addEventListener("click", (event) => {
event.stopPropagation();
console.log("Child clicked, no propagation!");
});
이벤트 위임(Event Delegation) 은 이벤트 핸들러를 부모 요소에 등록하여 자식 요소의 이벤트를 처리하는 기법이다. 동적으로 생성된 요소에도 적용되므로 효율적인 이벤트 처리가 가능하다.
이벤트 버블링을 활용하여, 부모 요소에서 자식 요소의 이벤트를 감지한다.
<ul id="list">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
const list = document.getElementById("list");
list.addEventListener("click", (event) => {
if (event.target.tagName === "LI") {
console.log(`You clicked on ${event.target.textContent}`);
}
});
<li>
요소의 텍스트 출력DOM 조작은 웹 애플리케이션의 성능에 큰 영향을 미친다. 브라우저는 DOM 트리를 기반으로 페이지를 렌더링하며, DOM을 수정하거나 조작하면 브라우저는 렌더링 과정을 다시 실행해야 한다. 특히, 과도한 DOM 조작은 페이지 로드 속도를 느리게 하고 사용자 경험을 저하시킬 수 있다. 배치 업데이트와 효율적인 DOM 조작 기법을 사용하면 성능 저하를 줄이고, 브라우저의 Reflow와 Repaint를 최소화할 수 있을 것이다.
for (let i = 0; i < 100; i++) {
const div = document.createElement("div");
div.textContent = `Item ${i}`;
document.body.appendChild(div);
}
DOM 조작 작업을 그룹화하여, 브라우저가 효율적으로 렌더링하도록 한다.
DOM 조작을 메모리상의 문서 조각(Document Fragment)에서 먼저 수행한 뒤, 최종적으로 DOM에 추가
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement("div");
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
document.body.appendChild(fragment);
Virtual DOM은 실제 DOM 조작의 비용을 줄이기 위해 등장한 개념으로, React, Vue와 같은 최신 프레임워크에서 채택하고 있다. Virtual DOM은 성능과 개발 효율성을 동시에 추구하는 현대 웹 개발의 핵심 기술이다.
Virtual DOM은 브라우저의 실제 DOM 트리를 메모리에 복제하여, DOM 조작을 가상 환경에서 먼저 수행한 뒤, 변경 사항만 실제 DOM에 반영하는 기술이다.
function App() {
const [count, setCount] = React.useState(0);
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increase</button>
</div>
);
}
React는 상태 변경(setCount
)이 발생할 때 Virtual DOM에서 변경된 노드만 비교하고 업데이트하여 성능을 최적화한다.
브라우저 렌더링 과정에서 Reflow와 Repaint는 성능에 중요한 영향을 미친다. 이를 최소화하면 DOM 조작의 효율성을 높일 수 있다. Reflow와 Repaint를 최소화하면 페이지의 렌더링 성능이 크게 향상되며, 사용자 경험을 개선할 수 있다.
width
, height
등)offsetWidth
, getBoundingClientRect
)여러 스타일 변경 작업을 한 번에 처리하여 Reflow 발생 횟수를 줄인다.
const element = document.getElementById("box");
element.style.width = "100px";
element.style.height = "100px";
element.style.backgroundColor = "blue"; // 개별 처리 → 비효율
element.style.cssText = "width: 100px; height: 100px; background-color: blue;";
DOM 조작을 Document Fragment에서 먼저 실행하여, Reflow를 최소화
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement("div");
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
document.body.appendChild(fragment);
인라인 스타일 변경 대신, CSS 클래스를 추가/제거하여 스타일을 변경
element.classList.add("new-style"); // CSS 클래스 적용
이벤트 핸들러(스크롤, 리사이즈 등)가 빈번하게 호출되지 않도록 제한
function throttle(func, limit) {
let inThrottle;
return function () {
if (!inThrottle) {
func.apply(this, arguments);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
DOM은 웹 애플리케이션 개발에서 필수적이지만, 대규모 애플리케이션에서는 몇 가지 한계가 있다.
DOM은 트리 구조로 이루어져 있어, 하나의 노드를 조작할 때도 브라우저가 트리 전체를 탐색하거나 렌더링 과정을 다시 실행해야 한다.
DOM은 상태 관리와 UI 렌더링 사이에 명확한 분리가 없으므로, 복잡한 상태를 가진 애플리케이션에서는 유지보수가 어렵다.
DOM 조작은 브라우저의 단일 스레드(메인 스레드)에서 실행되므로, 대규모 작업이 UI 반응성을 떨어뜨릴 수 있다.
Shadow DOM과 Web Components는 DOM의 복잡성을 줄이고, 구성 요소 기반 개발을 가능하게 하는 대안 기술이다. 그 중에서도 Web Components는 재사용성과 캡슐화를 높이며, 대규모 애플리케이션 개발에 적합하다.
Shadow DOM은 DOM의 일부를 캡슐화하여, 외부 CSS나 JavaScript의 영향을 받지 않도록 설계된 기술이다. 이를 통해 모듈화된 구성 요소를 쉽게 관리할 수 있다.
특징
예시: Shadow DOM 생성
const shadowHost = document.getElementById("host");
const shadowRoot = shadowHost.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<style>
p { color: red; }
</style>
<p>Shadow DOM Content</p>
`;
Web Components는 Shadow DOM을 포함하여 재사용 가능한 커스텀 HTML 요소를 만들기 위한 표준이다.
구성 요소
- Shadow DOM: 스타일과 구조의 캡슐화
Custom Elements 예시
class MyElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<style>
p { color: blue; }
</style>
<p>Hello from Custom Element!</p>
`;
}
}
customElements.define("my-element", MyElement);
React와 Vue 같은 최신 프레임워크는 DOM 관리의 효율성을 개선하기 위해 다양한 기술을 사용한다.
React는 Virtual DOM을 통해 실제 DOM의 조작을 최소화한다. 결과적으로 보면, React와 Vue는 DOM의 단점을 보완하여 현대적인 웹 애플리케이션 개발에 적합한 대안을 제공한다.
Vue는 반응형(Reactive) 데이터 바인딩을 사용하여 DOM 업데이트를 자동화한다.
const app = Vue.createApp({
data() {
return { message: "Hello, Vue!" };
},
});
app.mount("#app");
프레임워크 | DOM 관리 방식 | 장점 |
---|---|---|
React | Virtual DOM | 성능 최적화, 상태 관리 용이 |
Vue | Reactive DOM 바인딩 | 단순한 데이터 기반 DOM 업데이트 |
웹 개발 경험이 그리 많지도 않고, 그나마도 백엔드 파트만 맡았다 보니 오늘 조사는 상당히 새로운 느낌으로 진행하게 되었다. 처음에는 DOM이라는 용어 자체가 익숙하지 않고, 제대로 다뤄 본 적이 없어 뭔지만 알아보자는 느낌으로 들어왔는데, 꼬리를 물며 검색하다 보니 너무 많이 들어왔나 싶기도 하다.
하지만 지금 진행 중인 프로젝트에서도 "알아둬서 나쁠 건 없다"라는 걸 여러 번 느꼈던 만큼, 당장에 쓸 일 없어 보인다고 거르고 싶지는 않다. 예를 들면 url상의 번호를 바꾸며 페이지를 전환하는 대신, AJAX 등을 이용해 동적으로 페이지를 구현하는 사이트를 접했을 때 그걸 알아볼 수 있느냐의 문제랄까...?