📝

14. DOM

 

14-1. DOM 은 무엇인가?

DOM은 책 한 권이 나올 정도로 들여다 봐야 할 내용이 많다. 여기서는 핵심적인 개념을 이해할 수 있도록 정리해보도록 하겠다.
브라우저는 서버에서 받은 문서인 HTML, CSS를 파싱하고, 그 결과물인 DOM 트리CSSOM 트리를 융합해 사용자 장치에서 볼 수 있도록 변환한다.
좀 더 쉽게 설명하자면, HTML 개별 요소를 레고 부품이라고 할 수 있다. 그리고 설명서에 따라 (렌더링 엔진에 따라) 제작된 완성품(제작된 화면)이 바로 DOM인 것이다.
여기서 주의해야 할 것은 설명서대로 레고를 조립하고 부품을 추가로 가져다 붙일 수 있는 것처럼 DOM도 다 조립했다고 해서 조작할 수 없는 상태인 것이 아니라 추가로 생성, 수정, 삭제할 수 있다.
💡
Virtual DOM이란? 위에서 비유한 것처럼 레고 완성품을 만들고 하나의 부품만 교체하기 위해서는 전체를 교체(리로딩)해야 하는 상황이 발생되었다. 이러한 문제를 해결하고자 SPA(Single Page Application)이 나왔고 원하는 부품만 교환할 수 있게 기능을 제공해준다. 여기서 사용되는 것이 Virtual DOM이다. 레고로 비유를 다시 하자면 실제 조립(DOM)을 하지 않고 가상으로 조립(Virtual DOM)을 해놓고 바뀌는 부품이 있다면 바꿔놓고 조립을 하는 것이다.
DOM 트리는 HTML을, CSSOM 트리는 CSS 를 조작하는 API(Application Programming Interface)이다. 이 챕터에서는 문서객체모델을 의미하는 DOM(Document Object Model)에 집중하자.
💡
Parsing(파싱)이란? 일련의 문자열을 의미있는 token으로 분해하고 이들로 이루어진 parse tree를 만드는 과정을 말한다. 브라우저가 코드를 이해하고 사용할 수 있도록 해주기 위함이다.

14-1-1. DOM 인터페이스 정의

DOM은 HTML, XML 문서의 프로그래밍 인터페이스(API)이다. 즉, DOM은 웹 페이지를 프로그래밍 언어로 조작할 수 있도록 연결시켜주는 역할을 한다. DOM은 Document를 object들로 표현하는데, 그 이유는 웹 페이지를 생성하거나 수정하는데 사용되는 모든 property, method, event 들이 object이기 때문이다.
DOM은 JavaScript에 종속되어 있는 개념이 아니다. API 형태로 제공되기 때문에 Python으로도 조작할 수 있다.

14-1-2. 트리형 자료구조

DOM은 HTML 코드를 해석해서 트리 형태로 정보를 표현하는 문서를 생성한다. 브라우저는 DOM을 통해 화면에 웹 컨텐츠를 렌더링하며, 이를 제어하는 property와 method를 제공한다. 이를 통해 문서 구조, 내용, 스타일 등을 변경할 수 있다. HTML 문서는 HTML 요소들의 집합으로 이루어져 있고 요소들은 서로 중첩 관계를 가질 수 있다. 이러한 중첩 관계로 인해 계층적인 부모-자식 관계가 형성된다. 이처럼 부모 노드와 자식 노드를 이용해 노드 간 계층 구조를 표현하는 비선형 자료구조를 트리 자료구조라 한다.
💡
비선형(Non-linear) 자료구조란? - 하나의 자료 뒤에 여러 개의 자료가 존재할 수 있는 자료구조이다. - tree, graph 해당됨 선형(Linear) 자료구조란? - 하나의 자료 뒤에 하나의 자료만 존재할 수 있는 자료구조이다. - array, stack, queue 해당됨
알잘딱깔센 JavaScript알잘딱깔센 JavaScript
알잘딱깔센 JavaScript
알잘딱깔센 JavaScript알잘딱깔센 JavaScript
알잘딱깔센 JavaScript
실제로 어떠한 구조를 가지는지 code를 통해 살펴보자. HTML 구조는 아래와 같다. 책을 보고 실습할 경우 따로 파일을 만들지 않고 브라우저에서 수정하면서 실습하면 된다.
notion imagenotion image
 
콘솔창에서 아래와 같이 입력해보자. Node를 탐색할 수 있다. 여기서 주의깊게 보아야 할 것은 document.body.childNodes[1]라고 입력했을 때 해당 요소를 가리킨다는 것이다.
notion imagenotion image
 
아래와 같이 접근하여 DOM을 조작할 수 있다. 다시 한 번 상기해보자면 DOM은 레고 완성품에 비유할 수 있다. 우리는 완성된 작품에서 부품을 하나 교체(DOM 수정)한 것이다.
notion imagenotion image
 
2번째 h1hello world에 접근하기 위해 아래와 같이 입력할 수 있다.
notion imagenotion image
 
이를 트리로 그려보자.
알잘딱깔센 JavaScript알잘딱깔센 JavaScript
알잘딱깔센 JavaScript
💡
왜 text 노드는 그리지 않았을까? text 노드가 생긴 이유는 앞 뒤로 공백이 있기 때문이다. HTML코드를 아래와 같이 한 줄로 만들면 text 노드가 사라진다.
notion imagenotion image
notion imagenotion image

14-1-3. DOM을 구성하는 노드 객체와 타입 5가지

DOM을 구성하는 노드 객체는 DOM API를 통해 자신을 제어할 수 있다. DOM 인터페이스를 다루기 전에 먼저 Node를 이해하자. 아래 그림에서 나온 것처럼 다른 인터페이스들은 기본적으로 Node 아래 있어 다양한 타입을 유사하게 처리할 수 있게 한다. Node의 속성으로 자식 요소, 부모 요소를 반환할 수 있고 메소드를 통해 자식 요소로 추가할 수 있다.[1] 모든 노드 객체는 Object, EventTarget, Node 인터페이스를 상속받고 추가적인 특성을 가질 수 있다. DOM 타입들이 상속하는 노드 객체는 총 12개의 타입이 있다. 이 중에서 5가지만 소개하겠다.
알잘딱깔센 JavaScript알잘딱깔센 JavaScript
알잘딱깔센 JavaScript
  1. 문서 노드 (document node)
      • DOM 트리의 최상위에 존재하는 루트 노드로 돔 트리의 진입점이며, 브라우저가 렌더링한 HTML 문서 전체를 가리킨다.
      • Document를 이용해 웹 페이지를 동적으로 만드는 기능을 전역적으로 사용할 수 있다.
      • Document 의 속성과 새로운 요소를 생성하고 반환하는 메소드는 아래 링크를 참고한다.[2]
  1. 요소 노드 (element node)
      • Element 인터페이스는 Document 안의 다양한 객체가 부모로 갖는 기본 클래스로 HTMLElement SVGElement 등 특정 요소를 더 잘 표현할 수 있는 클래스로 나뉜다.
      • HTML 요소를 가리키는 객체로, 요소 노드는 부모-자식 관계를 통해 문서의 구조를 표현한다. 어트리뷰트 노드를 가질 수 있는 유일한 노드이다.
      • Element의 속성에는 classList, className, innerHTML 등이 있으며 메소드에는 대표적으로addEventListener 가 있다.[3]
  1. 어트리뷰트 노드 (attribute node)
      • 요소 노드와 연결되어 HTML 요소의 어트리뷰트를 가리키는 객체이다.
  1. 텍스트 노드 (text node)
      • HTML 요소의 텍스트를 가리키는 객체로 문서의 정보를 나타낸다. 자식 노드가 없는 leaf 노드로 DOM 트리의 최종단 노드이다.
  1. 주석 노드 (comment node)
      • 말 그대로 주석을 나타내주는 노드이다.
이 외에도 처리 명령을 나타내는 6.ProcessingInstruction 노드, 부모가 없는 아주 작은 Document 객체를 나타내는 7.DocumentFragment 노드, 8.DocumentType노드, 9.Notation 노드, 10.Entity 노드, 11.EntityReference 노드, 12.CdataSection 노드가 있다.

14-2. DOM의 역사

notion imagenotion image
 

14-2-1. DOM 배경

초기 웹 브라우저는 HTML과 CSS로 작성된 단순하고 정적인 웹페이지였다. 그러던 중 1994년, 넷스케이프사는 사용자와 브라우저 간 상호작용이 가능한 동적인 웹페이지를 위해 script 언어 개발에 착수하였다.
이 언어는 내부적으로 Mocha라 이름 지어졌고, 추후 LiveScript로 변경되었다. 넷스케이프 브라우저에는 이 언어를 이해하고 실행할 수 있는 LiveScript 엔진(해석기)를 내장하고 출시되었다. 당시 Java 언어의 인기가 치솟고 있었기 때문에 넷스케이프는 이 인기에 편승하고자 LiveScript의 이름을 JavaScript로 다시 한번 변경하게 되었다.
넷스케이프 브라우저의 성공 사례는 마이크로소프트사가 브라우저의 잠재력을 알아보고 시장에 진출하는 계기가 되었다. 1995년, 마이크로소프트사는 넷스케이프 사의 브라우저를 Reverse engineering 해서, 기능 일부를 변경한 JScript 엔진이 포함된 Internet Explorer를 출시하였다. 이후로 촉발된 웹 브라우저들의 점유율 경쟁, 즉 "Browser War"[4]는 각 브라우저 간 호환되지 않는 script 언어 때문에 개발자들에게 고통을 안겨주는 시발점이 되었다. 스크립트의 동작뿐만 아니라 DOM API의 명세 또한 브라우저마다 달라 표준화를 요구하는 목소리가 나오기 시작했다.
이러한 이유로 1997년 ECMAScript 표준이 정해졌고, 1998년 W3C에서 “DOM Level1”로 알려진 initial DOM 표준을 발표했다.
 
💡
Reverse engineering(역공학) binary 코드를 분석해서 소스코드를 복원하는 것을 말한다.
 

14-2-2. DOM Level

Version
Summary
DOM0 (Legacy)
- JavaScript 및 초기 브라우저와 동시에 개발, Netscape 2 이상에서 지원 - 접근할 수 있는 엘리먼트에 제한이 있어, Form, link, image 엘리먼트들은 Root Document Object로 시작하는 계층적 이름을 통해서만 접근이 가능
DOM1
- HTML 및 XML 문서의 기본 구조를 정의
DOM2
- getElementByID, XML Namespace, CSS를 지원
DOM3
- XPath, 키보드 Event handling, serializing document as XML을 추가

14-3. DOM에 어떻게 접근하는가?

DOM과 자바스크립트는 초기에 밀접하게 연결되어 있었지만, 나중에는 각각 분리되어 발전하여 DOM의 구현은 어떠한 언어에서도 가능하게 되었다. 하지만 웹 사이트에서는 JavaScript를 사용하므로 여기서는 자바스크립트를 활용한 DOM 조작에 집중하자.
동적으로 HTML을 조작하려면 먼저 요소 노드를 취득해야 한다. DOM(DOM API)이 제공하는 프로퍼티와 메서드를 사용하여 노드에 접근하고 HTML을 동적으로 변경하는 방법은 다음과 같다.
아래의 HTML 문서의 버튼을 대표적인 4가지 방법으로 취득해보겠다.
 
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>요소 노드 취득하기</title> </head> <body> <button id="btnID" class="btnClass">버튼</button> <button id="btnID2" class="btnClass">버튼</button> </body>

14-3-1 특정 요소 노드 취득

document.prototype.querySelector 메서드는 인수로 전달한 CSS 선택자를 만족하는 노드가 여러 개가 있다면, 첫 번째 요소 노드만 반환한다.
document.prototype.querySelectorAll 메서드는 인수로 전달한 CSS 선택자를 만족하는 노드가 여러 개 있다면, 모든 요소 노드를 탐색해 반환한다.
 
const button = document.querySelector("button"); const button1 = document.querySelectorAll("button"); console.log("button"); console.dir(button); console.log("----------------"); console.log("button1"); console.dir(button1);
notion imagenotion image
notion imagenotion image
 

14-3-2. id 를 이용한 요소 노드 취득

document.getElementById 메서드는 인수로 해당 id값을 갖는 하나의 요소 노드를 탐색해 반환한다. id값은 HTML 문서 내 유일한 값이어야 한다.
 
const button2 = document.getElementById("btnID"); console.log("button2" + button2); console.dir(button2);
notion imagenotion image
 

14-3-3. class 를 이용한 요소 노드 취득

document.prototype.getElementsByClassName 메서드는 인수로 해당 class 값을 갖는 모든 요소 노드들을 탐색해 반환한다.
 
const button3 = document.getElementsByClassName("btnClass");
notion imagenotion image

14-3-4. 태그를 이용한 요소 노드 취득

document.prototype.getElementsByTagName 메서드는 인수로 해당 태그 이름을 갖는 모든 요소 노드들을 탐색해 반환한다.
 
const button4 = document.getElementsByTagName("button"); console.log("button4" + button4); console.dir(button4);
notion imagenotion image
 
💡
NodeList와 HTMLCollection이란? DOM API가 여러 개의 결과 값을 반환하기 위한 컬렉션 객체로, 유사 배열 객체이면서 iterable하다. 배열로 변환한 후 사용하는 것을 권장한다는 공통점이 있다. NodeList는 querySelectorAll의 결과로, forEach 사용이 가능하다. HTMLCollection은 getElementsByTagName과 getElementsClassName의 결과로, forEach 사용이 불가능하다. 아래 단원에서 추가 설명을 살펴보자.

14-4. NodeList & HTMLCollection

14-3의 돔 요소 취득은 메서드에 따라 그 반환값이 다르다. 그 중 NodeListHTMLCollection 을 알아야 반환값을 다룰 수 있다.

14-4-1. NodeList, HTMLCollection 비교

NodeListHTMLCollection은 document의 메서드와 element의 property의 반환값으로 나타나는 컬렉션이며 유사 배열이다.
NodeListHTMLCollection은 유사 배열이지만 차이가 있다. 유사 배열이기 때문에 배열의 모든 메서드를 사용하진 못하지만 Array.from()을 통해 얕게 복사해 새로운 배열 객체를 만들 수 있다.
NodeList
HTMLCollection
forEach()
O
X
Array.from()
O
O

14-4-2. NodeList 정적, 동적인 경우

NodeListdocument.querySelectorAll() 메서드와 element.childNodes 에 의해 반환되는 경우에 따라 정적과 동적으로 나뉜다. document.querySelectorAll() 을 통해 반환된 NodeList 는 DOM의 내용이 변경되어도 반영되지 않고 element.childNodes 를 통해 반환된 경우 DOM의 내용이 변경될 때 실시간으로 반영된다.
 
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="button"> <div>1</div> </button> <script> const button = document.getElementById('button'); const buttonChild = button.childNodes; console.log(buttonChild.length); // 3 button.appendChild(document.createElement('div')); console.log(buttonChild.length); // 4 console.log(buttonChild); </script> </body> </html>
notion imagenotion image
notion imagenotion image
childNodes 사용시 반환된 buttonChildNodeList타입을 가지며 자식 요소를 추가했을 때 DOM의 변화가 바로 반영된 것을 알 수 있다.

14-4. DOM 예제

 
<!DOCTYPE html> <html lang="ko"> <head> <title>DOM test</title> <style> #d1 { margin-left: 10px; background-color: rgb(173, 216, 230); height: 20px; width: 20px; } </style> </head> <body> <div id="d1">&nbsp;</div> <form action=""> <p> <button type="button" class="styleBtn">getStyle</button> bg-color<input id="t1" type="text" /> </p> </form> <div> <button type="button" class="addBtn">노드 추가</button> <ul> <li>list1</li> <li>list1</li> <li>list1</li> </ul> </div> </body> </html>
const btn1 = document.querySelector(".styleBtn"); btn1.addEventListener("click", () => { const div = document.getElementById("d1"); const bgColorBtn = document.getElementById("t1"); div.style.backgroundColor = bgColorBtn.value; }) const btn2 = document.querySelector(".addBtn"); btn2.addEventListener("click", () => { const list = document.querySelectorAll("li"); list.forEach(i => { var li = document.createElement("li"); li.textContent = "list2"; i.appendChild(li); }); })
실행 전 모습실행 전 모습
실행 전 모습
black 으로 배경색 입력, getStyle 버튼 클릭 후black 으로 배경색 입력, getStyle 버튼 클릭 후
black 으로 배경색 입력, getStyle 버튼 클릭 후
노드 추가 버튼 클릭 후노드 추가 버튼 클릭 후
노드 추가 버튼 클릭 후
위 예제에서는 두 가지 동작이 발생한다. btn1(getStyle 버튼)을 클릭 시 divbackgroundColor를 입력한 값으로 변경 할 수 있다. btn2(노드 추가 버튼)를 클릭 시 기존에 있던 각 li 요소 아래에 새로운 li 요소를 추가한다.
 
 

References


  1. https://developer.mozilla.org/ko/docs/Web/API/Node
  1. https://developer.mozilla.org/ko/docs/Web/API/Document
  1. https://developer.mozilla.org/ko/docs/Web/API/Element
  1. https://acodez.in/browser-wars/