
인트로
겸사겸사 DOM부터 공부하자라는 마인드로 시작합니다.

DOM의 정의
DOM은 Document Object Model의 약자입니다. 직역하면 문서 객체 모델입니다.
HTML 문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API(application programming interface),
즉 프로퍼티와 메서드를 제공하는 트리 자료구조이다. -모자딥다-
HTML, XML 문서의 프로그래밍 interface이다. -MDN-
정의가 너무 어려우니 우선 DOM을 왜 쓰는지 보겠습니다.
MDN에서는 DOM이 문서의 구조화된 표현을 제공하며 프로그래밍 언어가 DOM 구조에 접근할 수 있도록 해준다고 합니다. 접근해서 뭘 하냐면요... 문서 구조, 스타일, 내용을 변경합니다. 웹페이지도 일종의 문서입니다.
우리는 이제 자바스크립트와 같은 스크립팅 언어를 이용해서 DOM 수정이 가능합니다.
결국 웹페이지를 변경하기 위해 DOM을 변경합니다
Node 란?
DOM에 들어가려고 하는데 갑자기 노드가 언급됩니다.
HTML DOM이 노드라는 계층적 단위에 정보를 저장하고 있기 때문인데요.
트리 자료구조를 위한 빌드업을 시작합니다.
HTML 배울 때 항상 보이는 html 요소 구조사진입니다.
이 친구들이 노드 객체로 변환됩니다. 그리고 DOM을 구성합니다.
이때 어트리뷰트는 어트리뷰트 노드, 텍스트 콘텐츠는 텍스트 노드로 변환됩니다.
HTML 문서는 HTML 요소들의 집합으로 이루어지고 중첩 관계를 갖습니다.
(중첩관계가 뭔지 아리까리하시면 더 보기를 확인하세요)
콘텐츠 영역에 텍스트뿐만 아니라 다른 HTML 요소도 포함될 수 있습니다. 태그가 중첩되고 중첩 관계가 생깁니다.
<p>안녕하세요 제가 이제 기울어져 볼게요? <i>기울어졌죠?</i> 그럼 <b>이제 진해질게요</b> </p>

HTML 요소 간 중첩 관계로 인해 계층적인 부자(not rich, yes parent-child) 관계가 형성됩니다.
이러한 관계들을 반영하여 노드 객체들을 트리 자료 구조로 구성합니다.
(여기서 노드 객체는 위에 언급한 HTML요소가 객제화된 것입니다.)
트리 자료구조 빌드업 성공
노드 객체들로 구성된 트리 자료구조를 DOM이라 합니다. DOM을 DOM 트리라고도 부릅니다.
트리 자료구조(tree data structure)는 다음과 같이 생겼습니다.
노드들의 계층 구조로 이루어져 있는데요. 노드 간에 부자, 형제 관계를 표현할 수 있습니다.
하나의 자료 뒤에 여러 개의 자료가 존재하기 때문에 트리는 비선형 자료구조에 속합니다.

부모 노드가 없는 최상위 노드를 루트 노드라고 합니다.
자식 노드가 없는 노드를 리프 노드라고 합니다.
노드 객체의 타입
노드 객체도 종류가 있고 상속 구조를 가집니다. 노드 객체는 총 12개의 노드 타입을 가지는데요.
중요한 4가지만 봅시다. 아래 그림과 함께 보면 쉽게 이해할 수 있습니다.
- 문서 노드 : DOM 트리의 루트 노드, document 객체를 가리킵니다,
- HTML 문서 전체를 나타냅니다. window.document 또는 document로 참고 가능합니다.
- 브라우저에서 하나의 전역 객체 window를 공유하기 때문에 HTML 문서당 document 객체는 유일합니다.
- DOM 트리 노드에 접근하기 위한 문지기인데요. 얘를 통해 요소, 어트리뷰트, 텍스트 노드에 접근해야 합니다.
- 요소 노드 : HTML 요소를 가리키는 객체입니다. 유일하게 속성노드를 가질 수 있습니다.
- 중첩 관계에 의해 부자 관계를 가지며, 이 부자 관계를 통해 정보를 구조화합니다.
- 어트리뷰트(속성) 노드 : HTML 요소의 어트리뷰트를 가리키는 객체입니다.
- 해당 어트리뷰트가 지정된 요소 노드에 연결되어 있습니다. 부모 노드와 연결되어 있지 않는데요.
- 부모 노드가 없으므로 요소 노드의 형제 노드가 아닙니다.
- 어트리뷰트 노드에 접근하고 싶다면 연결된 요소 노드에 접근해야 합니다.
- 텍스트 노드 : HTML 요소의 텍스트를 가리키는 객체입니다.
- 요소 노드의 자식 노드이며 동시에 자식 노드를 가질 수 없는 리프 노드입니다.
- 텍스트 노드에 접근하려면 부모 노드인 요소 노드에 접근해야 합니다.
노드 객체의 상속 구조... 읽어만 보기
DOM은 HTML 문서의 계층적 구조와 정보를 표현하며
이를 제어할 수 있는 API를 제공하는 트리 자료구조이다.
-모자딥다-
모자딥다가 DOM을 저렇게 멋있게 말했는데요.
이제 DOM이 트리 자료구조인건 알고 있으니 앞부분을 이해하려고 노력해봅니다.
DOM을 구성하는 노드 객체는 자신의 구조와 정보를 제어할 수 있는 DOM API를 사용할 수 있습니다.
DOM API를 통해 자신의 부모, 형제, 자식을 탐색하고 자신의 어트리뷰트와 텍스트를 조작할 수 있는 겁니다!
이때 DOM을 구성하는 노드 객체는 브라우저 환경에서 추가 제공하는 호스트 객체입니다.
노드 객체도 자바스크립트 객체이므로 프로토타입에 의한 상속 구조를 가집니다.
모든 노드 객체는 Object, EventTarget, Node 인터페이스를 상속받습니다.
문서 노드는 Document, HTMLDocument 인터페이스를 상속받습니다.
어트리뷰트 노드는 Attr, 텍스트 노드는 CharacterData 인터페이스를 상속받습니다.
요소 노드는 Element 인터페이스를 상속받습니다. ....
이외에도 요소 노드는 다양한 인터페이스를 상속받기 때문에 자세한 내용을 알고 싶다면 모자딥다 39.1.3을 확인하시면 됩니다. 거기까지 하면 내용이 너무 길어지니 여기까지 하겠습니다. 암튼 상속 구조를 가진다구
노드 객체의 상속 구조는 개발자 도구의 Elements 패널 우측의 Properties 패널에서 확인 가능합니다.
결국 DOM API를 통해 노드 타입에 따라 필요한 기능을 제공받고 HTML의 구조나 내용 또는 스타일을 조작하는 겁니다. DOM API야! 고맙다!
이제 좀 써봅시다
요소 노드 취득
이제 예제와 함께 직접 사용을 좀 해보려고 합니다.
내용이든 스타일이든 뭔가를 조작하려면 우선 요소 노드를 취득해야 합니다.
위에서도 봤지만 텍스트 노드나 어트리뷰트 노드를 조작하고 싶으면 요소 노드에 접근해야 합니다.
취득하는 방법이 다양하니 하나씩 보겠습니다.
1. id를 이용한 요소 노드 취득
Document.prototype.getElementById 메서드는 인수로 전달된 id 값을 갖는 첫 번째 요소 노드만 반환합니다.
인수로 전달된 id 값을 갖는 HTML 요소가 없다면 null을 반환합니다.
<h2 id="id_h2">h2입니다?</h2>
<script>
const $h2 = document.getElementById('id_h2');
console.log($h2); // <h2 id="id_h2" style="color: red;">h2입니다?</h2>
$h2.style.color = 'red';
</script>
(추가 예제)
HTML 요소에 id를 주면 id 값과 동일한 이름의 전역 변수가 암묵적으로 선언됩니다.
그리고 해당 노드 객체가 할당됩니다.
<h2 id="id_h2">h2입니다?</h2>
<script>
console.log(id_h2 === document.getElementById('id_h2')); //true
</script>
하지만 id 값과 동일한 이름의 전역 변수가 이미 있다면 해당 전역 변수에 노드 객체가 재할당되지 않습니다.
<h2 id="id_h2">h2입니다?</h2>
<script>
let id_h2 = 10;
console.log(id_h2); //10
console.log(id_h2 === document.getElementById('id_h2')); //false
</script>
2. 태그를 이용한 요소 노드 취득
Document.prototype/Element.prototype.getElementsByTagName 메서드는 HTMLCollection 객체를 반환합니다.
HTMLCollection은 여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체입니다.
<ul>
<li id="html">HTML</li>
<li id="css">CSS</li>
<li id="js">JS</li>
</ul>
<script>
const $li = document.getElementsByTagName('li');
console.log($li);
</script>
<ul>
<li id="html">HTML</li>
<li id="css">CSS</li>
<li id="js">JS</li>
</ul>
<script>
const $li = document.getElementsByTagName('li');
[...$li].forEach($li => {
$li.style.color = 'red';
console.log($li);
// 반복을 돌며
// <li id="html" style="color: red;">HTML</li>
// <li id="css" style="color: red;">CSS</li>
// <li id="js" style="color: red;">JS</li>
})
</script>
3. class를 이용한 요소 노드 취득
Document.prototype/Element.prototype.getElementsByClassName의 메서드는 HTMLCollection 객체를 반환합니다.
<ul>
<li class="web html">HTML</li>
<li class="web css">CSS</li>
<li class="web js">JS</li>
</ul>
<script>
const $web = document.getElementsByClassName('web');
console.log($web); // HTMLCollection(3) [li.web.html, li.web.css, li.web.js] ...
//스타일 변경하기
[...$web].forEach(li => li.style.color = 'red');
//인덱스를 사용했습니다 인덱스없이 forEach로 sytle 변경도 가능합니다.
const $js = document.getElementsByClassName('web js')[0];
console.log($js);// <li class="web js" style="color: blue;">JS</li>
$js.style.color = "blue"
</script>
4. CSS 선택자를 이용한 요소 노드 취득
Document.prototype/Element.prototype.querySelector 메서드는 인수로 전달한 CSS 선택자를 만족시키는 하나의 요소 노드를 탐색하여 반환합니다.
- 만족시키는 요소 노드가 여러 개일 경우 첫 번째 요소 노드만 반환합니다.
- 만족시키는 요소 노드가 없을 경우 null을 반환합니다.
- 문법 오류가 있을 경우 DOMException 에러가 발생합니다.
Document.prototype/Element.prototype.querySelectorAll 메서드는 만족시키는 모든 요소 노드를 탐색하여 반환합니다.
- 만족시키는 요소 노드가 없을 경우 빈 NodeList 객체를 반환합니다 (NodeList는 유사 배열 객체이며 이터러블입니다.)
- 문법 오류는 querySelector와 같습니다.
<ul>
<li class="web html">HTML</li>
<li class="web css">CSS</li>
<li class="web js">JS</li>
</ul>
<script>
const $liAll = document.querySelectorAll('ul > li')
const $li = document.querySelector('ul > li')
const $html = document.querySelector('.web.html')
console.log($liAll); // NodeList(3) [li.web.html, li.web.css, li.web.js] ...
console.log($li); //<li class="web html">HTML</li>
console.log($html); //<li class="web html">HTML</li>
</script>
HTML 문서의 모든 요소 노드를 취득하려면 querySelectorAll 메서드의 인수로 전체 선택자 '*'를 전달합니다.
CSS 선택자를 이용하는 경우 앞에 나온 방법보다 느리지만 구체적이고 일관된 방식으로 요소 노드를 취득할 수 있다는 장점이 있습니다.
따라서 id 어트리뷰트 외에는 CSS 선택자를 이용한 취득 방법을 추천합니다.
노드를 취득 가능한지/존재하는지 확인해 주는 메서드
Element.prototype.matches 메서드를 사용하면 특정 요소 노드를 취득할 수 있는지 확인할 수 있습니다.
이벤트 위임할 때 사용하면 유용하다고 합니다.
Element.prototype.hasChildNodes 메서드를 사용하면 자식 노드가 존재하는지 확인할 수 있습니다.
이때 텍스트 노드를 포함하여 존재를 확인하므로 요소 노드가 존재하는지 확인하려면 children.length 또는 childElementCount 프로퍼티를 사용하면 됩니다.
HTMLCollection, NodeList
두 가지 모두 여러 개의 결괏값을 반환하기 위한 DOM 컬렉션 객체인데요.
둘 다 유사 배열 객체이며 이터러블이기 때문에 for...of문으로 순회할 수 있습니다.
스프레드 문법을 사용해서 배열로 변환도 가능합니다.
HTMLCollection의 경우 노드 객체의 상태 변화를 실시간으로 반영하는 live 객체입니다.
(관련된 재미있는 예제가 있습니다)
아래 코드는 모든 li의 글자 색을 바꾸고자 작성했습니다.
<style>
.web {
color: red
}
.blue {
color: blue
}
</style>
</head>
<body>
<ul>
<li class="web html">HTML</li>
<li class="web css">CSS</li>
<li class="web js">JS</li>
</ul>
<script>
const $li = document.getElementsByClassName('web');
for (let i = 0; i < $li.length; i++) {
$li[i].className = 'blue';
}
</script>
하지만 결과는 다음과 같습니다.

가운데 색이 바뀌지 않았죠?
HTMLCollection을 돌면서 실시간으로 'web' css 선택자를 만족하는 요소가 사라졌기 때문입니다.
for문 안에서 $li를 찍어봤습니다. 실시간으로 li.web.html이 사라지고 요소가 2개밖에 안 남았죠?
i = 1 일 때 $li[1]이 CSS가 아니라 JS가 담긴 요소가 되어버렸습니다.

이러한 문제는 for문을 역방향으로 순회하거나 while문을 사용하여 회피할 수 있습니다.
사실 그냥 HTMLCollection 객체를 안 쓰면 됩니다.
forEach, map, filter, reduce와 같은 고차함수를 사용하면 해결됩니다.
HTMLCollection의 저런 동작을 따로 생각하기 싫다면 그냥 querySelectorAll을 사용하면 됩니다.
NodeList는 대부분 non-list 객체로 동작하므로 위와 같은 문제가 발생하지 않습니다.
그러나!!!! childNodes 프로퍼티가 반환하는 Nodelist 객체는 live 객체로 동작하므로 주의해야 합니다.
어쨌든 NodeList도 문제가 있네요?.... 하지만 결국 똑똑한 누군가는 해결책을 만듭니다.
그냥 HTMLCollection이나 NodeList 객체를 배열로 변환하여 사용하면 됩니다. 스프레드 문법을 사용하면 간단합니다.
게다가 배열의 유용한 고차 함수들을 훨씬 다양하게 사용할 수도 있습니다.
결론은 배열로 바꿔서 써라입니다. 우리에겐 스프레드가 있으니까
노드 탐색 가보자고
우리는 DOM 트리 상의 노드를 탐색할 수 있습니다.
먼저 어떠한 요소 노드를 취득한 후 형제 노드를 탐색하거나, 부모 노드를 탐색할 수 있습니다,
사실 HTML 요소 사이에는 스페이스, 탭, 줄 바꿈 등의 공백 문자는 텍스트 노드를 생성합니다.
Node.prototype.nextSibling, Node.prototype.previousSibling의 경우 요소 노드뿐만 아니라 모든 텍스트 노드를 포함하기 때문에 원하는 값이 안 나옵니다.
아래 예제에서도 줄 바꿈 등의 텍스트 노드를 보여줬습니다.
이럴 때는 nextElementSibling이나 previousElementSibling을 사용하면 됩니다.
<ul>
<li class="web html">HTML</li>
<li class="web css">CSS</li>
<li class="web js">JS</li>
</ul>
<script>
const $li = document.querySelector('.css');
console.log($li.parentNode); // <ul>...</ul>
console.log($li.childNodes); // NodeList [text] ...
console.log($li.firstChild); // "CSS"
console.log($li.lastChild); // "CSS"
console.log($li.nextSibling); // #text 줄바꿈텍스트
console.log($li.nextSibling.nextSibling); // <li class="web js">...<./li>
console.log($li.nextElementSibling); // <li class="web js">...<./li>
console.log($li.previousSibling.previousSibling); // <li class="web html">...<./li>
console.log($li.previousElementSibling); // <li class="web html">...<./li>
</script>
텍스트 노드는 DOM 트리의 리프 노드이므로 부모 노드가 텍스트 노드인 경우는 없습니다.
자식 노드 탐색 가보자고
Node.prototype.childNodes와 Node.prototype.children을 통해 자식 노드를 탐색할 수 있습니다.
- childNodes는 자식 노드를 모두 탐색하여 NodeList에 담아 반환합니다.
텍스트 노드도 포함되어 있을 수 있습니다. - children은 자식 노드 중에서 요소 노드만 모두 탐색하여 HTMLCollection에 담아 반환합니다.
텍스트 노드가 포함되어 있지 않습니다. - firstChild는 첫 번째 자식 노드를 반환합니다. 텍스트 노드이거나 요소 노드입니다.
- lastChild는 마지막 자식 노드를 반환합니다. 텍스트 노드이거나 요소 노드입니다.
- firstElementChild는 첫 번째 자식 요소 노드를 반환합니다.
- lastElementChild는 마지막 자식 요소 노드를 반환합니다.
<ul>
<li class="web html">HTML</li>
<li class="web css">CSS</li>
<li class="web js">JS</li>
</ul>
<script>
const $ul = document.querySelector('ul');
console.log($ul.childNodes); //NodeList(7) 요소노드 + 텍스트노드
console.log($ul.children); // HTMLCollection(3) 요소노드
console.log($ul.firstChild); // #text -> 텍스트 노드
console.log($ul.lastChild); // #text -> 텍스트 노드
console.log($ul.firstElementChild); // <li class="web html">HTML</li>
console.log($ul.lastElementChild); //<li class="web js">JS</li>
</script>
노드 정보 줍줍
- Node.prototype.nodeType : 노드 객체의 종류, 즉 노드 타입을 나타내는 상수를 반환합니다. 노드 타입 상수는 Node에 정의되어 있습니다.
- Node.ELEMENT_NODE : 요소 노드 타입을 나타내는 상수 1 반환
- Node.TEXT_NODE : 텍스트 노드 타입을 나타내는 상수 3 반환
- Node.DOCUMENT_NODE : 문서 노드 타입을 나타내는 상수 9 반환
- Node.prototype.nodeName : 노드 이름을 문자열로 반환합니다.
요소 노드 : 대문자 문자열로 태그 이름 ex) "UL" 반환
텍스트 노드 : 문자열 "#text"를 반환
문서 노드 : 문자열 "#document"를 반환
요소 노드의 텍스트 조작
위에서 본 노드 탐색, 노드 정보 프로퍼티는 모두 읽기 전용 접근자 프로퍼티입니다.
Node.prototype.nodeValue 프로퍼티는 참조와 할당이 모두 가능한데요.
노드 객체의 nodeValue 프로퍼티를 참조하면 텍스트 노드의 텍스트를 말합니다.
텍스트 노드가 아닐 경우 null을 반환합니다.
<ul>
<li class="web html">HTML</li>
<li class="web css">CSS</li>
<li class="web js">JS</li>
</ul>
<script>
console.log(document.nodeValue); // null
const $li = document.querySelector('li');
console.log($li.nodeValue); // null
const $textNode = $li.firstChild;
console.log($textNode.nodeValue); //HTML
</script>
nodeValue 프로퍼티에 값을 할당하면 텍스트를 변경할 수 있습니다. (간단한 예제)
<ul>
<li class="web html">HTML</li>
<li class="web css">CSS</li>
<li class="web js">JS</li>
</ul>
<script>
const $li = document.querySelector('li');
const $textNode = $li.firstChild;
$textNode.nodeValue = "html";
console.log($textNode.nodeValue); //html
</script>
innerText보다 textContent
Node.prototype.textContect 프로퍼티를 통해 요소 노드의 텍스트와 모든 자손 노드의 텍스트를 모두 취득하거나 변경할 수 있습니다.
요소 노드의 textContent를 참조하면 요소 노드의 콘텐츠 영역 내의 텍스트를 모두 반환합니다.
즉, 요소 노드의 childNodes가 반환하는 텍스트 노드의 값을 모두 반환한다는 이야기입니다.
이때 HTML 마크업은 무시됩니다.
<ul>
<li class="web html">HTML</li>
<li class="web css">CSS</li>
<li class="web js">JS</li>
</ul>
<script>
const $ul = document.querySelector('ul');
console.log($ul.textContent);
요소 노드의 textContent에 문자열을 할당하면 요소 노드의 모든 자식 노드가 제거됩니다.
그리고 할당한 문자열이 텍스트로 추가됩니다. 할당한 문자열에 HTML 마크업이 포함되어도 그냥 문자열로 인식됩니다.
<ul>
<li class="web html">HTML</li>
<li class="web css">CSS</li>
<li class="web js">JS</li>
</ul>
<script>
const $ul = document.querySelector('ul');
console.log($ul.textContent); // "HTML CSS JS"
$ul.textContent = "hi"
console.log($ul.textContent); // "hi"
$ul.textContent = '<div>상자</div>'
console.log($ul.textContent); // "<div>상자</div>"
</script>
innerHTML로 유사하게 동작할 수 있는데 textContent 쓰는 게 좋습니다.
textContent가 더 빠르고요
열심히 달려왔다.. DOM을 조작해보자..
이제 드디어 DOM을 조작하려고 합니다.
이제 새로운 노드를 만들어서 DOM에 추가하거나 기존 노드를 삭제 혹은 교체하려고 합니다.
DOM 조작이 성능에 영향을 주어 DOM 조작은 성능 최적화를 위해 주의해서 다뤄야 한다고 합니다.
성능에 영향을 주는 이유는 나중에 만나보죠. DOM만으로 벅차니까..
innerHTML
Element.prototype.innerHTML를 사용해서 요소 노드의 HTML 마크업을 취득하거나 변경할 수 있습니다.
요소 노드의 콘텐츠 영역 내에 포함된 모든 HTML 마크업을 문자열로 반환합니다.
<ul id="ul">
<li id="li" class="web html">HTML</li>
<li class="web css">CSS</li>
<li class="web js">JS</li>
</ul>
<script>
console.log(document.getElementById('ul').innerHTML);
</script>
innerHTML에 문자열을 할당하면 요소 노드의 모든 자식 노드가 제거됩니다.
그리고 할당한 문자열에 포함되어 있는 HTML 마크업이 파싱 되어 요소 노드의 자식 노드로 DOM에 반영됩니다.
document.getElementById('ul').innerHTML = `<li>Hi</li><li>bye</li>`;
innerHTML을 통해 아주 쉽게 DOM 조작이 가능한데 단점이 있습니다.
먼저 사용자로부터 입력받은 데이터를 innerHTML에 할당하면 크로스 사이트 스크립팅 공격을 받을 수 있습니다.
마크업 내에 악성 코드가 포함되어 있더래도 실행될 수 있습니다.
두 번째는 마크업 문자열을 할당할 경우 요소 노드의 모든 자식 노드를 제거하고 할당한 값으로 DOM을 변경하는 것입니다. 유지하고 싶은 자식 노드까지 모두 제거하고 다시 생성해야 하니 별로입니다. 일 두 번 하는 느낌이죠?
그리고 삽입 위치를 지정할 수 없습니다.
too much 단점... 그래서 소개합니다
바로 insertAdjacentHTML
Element.prototype.insertAdjacentHTML 메서드는 기존 요소를 제거하지 않으면서 삽입 위치를 지정할 수 있습니다.
첫 번째 인수로 전달한 위치에 두 번째 인수로 전달한 HTML 마크업 문자열을 파싱 한 후 삽입해 줍니다.
첫 번째 인수로 전달 가능한 값은 4가지입니다.
- 'beforebegin' - element 앞에
- 'afterbegin' - element 앞에 가장 첫 번째 child
- 'beforeend' - element 앞에 가장 마지막 child
- 'afterend' - element 뒤에
innerHTML보다 낫죠? 하지만 여전히 크로스 사이트 스크립팅 공격에는 취약합니다.
노드를 사용해서 DOM을 조작해보자
노드를 직접 생성/삽입/삭제/치환할 수도 있습니다. 그리고 지금부터 그걸 해보려고 합니다.
노드 생성
요소 노드
Document.prototype.createElement(tagName) 메서드를 사용해 요소 노드를 생성할 수 있습니다.
태그 이름을 문자열로 인수에 전달하면 됩니다.
텍스트 노드
Document.prototype.createTextNode(text) 메서드는 텍스트 노드를 생성해 줍니다.
text에는 문자열을 인수에 전달하면 됩니다.
이때 생성한다고 끝이 아니에요.
생성한 요소 노드는 기존 DOM에 추가된 상태가 아니고 텍스트 노드의 경우 요소 노드에 자식 노드로 추가되지 않았습니다. 둘 다 그냥 홀로 외롭게 있는 상태예요.
생성한 후에 DOM에 추가를 해줘야 합니다.
텍스트 노드를 요소 노드의 자식 노드로 추가해 봅시다.
Node.prototype.appendChild(childNode) 메서드를 이용해서
인수로 전달한 노드를 호출한 노드의 마지막 자식노드로 추가해 줍니다.
const $li = document.createElement('li');
console.log($li.childNodes);//NodeList []
const textNode = document.createTextNode('React');
$li.appendChild(textNode);
console.log($li.childNodes);//NodeList [text]
물론 위 예제처럼 생성한 요소 노드가 아무런 자식 노드를 가지고 있지 않다면 요소 노드.textContent를 사용해도 됩니다.
요소 노드를 DOM에 추가해 봅시다.
Node.prototype.appendChild 메서드를 사용해서 생성한 요소 노드를 DOM 있는 요소 노드의 자식 요소로 추가합니다.
const $ul = document.querySelector('ul');
$ul.appendChild($li);
(한 번에 여러 개의 노드를 생성하고 추가하기 예제)
근데 이 방법 비효율적이래요 재미 봤다^^ 다음 예제로 고고
<ul id="week">
</ul>
<script>
const $week = document.getElementById('week');
['Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat', 'Sun'].forEach(text => {
const $li = document.createElement('li');
const textNode = document.createTextNode(text);
$li.appendChild(textNode); //텍스트 노드를 요소 노드에 연결
$week.appendChild($li); //DOM에 요소 노드 연결
})
</script>

DOM을 변경할수록 성능에 영향이 가므로 안 좋다고 합니다. 이럴 때 컨테이너 요소를 갈아서 드셔보세요.
1. 컨테이너 요소를 생성한다.
2. DOM에 추가할 요소 노드를 컨테이너 요소에 자식 노드로 추가한다.
3. 컨테이너 요소를 DOM에 추가한다.
이러면 DOM은 딱 한 번만 변경되겠죠. DOG이득
근데 이것도 무조건 좋은 건 아닙니다. 이것 때문에 쓸모없는 컨테이너를 DOM에 추가해야 해요.
const $week = document.getElementById('week');
const $container = document.createElement('div');
['Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat', 'Sun'].forEach(text => {
const $li = document.createElement('li');
const textNode = document.createTextNode(text);
$li.appendChild(textNode);
$container.appendChild($li); //컨테이너에 요소 노드 연결
})
$week.appendChild($container); //DOM에 컨테이너 연결
하지만 결국 우린 답을 찾을 것이다.
소개합니다. DocumentFragment 부모 노드가 없어 별도로 존재하는 특징을 가지고 있는 노드입니다.
별도의 서브 DOM을 구성해 기존 DOM에 추가하려고 씁니다. 별도의 DOM이기 때문에 원래 DOM에 영향을 안 줘요.
심지어 DocumentFragmet를 사용해서 DOM에 추가하면 본인은 사라집니다..
우리가 원하는 자식 노드만 DOM에 추가되는 엄청난...
const $week = document.getElementById('week');
const $fragment = document.createDocumentFragment();
['Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat', 'Sun'].forEach(text => {
const $li = document.createElement('li');
const textNode = document.createTextNode(text);
$li.appendChild(textNode);
$fragment.appendChild($li); //별도의 DOM에 요소 노드 연결
})
$week.appendChild($fragment); //DOM에 연결 이때 fragment를 제거됨

노드를 삽입할 거예요
appendChild를 사용하면 항상 마지막 자식 노드로 추가합니다.
지정한 위치에 노드를 삽입하려면 어떻게 해야 할까요? 지정 위치에 삽입을 도와주는 메서드가 있습니다.
Node.prototype.insertBefore(newNode, childNode) 메서드는 newNode를 childNode 앞에 삽입합니다.
childNode 노드는 반드시 insertBefore 메서드를 호출한 노드의 자식 노드여야 해요.
childNode 노드가 null이면 newNode는 호출한 노드의 마지막 자식 노드로 추가됩니다.
<ul id="week">
<li>Mon</li>
<li>Wed</li>
</ul>
<script>
const $week = document.getElementById('week');
const $tue = document.createElement('li');
$tue.textContent = 'Tue';
$week.insertBefore($tue, $week.lastElementChild);
</script>
노드를 이동할 거예요
DOM에 이미 존재하는 노드를 appendChild나 insertBefore 메서드를 사용해서 다시 추가하면 노드를 이동시킬 수 있습니다. 현재 위치에서 노드가 제거되고 새로운 위치에 노드를 추가하기 때문이죠.
난 복사가 하고 싶은데
Node.prototype.cloneNode 메서드를 사용하면 노드의 사본을 생성합니다.
매개변수에 true를 인수로 전달하면 깊은 복사를 해서 자손 노드가 포함된 사본을 생성합니다.
false를 전달하거나 아무 값도 주지 않으면 얕은 복사를 해서 자신의 사본을 생성합니다. 이때 얕은 복사는 텍스트 노드도 없습니다.
<ul id="week">
<li>Sun</li>
</ul>
<script>
const $week = document.getElementById('week');
const $Sun = document.querySelector('li');
const $Sun2 = $Sun.cloneNode();
$Sun2.textContent = 'Sun2';
$week.appendChild($Sun2);
const $week2 = $week.cloneNode(true);
console.log($week2);
$week.appendChild($week2);
</script>
저는 교체하고 싶어요
Node.prototype.replaceChild(newChild, oldChild) 메서드는 호출한 노드의 자식 노드를 다른 노드로 교체합니다.
oldChild 노드를 newChild로 교체합니다. 이때 oldChild노드는 DOM에서 제거됩니다.
oldChild는 replacechild 메서드를 호출한 노드의 자식 노드여야 합니다.
마지막으로 삭제를 해보자
Node.prototype.removeChild(child) 메서드를 통해 원하는 노드를 삭제할 수 있습니다.
이때도 child는 removeChild 메서드를 호출한 노드의 자식 노드여야 합니다.
어트리뷰트.. 끝이 보인다
알다시피 HTML 요소는 여러 개의 어트리뷰트를 가집니다.
글로벌, 이벤트 핸들러, 특정 HTML 요소에만 사용 가능한 어트리뷰트 다양하죠.
초반에 이야기했지만 어트리뷰트들은 어트리뷰트 노드로 변환되어 요소 노드와 연결됩니다.
이때 어트리뷰트당 하나의 어트리뷰트 노드가 생성되는데요.
HTML 요소에 어트리뷰트가 3개였다? 그럼 3개의 어트리뷰트 노드가 생기는 겁니다.
우리는 Element.prototype.attributes 프로퍼티로 취득할 수 있습니다.
해당 메서드는 읽기 전용이며 NameNodeMap 객체를 반환합니다.
<input type="password" placeholder="enterpw" id="pw">
<script>
const { attributes } = document.getElementById('pw');
console.log(attributes); // NameNodeMap {0: type, 1: placeholder, 2: id, type: type ...
console.log(attributes.id.value); //pw
console.log(attributes.placeholder.value); //enterpw
</script>
어트리뷰트 조작하고 싶은데
Element.prototype.getAttribute/setAttribute 메서드를 쓰면 attributes 프로퍼티 바로 값을 취득하고 변경할 수 있습니다.
<input type="password" placeholder="enterpw" id="pw">
<script>
const $pw = document.getElementById('pw');
console.log($pw.getAttribute('id')); //pw
$pw.setAttribute('id', 'password');
console.log($pw.getAttribute('id')); //password
</script>
훨씬 편하네요. 굳
Element.prototype.hasAttribute 메서드를 통해 어트리뷰트가 존재하는지 확인도 가능합니다.
Element.prototype.removeAttribute 메서드를 사용하면 삭제도 됩니다.
상태가 두 개지요
요소 노드 객체는 HTML 어트리뷰트에 대응하는 프로퍼티를 가지고 있습니다.
이 DOM 프로퍼티들의 초기값은 HTML 어트리뷰트 값입니다.
왜 비슷한 게 두 개나 있는 것이지..?라는 생각이 들 수 있지만 둘의 역할은 다릅니다.
HTML 어트리뷰트의 역할은 HTML 요소의 초기 상태를 지정합니다. 이것은 변하지 않는 값입니다.
DOM 프로퍼티의 초기값이 HTML 어트리뷰트 값이라며.. 똑같은 거 아님?이라는 생각이 들지만 첫 렌더링까지는 동일하고 그 이후부터 달라집니다.
요소 노드는 상태를 가지고 사용자가 무언가를 입력하면 요소 노드의 상태가 변합니다.
checkbox의 경우 사용자의 체크 여부에 따라 상태가 달라진다고 볼 수 있겠죠.
이때 상태가 변경됐을 때의 최신 상태와 초기 상태도 관리해야 합니다. 새로고침할 때 초기 상태로 돌아가야 하니까요.
즉 요소 노드는 2개의 상태를 가집니다. 초기 상태와 최신 상태입니다.
초기 상태는 어트리뷰트 노드가 관리하며, 최신 상태는 DOM 프로퍼티가 관리합니다.
어트리뷰트 노드
어트리뷰트 노드에서 관리하는 어트리뷰트 값은 상태가 변경되어도 초기 상태를 그래도 유지합니다.
어트리뷰트 노드의 값을 취득하거나 바꾸려면 getAttribute/setAttribute 메서드를 사용해야 합니다.
이때 getAttribute로 가져온 값은 HTML 요소에 지정한 어트리뷰트 값, 그러니까 초기값이겠죠? 이 값은 항상 동일합니다.
setAttribute를 사용하면 HTML 요소에 지정한 어트리뷰트 값을 바꾸는 게 됩니다. (해보면 진짜로 바뀝니다)
DOM 프로퍼티
최신 상태를 관리하는 이 친구 상태 변화에 반응해 언제나 최신 상태를 유지하는 엄청난 인싸입니다.
사용자 입력에 의해 최신상태 값이 바뀌어도 getAttribute로 취득한 값은 변하지 않습니다.
DOM 프로퍼티에 값을 할당하는 것은 최신 상태 값을 변경하는 것을 말합니다. 초기상태에는 영향이 없어요.
<input type="password" placeholder="enterpw" id="pw" value="--">
<script>
const $pw = document.getElementById('pw');
$pw.value = '..'
console.log($pw.value); //..
console.log($pw.getAttribute('value')); //--
</script>
근데 여기에 함정이 있습니다.
본인은 실제로 placeholder 값을 바꿔서 예시를 보이려고 했는데 getAttribute에도 바뀐 값이 나와서 순간 당황했는데요.
모든 DOM 프로퍼티가 사용자의 입력에 의해 변경된 최신 상태를 관리하는 것은 아닙니다.
사용자 입력과 관계없는 placeholder나 id.. 이런 친구들은 상태 변화와 관계없이 항상 동일한 값을 유지합니다.
즉, 어트리뷰트 값이 변하면 프로퍼티 값도 변하고 프로퍼티 값이 변하면 어트리뷰트 값도 변합니다.
<input type="password" placeholder="enterpw" id="pw" value="--">
<script>
const $pw = document.getElementById('pw');
$pw.placeholder = 'password'
console.log($pw.placeholder); //password
console.log($pw.getAttribute('placeholder')); //password
</script>
사용자 입력에 의해 상태 변화가 있는 DOM 프로퍼티만 최신 상태 값을 관리하다!라고 보면 되겠죠.
이 외 값은 어트리뷰트와 DOM 프로퍼티의 값이 항상 동일합니다.
+) 짧게 추가
HTML 어트리뷰트와 DOM 프로퍼티가 언제나 1:1로 대응하진 않습니다. 대부분 1:1이지만 없는 친구도 있고 1:N으로 대응하기도 합니다. 이름과 key가 반드시 같은 것도 아닙니다.
getAttribute 메서드로 취득한 어트리뷰트 값은 항상 문자열인데요. DOM 프로퍼티로 취득한 최신 상태값은 문자열이 아닐 수도 있습니다. ex) checked 프로퍼티 값은 불리언 타입
data 어트리뷰트와 dataset 프로퍼티 이게 뭐..요..
얘네를 사용하면 HTML요소에 정의한 어트리뷰트와 자바스크립트 간에 데이터를 교환할 수 있다고 합니다.
자기들만의 돌림자가 있어서 알아보기도 쉬운데요.
data 어트리뷰트는 data- 접두사 다음에 임의의 이름을 붙입니다.
data 어트리뷰트 값을 dataset프로퍼티로 취득할 수 있습니다.
얘가 data 어트리뷰트 정보를 가진 DOMStringMap객체를 반환하거든요.
<input data-user-pw="1234" data-role="admin" type="password" id="pw">
<script>
const $pw = document.getElementById('pw');
console.log($pw.dataset.role); //admin
console.log($pw.dataset.userPw); //1234
</script>
user-pw라고 되어있는데 왜 userPw로 썼나요? 버그인가요? 아닙니다.
DOMStringMap객체가 임의의 이름을 카멜 케이스로 변환하여 가지기 때문에 저렇게 써야 합니다.
이 프로퍼티를 써서 data 어트리뷰트 값을 변경할 수도 있습니다.
$pw.dataset.role = 'member'
console.log($pw.dataset.role); //member
평소에 그냥 객체에 프로퍼티를 추가하는 것처럼 dataset 프로퍼티를 사용하여 HTML 요소에 data 어트리뷰트를 추가할 수 있습니다.
data- 접두사 다음에 존재하지 않는 이름을 키로 사용하여 값을 할당하면 됩니다.
카멜케이스로 추가한 프로퍼티의 키는 케밥케이스로 자동 변경되어 추가됩니다.
스타일이 나왔다? 진짜 끝이다.
훈화말씀 아니고 진짜 끝입니다. 스타일은 빠르게 끝냅시다.
HTMLElement.prototype.style 프로터티로 인라인 스타일을 취득하거나 추가 변경이 가능합니다.
$pw.style.color = "red";
단위 지정이 필요한 값은 반드시 단위 지정을 해줘야 합니다.
클래스 조작도 당연히 해야지
미리 class를 정의한 다음 class 어트리뷰트 값을 변경하여 스타일 변경이 가능합니다. 아주 유용합니다.
class어트리뷰트에 대응하는 DOM 프로퍼티는 className과 classList입니다.
왜냐? class가 js에서 예약어거든요.
- Element.prototype.className : HTML 요소의 class 어트리뷰트 값을 취득하거나 변경합니다.
문자열을 반환하기 때문에 공백으로 구분된 여러 개의 클래스를 반환하는 경우 불편합니다. - Element.prototype.classList : class 어트리뷰트 정보를 담은 DOMTokenList객체를 반환합니다.
컬렉션 객체로서 유사 배열 객체이며 이터러블입니다. 그리고 아주 유용한 메서드들을 제공합니다.- add(... className) 인수로 전달한 1개 이상의 문자열을 class 어트리뷰트 값으로 추가합니다.
- remove(...className) 인수로 전달한 1개 이상의 문자열과 일치하는 클래스를 삭제합니다.
일치하는 클래스가 없으면 아무 일도 일어나지 않습니다. - item(index) 인수로 전달한 index에 해당하는 클래스를 반환합니다.
- contains(className) 인수와 일치하는 클래스가 포함되어 있는지 확인합니다.
- replace(oldClassName, newClassName) 첫 번째 인수의 값을 두 번째 인수 값으로 변경합니다.
- toggle(className[. force]) 일치하는 클래스가 존재하면 제거하고 존재하지 않으면 추가합니다.
두 번째 인수로 조건식을 전달할 수 있습니다. 조건식 평가 결과가 true면 강제로 추가하고,
false면 강제로 제거합니다.
이것 말고도 더 있습니다.
Style 프로퍼티의 한계
style 프로퍼티는 인라인 스타일만 반환합니다.
클래스를 적용한 스타일이나 상속을 통해 암묵적으로 적용된 스타일을 참조는 style 프로퍼티로는 불가능합니다.
HTML 요소에 적용된 모든 CSS 스타일을 참조하려면 getComputedStyle 메서드를 사용합니다.
getComputedStyle(element[, pseudo]) 두 번째 인자로 의사 요소를 지정하는 문자열을 지정할 수도 있습니다.
(예제보기)
<style>
body {
color: red;
}
input {
width: 100px;
height: 100px;
font-size: 10px;
}
</style>
</head>
<body>
<input data-user-pw="1234" data-role="admin" type="password" id="pw">
<script>
const $pw = document.getElementById('pw');
const computedStyle = getComputedStyle($pw);
console.log(computedStyle);//CSSStyleDeclaration
console.log(computedStyle.width); //100px
console.log(computedStyle.fontSize); //10px
</script>
이렇게 기나긴 DOM의 여정이 끝났습니다. 원래 오늘 다른 것도 정리해야 하는데 DOM이 제 시간을 다 가져갔습니다.
그래도 정리하면서 이해하니까 좋네요.
MDN & 모던 자바스크립트 Deep Dive 내용을 참고하였습니다.
'내가 해냄 > JS' 카테고리의 다른 글
프로토타입 내가 해냄 (0) | 2023.03.15 |
---|---|
배열 고차 함수 내가 해냄 (0) | 2023.03.14 |
ES6 함수 내가 해냄 (0) | 2023.03.14 |
클래스 내가 해냄 (0) | 2023.03.08 |
this 내가 해냄 (0) | 2023.03.06 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!