문서 개체 모델 DOM은 자바스크립트 Node 객체의 계층화된 트리이다.
HTML 문서를 작성할 때에는 HTML 콘텐츠를 다른 HTML 콘텐츠 내에 캡슐화하게 되는데,
이를 통해 트리로 표현 가능한 계층 구조가 만들어진다.
대게 이러한 계층 구조나 캡슐화 시스템은 HTML 문서 내에서 들여 쓰기 표시를 통해 시각적으로 표시된다.
DOM은 원래 XML 문서를 위한 어플리케이션 프로그래밍 인터페이스였지만, HTML 문서에서도 사용되도록 확장되었다.
HTML 문서를 다룰 때 마주치게 되는 가장 일반적인 노드 유형은 다음과 같다.
DOCUMENT_NODE (window.document)
ELEMENT_NODE (<body>, <a>, <p>)
ATTRIBUTE_NODE (class="list")
TEXT_NODE (줄바꿈과 공백을 포함한 문서 내의 텍스트)
DOCUMENT_FRAGMENT_NODE (document.createDocumentFragment())
DOCUMENT_TYPE_NODE (<!DOCTYPE html>)
자바스크립트 브라우저 환경에서 Node 객체의 속성으로 기록되는 상수 값의 속성과 동일.
Node의 이 속성들은 상수값이며, Node 객체의 특정 유형에 매핑되는 숫자 코드 값을 저장하는 데 사용된다.
console.log(Node.ELEMENT_NODE) // element 노드의 숫자 코드 값인 1 출력.
다음 코드는 모든 노드 유형과 그 값을 출력한다.
for (var key in node){
console.log(key,' = '+Node[key]);
}
통상적인 DOM 트리의 각 노드 개체는 Node로부터 속성과 메서드를 상속받는다.
노드 유형에 따라 Node 개체를 확장한 하위 노드 개체/인터페이스가 추가로 존재한다.
Object < Node < Element < HTMLElement
Object < Node < Attr
Object < Node < CharacterData < Text
Object < Node < Document < HTMLDocument
Object < Node < DocumentFragment
모든 노드 유형이 Node로부터 상속받는다는 것뿐만 아니라 상속 체인이 길어질 수도 있음을 알아두는 것이 중요하다.
예를 들어 모든 HTMLAnchorElement노드는 HTMLElement, Element, Node, Object 개체로부터 속성 및 메서드를 상속받는다.
노드를 다루기 위한 속성 및 메서드
Node 속성
- childNodes
- firstChild
- lastChild
- nextSibling
- nodeName
- nodeType
- nodeValue
- parentNode
- previousSibling
Node 메서드
- appendChild()
- cloneNode()
- compareDocumentPosition()
- contains()
- hasChildNodes()
- insertBefor()
- isEqualNode()
- removeChild()
- replaceChild()
Document 메서드
- document.createElement()
- document.createTextNode()
HTML*Element 속성
- innerHTML
- outerHTML
- textContent
- innerText
- outerText
- firstElementChild
- lastElementChild
- nextElementChild
- previousElementChild
- children
HTML element 메서드
- insertAdjacentHTML()
DOM에 Element 및 Text 노드 생성 및 추가하기
<body>
<div id="A"></div>
<div id="B"></div>
<span id="C"></span>
<div id="D"></div>
<div id="E"></div>
</body>
document.getElementById("A").innerHTML = "<strong>안녕하세요1</strong>";
document.getElementById("B").outerHTML = '<div id="B" class="new">안녕하세요2</div>';
document.getElementById("C").textContent = "안녕하세요3";
document.getElementById("D").innerText = "안녕하세요4";
document.getElementById("E").outerText = "안녕하세요5";
console.log(document.body.innerHTML);
/* 다음이 출력됨
<div id="A"><strong>안녕하세요1</strong></div>
<div id="B" class="new">안녕하세요2</div>
<div id="C">안녕하세요3</div>
<div id="D">안녕하세요4</div>
안녕하세요5
*/
innerHTML 속성은 문자열 내에서 발견된 HTML 요소를 실제 DOM 노드로 변환하는 반면
textContent는 텍스트 노드를 생성하는 데만 사용 가능하다.
insertAdjacentHTML() 메서도를 사용해서 문장 구성하기
<body>
<strong id="elm">안녕하세요</strong>
</body>
var elm = document.getElementById("elm");
elm.insertAdjacentHTML('beforebegin','<span>가나다</span>');
elm.insertAdjacentHTML('afterbegin', '<span>ABC</span>');
elm.insertAdjacentHTML('beforeend', '<span>바보</span>');
elm.insertAdjacentHTML('afterend','<span>abc</span>');
console.log(document.body.innerHTML);
/* 다음이 출력됨
<span>가나다</span><strong id="elm"><span>ABC</span>안녕하세요<span>바보</span></strong><span>abc</span>
*/
insertAdjacentHTML의 beforebegin 및 afterend 옵션은 노드가 DOM트리 내에 존재하고 부모 요소를 가진 경우에만 동작한다.
textContent는 <script> 및 <style> 요소를 비롯한 모든 요소의 내용을 가져올 수 있지만. innerText는 그렇지 않다.
innerText는 스타일에 대해서는 알고있지만, textContent와 달리 숨겨진 요소들의 텍스트는 반환하지 않는다.
appendChild() 및 insertBefore()를 사용하여 노드 개체를 DOM에 추가하기
appendChild() 및 insertBefore() 노드 메서드는 JavaScript 노드 개체를 DOM 트리에 삽입할 수 있게 해준다.
<body>
<p>이 길 함께 가는</p>
</body>
var elementNode = document.createElement("strong");
var textNode = document.createTextNode("그대");
document.querySelector("p").appendChild(elementNode);
document.querySelector("strong").appendChild(textNode);
console.log(document.body.innerHTML);
/*
<p>이 길 함께 가는<strong>그대</strong></p>
*/
<body>
<ul>
<li>2</li>
<li>3</li>
</ul>
</body>
var text1 = document.createTextNode("1");
var li = document.createElement("li");
li.appendChild(text1);
var ul = document.querySelector("ul");
/*
앞에서 생성한 li element를 DOM에 추가한다.
<ul>에 대해 ul.firstChild를 호출해서 <li>2</li>에 대한 참조를 전달하고 있다.
*/
ul.insertBefore(li, ul.firstChild);
console.log(document.body.innerHTML);
/* 다음과 같이 출력됨
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
*/
insertBefore() 메서드는 2개의 매개변수를 필요로 하는데, 삽입될 노드와 해당 노드를 삽입하고자 하는 문서 내의 참조 노드이다.
insertBefore() 메서드의 두번째 매개변수를 전달하지 않으면, 이 메서드는 appendChild()처럼 동작한다.
removeChild() 및 replaceChild()를 사용하여 노드를 제거하거나 바꾸기
DOM에서 노드를 제거하는 것은 여러 단계의 과정으로 이루어진다.
먼저 삭제하고자 하는 노드를 선택해야 하고, 다음으로 부모 노드에 대한 접근을 얻어야 하는데
보통 parentNode 속성을 사용하게 된다.
<body>
<div id="A">시간은 흘러가고 </div>
<div id="B">언제까지라도 달려</div>
</body>
// element 노드 삭제
var divA = document.getElementById("A");
divA.parentNode.removeChild(divA);
// 텍스트 노드 삭제
var divB = document.getElementById("B").firstChild;
divB.parentNode.removeChild(divB);
console.log(document.body.innerHTML);
// 빈 div#B만 남게됨.
<body>
<div id="A">이해할수가 없어요</div>
<div id="B">부질없는 짓이었었나</div>
</body>
// element 노드 바꾸기
var divA = document.getElementById("A");
var newSpan = document.createElement("span");
newSpan.textContent = "두근두근";
divA.parentNode.replaceChild(newSpan, divA);
// 텍스트 노드 바꾸기
var divB = document.getElementById("B").firstChild;
var newText = document.createTextNode("두근두근두근");
divB.parentNode.repelaceChild(newText, divB);
console.log(document.body.innerHTML);
/*
이건가?...
<span>두근두근</span>
<div id="B">두근두근두근</div>
*/
제거하거나 바꾸는 대상이 무엇인지에 따라 innerHTML, outerHTML, textContent 속성에 빈 문자열을 주는 것이 더 쉽고 빠를 수도 있다.
브라우저의 메모리 누수가 발생할 수 있으므로 조심?..
replaceChild() 및 removeChild()는 각각 교체되거나 제거된 노드를 반환한다.
기본적으로 해당 노드는 바꾸거나 제거하는 것이므로 사라지지 않는다.
이 동작은 해당 노드가 현재 문서의 범위를 벗어나게 만든다. 해당 노드에 대한 메모리상의 참조는 여전히 가지게 된다.... ???ㅎㅎ?
cloneNode()를 사용하여 노드 복제하기
단일 노드 혹은 노드 및 모든 자식 노드를 복제할 수 있다.
<body>
<ul>
<li>그냥 살아갈만해</li>
<li>하루하루 가긴가거든</li>
</ul>
</body>
var cloneUL = document.querySelector("ul").cloneNode();
console.log(cloneUL.constructor); // HTML UListElement()가 출력됨.
console.log(cloneUL.innerHTML); // ul 만이 복제되었으므로 빈 문자열이 출력됨.
노드와 그 자식 노드를 모두 복제하려면 cloneNode() 메서드의 매개변수로 true를 전달한다.
<body>
<ul>
<li>그냥 살아갈만해</li>
<li>하루하루 가긴가거든</li>
</ul>
</body>
var cloneUL = document.querySelector("ul").cloneNode(true);
console.log(cloneUL.constructor); // HTML UListElement()가 출력됨.
console.log(cloneUL.innerHTML); // <li>그냥 살아갈만해</li><li>하루하루 가긴가거든</li> 가 출력됨.
cloneNode()메서드를 다시 사용하되, 이번에는 자식 노드들도 모두 복제하고 있다.
Element 노드를 복제할 때, 모든 특성 및 값(인라인 이벤트 포함)도 복제된다.
addEventListener()나 node.onclick으로 추가된 것은 복제되지 않는다.
cloneNode(true)를 사용해서 노드와 그 자식을 복제하면 NodeList가 반환될 것이라 생각할 수 있지만, 실제로는 그렇지 않다.
cloneNode() 때문에 문서 내에서 요소 ID가 중복될 수도 있다.
노드 컬렉션(NodeList와 HTMLCollection)에 대한 이해
트리에서 노드 그룹을 선택하거나 사전에 정의된 노드 집합에 접근하려면,
해당 노드들이 NodeList나 HTMLCollection 내에 있어야 한다.
배열과 유사한 이 개체 컬렉션들은 다음과 같은 특징을 가진다.
- 컬렉션은 라이브 상태 혹은 정적 (static)일 수 있다. 이는 컬렉션 내에 포함된 노드들이 현재 문서 혹은 현재 문서에 대한 스냅샷의 일부라는 것을 의미한다.
- 기본적으로 노드는 트리 순서에 따라 컬렉션 내에서 정렬된다. 이 순서는 트리 루트로부터 분기점까지의 선형 경로와 일치한다.
- 컬렉션은 리스트 내의 요소 개수를 나타내는 length 속성을 가진다.
직계 자식 노드 전부에 대한 리스트/컬렉션 얻기
childNodes 속성을 사용하면 직계 자식 노드에 대한 배열 형태의 리스트 (예 : NodeList)가 나온다.
다음 코드에서는 <ul> element를 선택한 후, 해당 속성을 사용하여 <ul>내에 포함된 직계 자식 노드의 전체 리스트를 만들고 있다.
<body>
<ul>
<li>하잉</li>
<li>빠잉</li>
</ul>
</body>
var ulElementChildNodes = document.querySelector("ul").childNodes;
console.log(ulElementChildNodes); // ul 내의 전체 노드로 이루어진 유사 배열 리스트를 출력
/*
NodeList에 대해 루프를 돌 수 있도록 NodeList의 메서드인 forEach를 호출함.
NodeList가 유사 배열이라 가능한 것이며, Array로부터 직접 상속받은 것은 아님 */
Array.prototype.forEach.call(ulElementChildNodes, function(item){
console.log(item); // 배열 내의 각 항목을 출력
});
NodeList나 HTMLCollection을 자바스크립트 배열로 변환
NodeList나 HTMLColletion은 배열 형태이지만, array의 메서드를 상속하는 진정한 자바스크립트 배열은 아니다.
isArray()를 사용하여 확인.
<a href="#"></a>
console.log(Array.isArray(document.links)); // HTMLCollection이지 Array가 아니므로 false가 반환된다.
console.log(Array.isArray(document.querySelectorAll("a"))); // NodeList이지 Array가 아니므로 false가 반환된다.
NodeList나 HTMLCollection을 진정한 자바스크립트 배열로 변환한느 것은 몇가지 이점을 가져다 준다.
NodeList나 HTMLCollection이 라이브 리스트인데 비해
현재 DOM에 국한되지 않은 리스트 스냅샷을 만들 수 있게 해준다.
리스트를 자바스크립트 배열로 변환하면 Array 개체가 제공하는 메서드들(forEach, pop, map, reduce 등)에 접근할 수 있게 된다.
유사배열 리스트를 진정한 자바스크립트 배열로 변환하기 위해 call() 혹은 apply()에 유사 배열 리스트를 전달하면,
call()혹은 apply()는 진짜 자바스크립트 배열을 반환하는 메서드를 호출한다.
다음 코드에서는 .slice() 메서드를 사용하고 있는데, 실제로는 아무것도 잘라내지 않는다.
.slice()가 배열을 반환하므로 리스트를 자바스크립트 array로 반환하는데 사용하고 있을 뿐이다.
<a href="#"></a>
console.log(Array.isArray(Array.prototype.slice.call(document.links))); // true 반환
console.log(Array.isArray(Array.prototype.slice.call(document.querySelectorAll("a")))); // true 반환
DOM 내의 노드 탐색
DOM을 탐색함으로써 다른 노드에 대한 참조를 얻을 수 있다.
- parentNode
- firstChild
- lastChild
- nextSibling
- previousSibling
<body>
<ul><!-- comment -->
<li id="A"></li>
<li id="B"></li>
<!-- comment -->
</ul>
</body>
// ul을 선택해서 저장.
var ul = document.querySelector("ul");
// ul의 parentNode
console.log(ul.parentNode.nodeName); // body
// ul의 첫번째 자식
console.log(ul.firstChild.nodeName); // comment
// ul의 마지막 자식
console.log(ul.lastChild.nodeName); // 줄바꿈이 있기 때문에 comment가 아닌 text가 출력
// 첫번째 li의 nextSibling
console.log(ul.querySelector("#A").nextSibling.nodeName); // text가 출력됨.
// 마지막 li의 previousSibling
console.log(ul.querySelector("#B").previousSibling.nodeName); // text가 출력됨.
DOM을 탐색하는 것에는 element 노드뿐만 아니라 text와 comment 노드도 포함된다.
다음 속성들을 사용하면, text와 comment 노드를 무시하고 DOM을 탐색하는 것이 가능하다.
- firstElementChild
- lastElementChild
- nextElementSibling
- previousElementSibling
- children
- parentElement
- childElementCount
<body>
<ul><!-- comment -->
<li id="A"></li>
<li id="B"></li>
<!-- comment -->
</ul>
</body>
// ul을 선택해서 저장.
var ul = document.querySelector("ul");
// ul의 첫 번째 자식.
console.log(ul.firstElementChild.nodeName); // li 출력
// ul의 마지막 자식
console.log(ul.lastElementChild.nodeName); // li 출력
// 첫 번째 li의 nextSibling
console.log(ul.querySelector("#A").nextElementSibling.nodeName); // li 출력
// 마지막 li의 previousSibling
console.log(ul.querySelector("#B").previousElementSibling.nodeName); // li 출력
// ul의 자식 노드 중 element만 가져오기
console.log(ul.children); // HTMLCollection이며, 모든 자식 노드는 text 노드를 가진다.
// 첫 번째 li의 부모 element
console.log(ul.firstElementChild.parentElement); // ul 출력
contains()와 compareDocumentPosition()으로 DOM트리 내의 Node 위치를 확인하기
contains() 메서드를 사용하면 특정 노드가 다른 노드 내에 포함되어있는지를 알 수 있다.
<!DOCTYPE html>
<html lang="ko">
<body>
<script>
// body가 html 내에 있나여
var inside = document.querySelector("html").contains(document.querySelector("body"));
console.log(inside); // true 출력
</script>
DOM 트리 내에서 주변 노드와 연관된 노드 위치에 대해 보다 확실한 정보를 얻고 싶을 경우,
노드의 compareDocumentPosition() 메서드를 사용하면 된다.
기본적으로 이 메서드는 전달된 노드에 상대적으로 선택된 노드에 대한 정보를 요청할 수 있게 해준다.
compareDocumentPosition()에서 반환되는 숫자 코드와 정보
0 : 동일한 Element
1 : DOCUMENT_POSITION_DISCONNECTED 선택된 노드와 전달된 노드가 동일한 문서에 존재하지 않음.
2 : DOCUMENT_POSITION_PRECEDING 전달된 노드가 선택된 노드 앞에 있음.
4 : DOCUMENT_POSITION_FOLLOWING 전달된 노드가 선택된 노드 뒤에 있음.
8 : DOCUMENT_POSITION_CONTAINS 전달된 노드가 선택된 노드의 조상임.
16, 10 : DOCUMENT_POSITION_CONTAINED_BY 전달된 노드가 선택된 노드의 자손임.
contains()는 선택된 노드와 전달된 노드가 동일한 경우 true를 반환한다.
compareDocumentPosition()은 특정 노드가 다른 노드와 하나 이상의 관계를 가질수 있기 때문에 혼동될 수 있다.
예를 들어 노드가 포함 관계(16)이자 앞에 있는 경우(4), 20을 반환한다.
두 노드가 동일한지 판단하기
다음 조건들이 만족되는 경우에만 동일하다.
- 두 노드가 동일한 형식이다.
- nodeName, localName, namespaceURI, prefix, nodeValue 문자열 특성이 동일하다.
즉 둘다 null이거나, 동일한 길이와 동일한 문자를 가져야 한다.
- NamedNodeMaps 특성이 동일하다. 즉 둘 다 null 이거나 길이가 동일해야 하며,
하나의 맵 내에 존재하는 각 노드들과 다른 맵에 존재하는 노드가 동일해야 하되 인덱스가 동일할 필요는 없다.
- childNodes NodeList가 동일하다. 즉 둘 다 null이거나, 동일한 길이를 가지고 같은 인덱스의 노드가 동일해야 한다.
정규화가 동일성에 영향을 미칠 수 있으므로, 이를 피하기 위해서는 비교를 수행하기 전에 노드를 정규화해야 한다.
isEqualNode() 메서드를 호출하면, 매개변수로 전달하는 노드와 동일한지를 물어보게 된다.
다음 코드에서는 동일한 두 노드의 예와 동일 조건을 만족시키지 못하는 두 노드의 예를 보여준다.
<body>
<input type="text">
<input type="text">
<textarea></textarea>
<textarea></textarea>
</body>
<script>
var input = document.querySelectorAll("input");
console.log(input[0].isEqualNode(input[1]));
// 자식 text 노드가 동일하지 않으므로 false가 출력
var textarea = document.querySelectorAll("textarea");
console.log(textarea[0].isEqualNode(textarea[1]));
</script>
두 노드가 완전히 동일한지가 아니라, 두 노드 참조가 동일한 노드를 참조하고 있는지를 알고 싶다면,
=== 연산자를 사용하여 간단하게 확인해볼 수 있다.
document.body === document.body
'UI개발' 카테고리의 다른 글
DOM - Element 노드 / Element 노드 선택 (0) | 2016.09.19 |
---|---|
DOM - Document 노드 (0) | 2016.09.19 |
-webkit-tap-highligh-color (0) | 2016.08.07 |
ui, ux (0) | 2016.08.07 |
검색엔진최적화 (0) | 2016.08.07 |