[PoiemaWeb : Javascript] 29. 문서 객체 모델(Document Object Model)

[PoiemaWeb : Javascript] 29. 문서 객체 모델(Document Object Model)

1. DOM (Document Object Model) : https://relaxed-it-study.tistory.com/147

DOM은 브라우저의 렌더링 엔진이 웹 문서를 로드한 후, 파싱해 웹 문서를 브라우저가 이해할 수 있는 구조로 구성하여 메모리에 적재하는 것을 말한다.

-> 모든 요소와 요소의 Attribute, 텍스트를 각각의 객체로 만들고 이들 객체를 부자 관계로 표현할 수 있도록 트리 구조로 구성한 것

브라우저는 웹 문서(HTML, XML, SVG)를 로드한 후, 파싱하여 DOM(문서 객체 모델: Document Object Model)을 생성한다.

웹 문서의 동적 변경을 위해 DOM은 프로그래밍 언어가 자신에 접근하고 수정할 수 있는 방법으로 Property와 메소드를 갖는 자바스크립트 객체로 제공된다. -> DOM API(Application Programming Interface)

-> 정적인 웹페이지에 접근하여 동적으로 웹페이지를 변경하기 위한 유일한 방법은 메모리 상에 존재하는 DOM을 변경하는 것이고 이를 위해 필요한 것이 DOM API이다.

DOM은 HTML, ECMAScript에서 정의한 표준이 아닌 별개의 W3C의 공식 표준이며 플랫폼/프로그래밍 언어 중립적이다.

> DOM의 기능

HTML 문서에 대한 모델 구성 : 브라우저는 HTML 문서를 로드한 후 해당 문서에 대한 모델을 메모리에 생성한다.

이때 모델은 객체의 트리로 구성되는데 이것을 DOM tree 라 한다.

HTML 문서 내의 각 요소에 접근 / 수정 : DOM은 모델 내의 각 객체에 접근하고 수정할 수 있는 Property와 메소드를 제공한다.

DOM이 수정되면 브라우저를 통해 사용자가 보게 될 내용 또한 변경된다.

2. DOM tree

DOM tree는 브라우저가 HTML 문서를 로드한 후 파싱하여 생성하는 모델을 말한다.

객체의 트리로 구조화되어 있기 때문에 DOM tree라 부른다.

.red { color: #ff0000; } .blue { color: #0000ff; } Cities Seoul London Newyork Tokyo

DOM tree

DOM에서 모든 요소, 어트리뷰트, 텍스트는 하나의 객체이며 Document 객체의 자식이다.

요소의 중첩관계는 객체의 트리로 구조화하여 부자관계를 표현한다.

DOM tree의 진입점(Entry point)는 document 객체이며 최종점은 요소의 텍스트를 나타내는 객체이다.

> DOM tree의 노드 종류

문서 노드(Document Node) : 트리 최상위에 존재하며, 각각 요소, Attribute, 텍스트 노드에 접근하려면 문서 노드를 통해야한다. -> DOM tree에 접근하기 위한 시작점(entry point)

요소 노드(Element Node) : 요소 노드는 HTML 요소를 표현한다. HTML 요소를 중첩에 의해 부자 관계를 가지며 이 부자 관계를 통해 정보를 구조화한다. -> 요소 노드는 문서의 구조를 서술한다.

Attribute, 텍스트 노드에 접근하려면 먼저 요소 노드를 찾아 접근해야한다.

모든 요소 노드는 요소별 특성을 표현하기 위해 HTMLElement 객체를 상속한 객체로 구성된다.

Attribute 노드(Attribute Node) : Attribute 노드는 HTML 요소의 Attribute를 표현한다.

Attribute 노드는 해당 Attribute가 지정된 요소의 자식이 아니라 해당 요소의 일부로 표현한다.

-> 해당 요소 노드를 찾아 접근하면 Attribute를 참조, 수정할 수 있다.

텍스트 노드(Text Node) : 텍스트 노드는 HTML 요소의 텍스트를 표현한다.

텍스트 노드는 요소 노드의 자식이며 자신의 자식 노드를 가질 수 없다. -> DOM tree의 최종단

DOM tree의 객체 구성

> DOM tree를 크롬에서 확인하기

개발자도구(Developer Tools)의 Elements 를 선택한 후 오른쪽의 properties을 선택한다.

크롬 개발자도구(Developer Tools)에서 확인한 DOM 트리

> DOM을 통해 웹페이지 조작(mainpulate)하기

조작하고자 하는 요소를 선택 또는 탐색한다. 선택된 요소의 콘텐츠 또는 Attribute를 조작한다.

-> 자바스크립트는 웹페이지 조작에 필요한 수단인 API를 제공한다.

3. DOM Query / Traversing (요소에의 접근) : https://relaxed-it-study.tistory.com/148

3-1. 하나의 요소 노드 선택(DOM Query)

> document.getElementById(id)

id Attribute 값으로 요소 노드 한 개를 선택한다. 복수개가 선택된 경우, 첫번째 요소만 반환한다.

Return : HTMLElement를 상속받은 객체

모든 브라우저에서 동작한다.

// id로 하나의 요소를 선택한다. const elem = document.getElementById('one'); // 클래스 어트리뷰트의 값을 변경한다. elem.className = 'blue'; // 그림: DOM tree의 객체 구성 참고 console.log(elem); // Seoul console.log(elem.__proto__); // HTMLLIElement console.log(elem.__proto__.__proto__); // HTMLElement console.log(elem.__proto__.__proto__.__proto__); // Element console.log(elem.__proto__.__proto__.__proto__.__proto__); // Node

> document.querySelector(cssSelector)

CSS 셀렉터를 사용하여 요소 노드를 한 개를 선택한다. 복수개가 선택된 경우, 첫번째 요소만 반환한다.

Return: HTMLElement를 상속받은 객체

IE8 이상의 브라우저에서 동작한다.

// CSS 셀렉터를 이용해 요소를 선택한다 const elem = document.querySelector('li.red'); // 클래스 어트리뷰트의 값을 변경한다. elem.className = 'blue';

3-2. 여러 개의 요소 노드 선택(DOM Query)

> document.getElementByClassName(class)

class Attribute 값으로 요소 노드를 모두 선택한다. 공백으로 구분하여 여러 개의 class를 지정할 수 있다.

Return: HTMLCollection (live)

IE9 이상의 브라우저에서 동작한다.

> Node의 상태를 변경하기 위해 loop를 사용할 경우 주의점

// HTMLCollection을 반환한다. HTMLCollection은 live하다. const elems = document.getElementsByClassName('red'); for (let i = 0; i < elems.length; i++) { // 클래스 어트리뷰트의 값을 변경한다. elems[i].className = 'blue'; }

위의 예제를 실행하면 두번째 요소만 클래스 변경이 되지 않는다.

getElementByClassName()의 반환값인 HTMLCollection은 반환값이 복수인 경우, HTMLElement의 리스트를 담아 반환하기위한 객체이다.

배열과 비슷한 사용법을 가지고 있지만 배열은 아닌 유사배열이다.

HTMLCollection은 실시간으로 Node의 상태 변경을 반영한다.

> 위의 예제가 동작하는 순서

i가 0일때, elems의 첫 요소(li#one.red)의 class Attribute의 값이 className Property에 의해 red에서 blue로 변경된다. (elems는 실시간으로 Node의 상태 변경을 반영하는 HTMLCollection 객체)

elems의 첫 요소는 li#one.red에서 li#one.blue로 변경되었으므로 getElementsByClassName 메소드의 인자로 지정한 선택 조건(‘red’)과 더이상 부합하지 않게 되어 반환값에서 실시간으로 제거된다.

i가 1일때, elems에서 첫째 요소는 제거되었으므로 elems[1]은 3번째 요소(li#three.red)가 된다.

li#three.red의 class Attribute 값이 blue로 변경되고 마찬가지로 HTMLCollection에서 제외된다.

i가 2일때, HTMLCollection의 1,3번째 요소가 실시간으로 제거되었으므로 2번째 요소(li#two.red)만 남았다.

elems.length는 1이므로 for 문의 조건식 i < elems.length가 false로 평가되어 반복을 종료한다.

-> elems에 남아 있는 2번째 li 요소(li#two.red)의 class 값은 변경되지 않는다.

-> HTMLCollection는 실시간으로 Node의 상태 변경을 반영하기 때문에 loop가 필요한 경우 주의가 필요하다.

> 위의 문제를 회피하는 방법

반복문을 역방향으로 돌린다.

const elems = document.getElementsByClassName('red'); for (let i = elems.length - 1; i >= 0; i--) { elems[i].className = 'blue'; }

while문을 사용한다.

const elems = document.getElementsByClassName('red'); // 무한 반복을 위해 index를 0으로 고정 let i = 0; while (elems.length > i) { // elems에 요소가 남아 있지 않을 때까지 무한반복 elems[i].className = 'blue'; // i++; }

HTMLCollection을 배열로 변경한다. (권장)

const elems = document.getElementsByClassName('red'); // 유사 배열 객체인 HTMLCollection을 배열로 변환한다. // 배열로 변환된 HTMLCollection은 더 이상 live하지 않다. console.log([...elems]); // [li#one.red, li#two.red, li#three.red] [...elems].forEach(elem => elem.className = 'blue');

querySelectorAll 메소드를 사용한다.

// querySelectorAll는 Nodelist(non-live)를 반환한다. IE8+ const elems = document.querySelectorAll('.red'); [...elems].forEach(elem => elem.className = 'blue');

> document.getElementsByTagName(tagName)

태그명으로 요소 노드를 모두 선택한다.

Return: HTMLCollection (live)

모든 브라우저에서 동작한다.

// HTMLCollection을 반환한다. const elems = document.getElementsByTagName('li'); [...elems].forEach(elem => elem.className = 'blue');

> document.querySelectorAll(selector)

지정된 CSS 선택자를 사용하여 요소 노드를 모두 선택한다.

Return: NodeList (non-live)

IE8 이상의 브라우저에서 동작

// Nodelist를 반환한다. const elems = document.querySelectorAll('li.red'); [...elems].forEach(elem => elem.className = 'blue');

3-3. DOM Traversing (탐색)

DOM Traversing: nextSibling

> parentNode

부모 노드를 탐색한다.

Return: HTMLElement를 상속받은 객체

모든 브라우저에서 동작한다.

const elem = document.querySelector('#two'); elem.parentNode.className = 'blue';

> firstChild, lastChild

자식 노드를 탐색한다.

Return: HTMLElement를 상속받은 객체

IE9 이상의 브라우저에서 동작한다.

Seoul London Newyork Tokyo const elem = document.querySelector('ul'); // first Child elem.firstChild.className = 'blue'; // last Child elem.lastChild.className = 'blue';

> 결과

첫번째 자식과 마지막 자식의 클래스는 변경되지 않았다.

-> IE를 제외한 대부분의 브라우저들은 요소 사이의 공백 또는 줄바꿈 문자를 텍스트 노드로 취급하기 때문이다.

위의 문제를 회피하기 위해서 HTML 공백을 제거하거나 jQuery: .prev()와 jQuery: .next(), firstElementChild, lastElementChild를 사용한다.

HTML 공백 제거

SeoulLondonNewyorkTokyo const elem = document.querySelector('ul'); // first Child elem.firstChild.className = 'blue'; // last Child elem.lastChild.className = 'blue';

firstElementChild, lastElementChild 사용

Seoul London Newyork Tokyo const elem = document.querySelector('ul'); // first Child elem.firstElementChild.className = 'blue'; // last Child elem.lastElementChild.className = 'blue';

> hasChildNodes()

자식 노드가 있는지 확인하고 Boolean 값을 반환한다.

Return: Boolean 값

모든 브라우저에서 동작한다.

> childNodes

자식 노드의 컬렉션을 반환한다. 텍스트 요소를 포함한 모든 자식 요소를 반환한다.

Return: NodeList (non-live)

모든 브라우저에서 동작한다.

> children

자식 노드의 컬렉션을 반환한다. 자식 요소 중에서 Element type 요소만을 반환한다.

Return: HTMLCollection (live)

IE9 이상의 브라우저에서 동작한다.

const elem = document.querySelector('ul'); if (elem.hasChildNodes()) { console.log(elem.childNodes); // 텍스트 요소를 포함한 모든 자식 요소를 반환한다. // NodeList(9) [text, li#one.red, text, li#two.red, text, li#three.red, text, li#four, text] console.log(elem.children); // 자식 요소 중에서 Element type 요소만을 반환한다. // HTMLCollection(4) [li#one.red, li#two.red, li#three.red, li#four, one: li#one.red, two: li#two.red, three: li#three.red, four: li#four] [...elem.children].forEach(el => console.log(el.nodeType)); // 1 (=> Element node) }

> previousSibling, nextSibling

형제 노드를 탐색한다. text node를 포함한 모든 형제 노드를 탐색한다.

Return: HTMLElement를 상속받은 객체

모든 브라우저에서 동작한다.

> previousElementSibling, nextElementSibling

형제 노드를 탐색한다. 형제 노드 중에서 Element type 요소만을 탐색한다.

Return: HTMLElement를 상속받은 객체

IE9 이상의 브라우저에서 동작한다.

const elem = document.querySelector('ul'); elem.firstElementChild.nextElementSibling.className = 'blue'; elem.firstElementChild.nextElementSibling.previousElementSibling.className = 'blue';

4. DOM Manipulation (조작) : https://relaxed-it-study.tistory.com/149

4-1. 텍스트 노드에의 접근/수정

요소의 텍스트는 텍스트 노드에 저장되어있다.

> 텍스트 노드에 접근하는 순서

해당 텍스트 노드의 부모 노드를 선택한다. 텍스트 노드는 요소 노드의 자식이다. firstChild Property를 사용하여 텍스트 노드를 탐색한다. 텍스트 노드의 유일한 Property(nodeValue)를 이용하여 텍스트를 취득한다. nodeValue를 이용하여 텍스트를 수정한다.

> nodeValue

노드의 값을 반환한다.

Return: 텍스트 노드의 경우는 문자열, 요소 노드의 경우 null 반환

IE6 이상의 브라우저에서 동작한다.

nodeName, nodeType을 통해 노드의 정보를 취득할 수 있다.

// 해당 텍스트 노드의 부모 요소 노드를 선택한다. const one = document.getElementById('one'); console.dir(one); // HTMLLIElement: li#one.red // nodeName, nodeType을 통해 노드의 정보를 취득할 수 있다. console.log(one.nodeName); // LI console.log(one.nodeType); // 1: Element node // firstChild Property를 사용하여 텍스트 노드를 탐색한다. const textNode = one.firstChild; // nodeName, nodeType을 통해 노드의 정보를 취득할 수 있다. console.log(textNode.nodeName); // #text console.log(textNode.nodeType); // 3: Text node // nodeValue Property를 사용하여 노드의 값을 취득한다. console.log(textNode.nodeValue); // Seoul // nodeValue Property를 이용하여 텍스트를 수정한다. textNode.nodeValue = 'Pusan';

4-2. Attribute 노드에의 접근/수정

> className

class Attribute의 값을 취득 또는 변경한다.

className Property에 값을 할당하는 경우, class Attribute가 존재하지 않으면 class 어트리뷰트Attribute를 생성하고 지정된 값을 설정한다.

class Attribute의 값이 여러 개일 경우, 공백으로 구분된 문자열이 반환되므로 String 메소드 split(' ')를 사용하여 배열로 변경하여 사용한다.

모든 브라우저에서 동작한다.

> classList

add, remove, item, toggle, contains, replace 메소드를 제공한다.

IE10 이상의 브라우저에서 동작한다.

const elems = document.querySelectorAll('li'); // className [...elems].forEach(elem => { // class Attribute 값을 취득하여 확인 if (elem.className === 'red') { // class Attribute 값을 변경한다. elem.className = 'blue'; } }); // classList [...elems].forEach(elem => { // class Attribute 값 확인 if (elem.classList.contains('blue')) { // class Attribute 값 변경한다. elem.classList.replace('blue', 'red'); } });

> id

id Attribute의 값을 취득 또는 변경한다.

id Property에 값을 할당하는 경우, id Attribute가 존재하지 않으면 id Attribute를 생성하고 지정된 값을 설정한다.

모든 브라우저에서 동작한다.

// h1 태그 요소 중 첫번째 요소를 취득 const heading = document.querySelector('h1'); console.dir(heading); // HTMLHeadingElement: h1 console.log(heading.firstChild.nodeValue); // Cities // id Attribute 값을 변경. // id Attribute가 존재하지 않으면 id Attribute를 생성하고 지정된 값을 설정 heading.id = 'heading'; console.log(heading.id); // heading

> hasAttribute(attribute)

지정한 Attribute를 가지고 있는지 검사한다.

Return : Boolean

IE8 이상의 브라우저에서 동작한다.

> getAttribute(attribute)

Attribute의 값을 취득한다.

Return : 문자열

모든 브라우저에서 동작한다.

> setAttribute(attribute, value)

Attribute와 Attribute 값을 설정한다.

Return : undefined

모든 브라우저에서 동작한다.

> removeAttribute(attribute)

지정한 Attribute를 제거한다.

Return : undefined

모든 브라우저에서 동작한다.

const input = document.querySelector('input[type=text]'); console.log(input); // value Attribute가 존재하지 않으면 if (!input.hasAttribute('value')) { // value Attribute를 추가하고 값으로 'hello!'를 설정 input.setAttribute('value', 'hello!'); } // value Attribute 값을 취득 console.log(input.getAttribute('value')); // hello! // value Attribute를 제거 input.removeAttribute('value'); // value Attribute의 존재를 확인 console.log(input.hasAttribute('value')); // false

> 결과

> 예제

show const $password = document.querySelector('.password'); const $show = document.querySelector('.show'); function makeClickHandler() { let isShow = false; return function () { $password.setAttribute('type', isShow ? 'password' : 'text'); isShow = !isShow; $show.innerHTML = isShow ? 'hide' : 'show'; }; } $show.onclick = makeClickHandler();

show 를 클릭하면 가려진 숫자가 보여지게 hide 를 클릭하면 숫자가 다시 가려지게

4-3. HTML 콘텐츠 조작(Manipulation)

※ 주의

마크업이 포함된 콘텐츠를 추가하는 행위는 크로스 스크립팅 공격(XSS: Cross-Site Scripting Attacks)에 취약하므로 주의가 필요하다.

크로스 스크립팅 공격(XSS: Cross-Site Scripting Attacks) : 웹 해킹 공격 기법 중 하나로 게시판이나 웹 메일 등에 자바스크립티와 같은 스크립트 코드를 삽입해 개발자가 고려 하지 않은 기능이 작동하게 하는 공격.

> textContent

요소의 텍스트 콘텐츠를 취득 또는 변경한다. (마크업은 무시됨)

textContent를 통해 요소에 새로운 텍스트를 할당하면 텍스트를 변경할 수 있다.

순수한 텍스트만 지정해야 하며 마크업을 포함시키면 문자열로 인식되어 그대로 출력된다.

IE9 이상의 브라우저에서 동작한다.

.red { color: #ff0000; } .blue { color: #0000ff; } Cities Seoul London Newyork Tokyo const ul = document.querySelector('ul'); // 요소의 텍스트 취득 console.log(ul.textContent); /* Seoul London Newyork Tokyo */ const one = document.getElementById('one'); // 요소의 텍스트 취득 console.log(one.textContent); // Seoul // 요소의 텍스트 변경 one.textContent += ', Korea'; console.log(one.textContent); // Seoul, Korea // 요소의 마크업이 포함된 콘텐츠 변경. one.textContent = '

Heading

'; // 마크업이 문자열로 표시된다. console.log(one.textContent); //

Heading

> 결과

> innerText

innerText 프로퍼티를 사용하여도 요소의 텍스트 콘텐츠에만 접근할 수 있다.

사용하는 것이 좋지 않은 이유 비표준이다. CSS에 순종적이다. 예를 들어 CSS에 의해 비표시(visibility: hidden;)로 지정되어 있다면 텍스트가 반환되지 않는다. CSS를 고려해야 하므로 textContent Property보다 느리다.

> innerHTML

해당 요소의 모든 자식 요소를 포함하는 모든 콘텐츠를 하나의 문자열로 취득할 수 있다. 이 문자열은 마크업을 포함한다.

const ul = document.querySelector('ul'); // innerHTML 프로퍼티는 모든 자식 요소를 포함하는 모든 콘텐츠를 하나의 문자열로 취득할 수 있다. 이 문자열은 마크업을 포함한다. console.log(ul.innerHTML); // IE를 제외한 대부분의 브라우저들은 요소 사이의 공백 또는 줄바꿈 문자를 텍스트 노드로 취급한다 /* Seoul London Newyork Tokyo */

innerHTML Property를 사용하여 마크업이 포함된 새로운 콘텐츠를 지정하면 새로운 요소를 DOM에 추가할 수 있다.

const one = document.getElementById('one'); // 마크업이 포함된 콘텐츠 취득 console.log(one.innerHTML); // Seoul // 마크업이 포함된 콘텐츠 변경 one.innerHTML += ', Korea'; // 마크업이 포함된 콘텐츠 취득 console.log(one.innerHTML); // Seoul , Korea

> 크로스 스크립팅 공격 사례

// 스크립트 태그를 추가하여 자바스크립트가 실행되도록 한다. // HTML5에서 innerHTML로 삽입된 코드는 실행되지 않는다. // 크롬, 파이어폭스 등의 브라우저나 최신 브라우저 환경에서는 작동하지 않을 수도 있다. elem.innerHTML = '