39장 DOM

이로그·2024년 3월 12일
0

39장 DOM

  • DOM : HTML 문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API, 즉 프로퍼티와 메서드를 제공하는 트리 자료구조다.

39.1 노드

39.1.1 HTML 요소와 노드 객체

  • HTML 요소는 HTML 문서를 구성하는 개별적인 요소를 의미한다.
  • HTML 요소는 렌더링 엔진에 의해 파싱되어 DOM을 구성하는 요소 노드 객체로 변환된다. 이때 HTML 요소의 어트리뷰트(class, id, ...)는 어트리뷰트 노드로, HTML 요소의 텍스트 콘텐츠는 텍스트 노드로 변환된다.
  • HTML 요소는 중첩 관계를 갖기 때문에, 콘텐츠 영역에는 다른 HTML 요소도 포함할 수 있다.
  • 이때 HTML 요소 간에는 중첩 관계에 의해 트리 자료 구조로 구성된다.
  • 노드 객체들로 구성된 트리 자료구조를 DOM이라 한다. DOM 트리라 부르기도 한다.

39.1.2 노드 객체의 타입

  • 노드 객체는 총 12개의 종류가 있고, 이 중에서 중요한 노트 타입은 4가지다.
    • 문서 노드 : DOM 트리의 최상위에 존재하는 루트 노드로서 document 객체를 가리킨다. 즉, HTML 문서당 document 객체는 유일하다. DOM 트리의 노드들에게 접근하기 위한 진입점 역할을 담당한다.
    • 요소 노드 : HTML 요소를 가리키는 객체다. 문서의 구조를 표현한다.
    • 어트리뷰트 노드 : HTML 요소의 어트리뷰트를 가리키는 객체다. 어트리뷰트가 지정된 HTML 요소의 요소 노드와 형제 관계를 갖는다.
    • 텍스트 노드 : HTML 요소의 텍스트를 가리키는 객체다. 문서의 정보를 표현한다. 자식 노드를 가질 수 없는 리프 노드다. DOM 트리의 최종단이다.

39.1.3 노드 객체의 상속 구조

  • 모든 노드 객체는 Object, EventTarget, Node 인터페이스를 상속 받는다.
  • 문서 노드는 Document, HTMLDocument 인터페이스를 상속 받는다.
  • 어트리뷰트 노드는 Attr 인터페이스를 상속 받는다.
  • 텍스트 노드는 CharacterData 인터페이스를 각각 상속 받는다.
  • 요소 노드는 Elment 인터페이스를 상속 받는다.
  • 노드 객체는 공통된 기능일수록 프토토타입 체인의 상위에, 개별적인 고유 기능일수록 프로토타입 체인의 하위에 프로토타입 체인을 구축하여 노드 객체에 필요한 기능, 즉 프로퍼티와 메서드를 제공하는 상속 구조를 갖는다.
  • DOM은 HTML 문서의 계층적 구조와 정보를 표현하는 것은 물론 노드 객체의 종류, 즉 노드 타입에 따라 필요한 기능을 프로퍼티와 메서드의 집합인 DOM API로 제공한다. 이 DOM API를 통해 HTML의 구조나 내용 또는 스타일 등을 동적으로 저장할 수 있다.

39.2 요소 노드 취득

  • HTML의 구조나 내용 또는 스타일 등을 동적으로 조작하려면 먼저 요소 노드를 취득해야 한다.

39.2.1 id를 이용하여 요소 노드 취득

  • Document.prototype.getElementById 메서드는 인수로 전달한 id 어트리뷰트 값을 갖는 하나의 요소 노드를 탐색하여 반환한다.
  • id 값은 HTML 문서 내에서 유일한 값이어야 하며, class 어트리뷰트와는 달리 공백 문자로 구분하여 여러 개의 값을 가질 수 없다.
  • 만약 HTML 문서 내에 동일한 id 값이 여러개가 존재할 경우, 가장 첫번째 요소 노드만 반환한다.
  • HTML 요소에 id 어트리뷰트를 부여하면 id 값과 동일한 이름의 전역 변수가 암묵적으로 선언되고 해당 노드 객체가 할당되는 부수 효과가 있다.
  • 단 id 값과 동일한 이름의 전역 변수가 이미 선언되어 있으면 이 전역 변수에 노드 객체가 재할당 되지 않는다.
<ul>
    <li id="banana">Apple</li>
    <li id="banana">Banana</li>
    <li id="banana">Orange</li>
</ul>
<div id="foo">foo</div>
<script>
    const $elem = document.getElementById('banana');
    $elem.style.color = 'red'; // Apple 텍스트를 갖고 있는 요소 노드만 color 변경

    const $elem2 = document.getElementById('nonono'); // 요소 없음 -> null 반환

    // id 값과 동일한 전역 변수
    console.log(foo === document.getElementById('foo')); // true

    let foo = 1; // id 값과 동일한 전역 변수에 값 추가 -> 노드 객체 재할당x
    console.log(foo); // 1
</script>

39.2.2 태그 이름을 이용한 요소 노드 취득

  • Document.prototype/Element.prototype.getElementsByTagName 메서드는 인수로 전달한 태그 이름을 갖는 모든 요소 노드들을 탐색하여 반환한다.
  • 여러 개의 요소 노드 객체를 갖는 HTMLCollection 객체를 반환한다.
  • 모든 요소 노드를 취득하려면 getElementsByTagName 메서드의 인수로 '*'를 전달한다.
  • Element.prototype.getElementsByTagName 메서드는 특정 요소 노드를 통해 호출하여 자손 노드 중에서 요소 노드를 탐색하여 반환한다.
  • 존재 하지 않는 경우 빈 HTMLCollection 객체를 반환한다.
<ul id="fruits">
    <li id="apple">Apple</li>
    <li id="banana">Banana</li>
    <li id="orange">Orange</li>
</ul>
<script>
    // 태그 이름이 li인 요소 노드를 모두 탐색하여 color를 red로 설정
    const $elems = document.getElementsByTagName('li');
    [...$elems].forEach(elem => {elem.style.color = 'red';});

    // 문서 내의 모든 요소 노드 취득
    const $all = document.getElementsByTagName('*');

    // Element.prototype.getElementsByTagName
    const $fruits = document.getElementById('fruits');
    const $listFromFruits = $fruits.getElementsByTagName('li');
    console.log($listFromFruits); // HTMLCollection(3) [li, li, li]
</script>

39.2.3 class를 이용한 요소 노드 취득

  • Document.prototype/Element.prototype.getElementsByClassName 메서드는 인수로 전달한 class 어트리뷰트 값을 갖는 모든 요소 노드들을 탐색하여 반환한다.
  • 인수로 전달할 class 값은 공백으로 구분하여 여러 개의 class를 지정할 수 있다.
  • 여러개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 HTMLCollection 객체를 반환한다.
// fruit 클래스를 가진 모든 노드 요소를 탐색
document.getElementsByClassName('fruit');
// fruit, apple 클래스를 가진 모든 노드 요소를 탐색
document.getElementsByClassName('fruit apple');
// fuits 요소의 apple 클래스를 가지는 자손 노드 탐색
const $fruits = document.getElementById('fruits');
const $listFromFruits = $fruits.getElementsByClassName('apple');
// 요소가 존재하지 않으면 빈 객체 반환
document.getElementsByClassName('nonono'); // []

39.2.4 CSS 선택자를 이용한 요소 노드 취득

  • CSS 선택자 : 스타일을 적용하고자 하는 HTML 요소를 특정할때 사용하는 문법
/* 전체 선택자 : 모든 요소를 선택 */
* { ... }
/* 태그 선택자 : 모든 p 태그 요소를 모두 선택 */
p { ... }
/* id 선택자 : id 값이 foo인 요소를 모두 선택 */
#foo { ... }
/* class 선택자 : class 값이 foo인 요소를 모두 선택 */
.foo { ... }
/* 어트리뷰트 선택자 : input 요소 중에 type 어트리뷰트 값이 'text'인 요소를 모두 선택 */
input[type=text] { ... }
/* 후손 선택자 : div 요소의 후손 요소 중 p 요소를 모두 선택 */
div p { ... }
/* 자식 선택자 : div 요소의 자식 요소 중 p 요소를 모두 선택 */
div > p { ... }
/* 인접 형제 선택자 : p 요소의 형제 요소 중에 p 요소 바로 뒤에 위치하는 ul 요소를 선택 */
p + ul { ... }
/* 일반 형제 선택자 : p 요소의 형제 요소 중에 p 요소 뒤에 위치하는 ul 요소를 모두 선택 */
p ~ ul { ... }
/* 가상 클래스 선택자 : hover 상태인 a 요소를 모두 선택 */
a:hover { ... }
/* 가상 요소 선택자 : p 요소의 콘텐츠 앞에 위치하는 공간을 선택
    일반적으로 content 프로퍼티와 함께 사용된다. */
p::before { content: ''; }
  • Document.prototype/Element.prototype.querySelector 메서드는 인수로 전달한 css 선택자를 만족시키는 하나의 요소 노드를 탐색하여 반환한다.
    • 해당 노드가 여러 개인 경우 첫 번째 요소 노드만 반환한다.
    • 해당 노드가 존재하지 않는 경우 null을 반환한다.
  • Document.prototype/Element.prototype.querySelectorAll 메서드는 인수로 전달한 css 선택자를 만족시키는 모든 요소 노드를 탐색하여 NodeList 객체를 반환한다. NodeList 객체는 유사 배열 객체이면서 이터러블이다.
    • 해당 요소가 존재하지 않는 경우 빈 NodeList 객체를 반환한다.
// banana 클래스를 가지는 요소 중 첫번째 요소 반환
document.querySelector('.banana');
// ul > li 에 해당하는 요소 모두 반환
document.querySelectorAll('ul > li');

39.2.5 특정 요소 노드를 취득할 수 있는지 확인

  • Element.prototype.matches 메서드는 인수로 전달한 CSS 선택자를 통해 특정 요소 노드를 취득할 수 있는지 확인한다.
  • 이벤트 위침을 사용할 때 유용하다.
<ul id="fruits">
    <li class="apple">Apple</li>
    <li class="banana">Banana</li>
    <li class="orange">Orange</li>
</ul>

const $apple = document.querySelector('.apple');
console.log($apple.matches('#fruits > li.apple')); // true
console.log($apple.matches('#fruits > li.banana')); // false

39.2.6 HTMLCollection과 NodeList

  • DOM API가 여러 개의 결과 값을 반환하기 위한 DOM 컬렉션 객체다.
  • HTMLCollection과 NodeList는 모두 유사 배열 객체이면서 이터러블이다.
  • for...of문으로 순회 할수 있고, 스프레드 문법을 사용하여 배열로 변환할 수 있다.
  • 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는 객체이다.
  • 단, NodeList는 대부분의 경우 과거의 정적 상태를 유지하는 non-live 객체로 동작하고, 경우에 따라 live 객체로 동작한다.

HTMLCollection

  • getElementsByTagName, getElementsByClassName 메서드가 반환하는 HTMLCollection 객체는 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는 DOM 컬렉션 객체다.
<ul id="fruits">
    <li class="red">Apple</li>
    <li class="red">Banana</li>
    <li class="red">Orange</li>
</ul>

const $elems = document.getElementsByClassName('red');
// red -> blue 클래스로 변경
for (let i = 0; i < $elems.length; i++) {
    $elems[i].className = 'blue';
}
// 이때 이 모든 red 요소가 blue 요소로 바뀌지 않는다.
// 이유는, HTMLCollection이 살아 있는 객체 이기 때문에, $elems에서 실시간으로 삭제 되기 때문에 두번째 요소가 첫번째 요소로 바뀌게 된다.
// for문을 역순으로 순회하는 방법으로 회피할 수도 있고, while문을 사용하여 무한 반복하는 방법으로 회피할 수 있다.
// 더 간단한 해결책은 HTMLCollection 객체를 사용하지 않도록, HTMLCollection 객체를 배열로 변환하고 배열의 고차함수를 사용하면 쉽게 해결할 수 있다.
[...$elems].forEach(elem => elem.className = 'blue');

NodeList

  • querySelectorAll 메서드는 DOM 컬렉션 객체인 NodeList 객체를 반환한다. 이때 NodeList 객체는 실시간으로 노드 객체의 상태 변경을 반영하지 않는 객체다.
  • NodeList 객체는 forEach, item, entries, keys, values 메서드를 사용할 수 있다.
  • 하지만, childNodes 프로퍼티가 반환하는 NodeList 객체는 HTMLCollection 객체와 같이 실시간으로 노드 객체의 상태 변경을 반영하는 live 객체로 동작하므로 주의가 필요하다.
  • 예상과 다르게 동작할 수 있기 때문에, HTMLCollection 이나 NodeList 객체를 배열로 변환하여 사용하는 것을 권장한다.
  • NodeList 또한 유사 배열 객체이면서 이터러블이기 때문에 스프레드 연산자를 이용하면 간단하게 배열로 변환할 수 있다.
const $fruits = document.getElementsById('fruits');
const { childNodes } = $fruits;
[...childNodes].forEach(childNode => {
    $fruits.removeChild(childNode);
});
// 모든 자식 노드 삭제 완료.
console.log(chlidNode); // NodeList []

39.3 노드 탐색

  • Node.prototype 제공 : parentNode, previousSibling, firstChild, childNodes 프로퍼티
  • Elment.prototype 제공 : 프로퍼티키에 Element가 포함된 previousElementSibling, nextElementSibling과 children 프로퍼티
  • 노드 탐색 프로퍼티는 모두 접근자 프로퍼티이고, getter만 존재하는 읽기 전용 접근자 프로퍼티다.

39.3.1 공백 텍스트 노드

  • 스페이스, 탭, 줄바꿈 등의 공백 문자는 텍스트 노드를 생성한다.
  • 노드를 탐색할 때 공백 문자가 생성한 공백 텍스트 노드에 주의해야 한다.

39.3.2 자식 노드 탐색

  • Node.prototype.childNodes
    • 자식 노드를 모두 탐색하여 NodeList에 담아 반환한다.
    • childNodes 프로퍼티가 반환한 NodeList에는 요소노드뿐만 아니라 텍스트 노드도 포함되어 있을 수 있다.
  • Element.prototype.children
    • 자식 노드 중에서 요소 노드만 모두 탐색하여 HTMLCollection에 담아 반환한다.
    • children 프로퍼티가 반환한 HTMLCollection에는 텍스트 노드가 포함되지 않는다.
  • Node.prototype.firstChild
    • 첫번째 자식 노드를 반환한다. 텍스트 노드이거나 요소 노드다.
  • Node.prototype.lastChild
    • 마지막 자식 노드를 반환한다. 텍스트 노드이거나 요소 노드다.
  • Element.prototype.firstElementChild
    • 첫번째 자식 요소 노드를 반환한다. 요소 노드만 반환한다.
  • Element.prototype.lastElementChild
    • 마지막 자식 요소 노드를 반환한다. 요소 노드만 반환한다.
<ul id="fruits">
    <li class="apple">Apple</li>
    <li class="banana">Banana</li>
    <li class="orange">Orange</li>
</ul>

const $fruits = document.getElementById('fruits');
console.log($fruits.childNodes); // [text, li.apple, text, li.banana, text, li.orange, text]
console.log($fruits.children); // [li.apple, li.banana, li.orange]
console.log($fruits.firstChild); // #text
console.log($fruits.lastChild); // #text
console.log($fruits.firstElementChild); // li.apple
console.log($fruits.lastElementChild); // li.orange

39.3.3 자식 노드 존재 확인

  • Node.prototype.hasChildNodes 메서드를 사용하면 자식 노드가 존재하는지 확인할 수 있다.
  • 존재하면 true, 존재하지 않으면 false를 반환한다.
  • 텍스트 노드를 포함하여 자식 노드의 존재를 확인한다.
  • 요소 노드가 존재하는지 확인하려면 children.length 또는 Elment 인터페이스의 childElementCount 프로퍼티를 사용한다.
<ul id="fruits">
</ul>
const $fruits = document.getElementById('fruits');
console.log($fruits.hasChildNodes); // true

// 자식 노드 중에 텍스트 노드가 아닌 요소 노드가 존재하는지 확인한다.
console.log(!!$fruits.children.length); // 0 false
console.log(!!$fruits.children.length); // 0 false

39.3.4 요소 노드의 텍스트 노드 탐색

  • firstChild 프로퍼티로 텍스트 노드에 접근할 수 있다.
  • firstChild 프로퍼티는 첫번째 자식 노드를 반환한다.
<div id="foo">Hello</div>
console.log(document.getElementById('foo').firstChild); // #text

39.3.5 부모 노드 탐색

  • Node.prototype.parentNode 프로퍼티를 사용하면 부모 노드를 탐색할 수 있다.
  • 부모 노드가 텍스트 노드인 경우는 없다.
<ul id="fruits">
    <li class="apple">Apple</li>
    <li class="banana">Banana</li>
    <li class="orange">Orange</li>
</ul>

const $banana = document.querySelector('.banana');
console.log($banana.parentNode); // ul#fruits

39.3.6 형제 노드 탐색

  • Node.prototype.previousSibling
    • 부모 노드가 같은 형제 노드 중에서 자신의 이전 형제 노드를 탐색하여 반환한다.
    • 요소 노드뿐만 아니라 텍스트노드 까지 모두 탐색한다.
  • Node.prototype.nextSibling
    • 부모 노드가 같은 형제 노드 중에서 자신의 다음 형제 노드를 탐색하여 반환한다.
    • 요소 노드 뿐만 아니라 텍스트노드 까지 모두 탐색한다.
  • Elment.prototype.previousElementSibling
    • 부모 노드가 같은 형제 요소 노드 중에서 자신의 이전 형제 요소 노드를 탐색하여 반환한다.
    • 요소 노드만 반환한다
  • Elment.prototype.nextElementSibling
    • 부모 노드가 같은 형제 요소 노드 중에서 자신의 다음 형제 요소 노드를 탐색하여 반환한다.
    • 요소 노드만 반환한다.
<ul id="fruits">
    <li class="apple">Apple</li>
    <li class="banana">Banana</li>
    <li class="orange">Orange</li>
</ul>

const $fruits = document.getElemetById('fruits');

// fruits의 첫번째 자식 노드 탐색
const { firstChild } = $fruits;
console.log(firstChild); // #text

// fruits의 첫번째 자식 노드의 다음 형재 노드 탐색
const { nextSibling } = firstChild;
console.log(nextSiblings); // li.apple

// li.apple 요소의 이전 형제 노드 탐색
const { previousSibling } = nextSiblings;
console.log(previousSibling); // #text

// $fruits 요소의 첫번째 자식 요소 노드 탐색
const { firstElementChild } = $fruits;
console.log(firstElementChild); // li.apple

// #fruits 요소의 첫번째 자식 노드 다음 형제 노드 탐색
const { nextElementSibling } = firstElementChild;
console.log(nextElementSibling); // li.banana

// li.banana 요소의 이전 형제 요소 노드를 탐색
const { previousElementSibling } = nextElementSibling;
console.log(previousElementSibling); // li.apple

39.4 노드 정보 취득

  • Node.prototype.nodeType
    • 노드 객체의 종류, 즉 노드 타입을 나타내는 상수를 반환한다.
      • Node.ELEMENT_NODE : 요소 노드 타입 상수 1 반환
      • Node.TEXT_NODE : 텍스트 노드 타입 상수 3 반환
      • Node.DOCUMENT_NODE : 문서 노드 타입을 나타내는 9 반환
  • Node.prototype.nodeName
    • 노드의 이름을 문자열로 반환한다.
      • 요소 노드 : 대문자 문자열로 태그이름 반환
      • 텍스트 노드 : 문자열 '#text' 반환
      • 문서 노드 : 문자열 '#document' 반환
<div id="foo">Hello</div>

console.log(document.nodeType); // 9
console.log(document.nodeName); // #document

const $foo = document.getElementById('foo');
console.log($foo.nodeType); // 1
console.log($foo.nodeName); // DIV

const $textNode = $foo.firstChild;
console.log($textNode.nodeType); // 3
console.log($textNode.nodeName); // #text

39.5 요소 노드의 텍스트 조작

39.5.1 nodeValue

  • Node.prototype.nodeValue 프로퍼티는 setter 와 getter 모두 존재하는 접근자 프로퍼티다. 따라서 참조와 할당 모두 가능하다.
  • nodeValue 프로퍼티를 참조하면, 노드 객체의 값(텍스트 노드의 텍스트)을 반환한다.
  • 문서 노드나 요소 노드의 nodeValue 프로퍼티를 참조하면 null을 반환한다.
<div id="foo">Hello</div>

// 문서 노드의 nodeValue 참조
console.log(document.nodeValue); // null

// 요소 노드의 nodeValue 참조
const $foo = document.getElementById('foo');
console.log($foo.nodeValue); // null

// 텍스트 노드의 nodeValue 참조
const $textNode = $foo.firstChild;
console.log($textNode.nodeValue); // Hello

// 텍스트 노드의 nodeValue 할당
$textNode.nodeValue = 'World';
console.log($textNode.nodeValue); // World

39.5.2 textContent

  • Node.prototype.textContent 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티로서 요소 노드의 텍스트와 모든 자손 노드의 텍스트를 모두 취득하거나 변경한다.
  • 요소 노드의 textContent 프로퍼티에 문자열을 할당하면 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가된다. 이때 문자열에 HTML 마크업이 포함되어 있더라도 문자열 그대로 인식되어 텍스트로 취급된다. HTML 마크업이 파싱되지 않는다.
<div id="foo">Hello <span>world!</span></div>

// 요소 노드의 참조
console.log(document.getElementById('foo').textContent); // Hello world;

// 요소 노드의 할당
document.getElementById('foo').textContent = 'Hi <span>there!</span>';
  • textContent와 유사한 동작을 하는 innerText 프로퍼티가 있지만, css를 고려해야 하며 css에 의해 비표시로 지정된 요소 노드의 텍스트를 반환하지 않기 때문에 사용하지 않는 것이 좋다.

39.6 DOM 조작

  • DOM 조작 : 새로운 노드를 생성하여 DOM에 추가하거나 기존 노드를 삭제 또는 교체하는 것.
  • DOM 조작에 의해 DOM에 새로운 노드가 추가되거나 삭제되면 리플로우와 리페인트가 발생하는 원인이 되므로 성능에 영향을 준다.

39.6.1 innerHTML

  • Element.prototype.innerHTML 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티로서 요소 노드의 HTML 마크업을 취득하거나 변경한다.
  • 참조시 요소 노드의 콘텐츠 영역 내에 포함된 모든 HTML 마크업을 문자열로 변환한다.
  • 할당시 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열에 포함되어 있는 HTML 마크업이 파싱되어 요소 노드의 자식 노드로 DOM에 반영된다.
  • 단, innerHTML 프로퍼티로 DOM 조작시 사용자로부터 입력받은 데이터를 그대로 할당하는 것은 크로스 사이트 스크립팅 공격에 취약하므로 위험하다.
  • HTML5는 innerHTML 프로퍼티로 삽입된 script 요소 내의 자바스크립트 코드를 실행하지 않지만, 노드요소(태그)에 걸어둔 이벤트는 작동한다.
<div id="foo">Hello <span>world!</span></div>


// 참조시
console.log(document.getElementById('foo').innerHTML); // 'Hello <span>world!</span>

const $foo = document.getElementById('foo');
// 노드 교체
$foo.innerHTML = 'Hi <span>there!</span>';

// 노드 추가
$foo.innerHTML += 'Hi~~ <span>there222!</span>';
// 위의 코드와 동일하다. $foo.innerHTML = $foo.innerHTML + 'Hi~~ <span>there222!</span>';

// 노드 삭제
$foo.innerHTMl = '';

39.6.2 insertAdjacentHTML 메서드

  • Element.prototype.insertAdjacentHTML(position, DOMString) 메서드는 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입한다.
  • insertAdjacentHTML 메서드는 두 번째 인수로 전달한 HTML 마크업 문자열(DOMString)을 파싱하고, 그 결과로 생성된 노드를 첫번째 인수로 전달한 위치(position)에 삽입하여 DOM에 반영한다.
  • 첫번째 인수로 전달할 수 있는 문자열은 'beforebegin', 'afterbegin', 'beforeend', 'afterend' 4가지다.

'beforebegin'<div id="foo">'afterbegin' text 'beforeend'</div>'afterend'

  • insertAdjacentHTML 메서드는 새롭게 삽입될 요소만을 파싱하여 자식 요소로 추가하므로 inner 프로퍼티보다 효율적이고 빠르다.
  • 단, innerHTML 프로퍼티와 마찬가지로 크로스 사이트 스크립팅 공격에 취약하다는 점은 동일하다.
<!-- beforebegin -->
<div id="foo">
  <!-- afterbegin -->
  text
  <!-- beforeend -->
</div>
<!-- afterend -->

<script>
const $foo = document.getElementById('foo');
$foo.insertAdjacentHTML('beforebegin', '<p>beforebegin</p>');
$foo.insertAdjacentHTML('afterbegin', '<p>afterbegin</p>');
$foo.insertAdjacentHTML('beforeend', '<p>beforeend</p>');
$foo.insertAdjacentHTML('afterend', '<p>afterend</p>');
</script>

39.6.3 노드 생성과 추가

요소 노드 생성

  • Document.prototype.createElement(tagName) 메서드는 요소 노드를 생성하여 반환한다.
  • DOM에 요소 노드를 생성할 뿐, 추가하지는 않는다.
  • createElement 메서드로 생성한 요소 노드는 아무런 자식 노드를 가지고 있지 않는다.

텍스트 노드 생성

  • Document.prototype.createTextNode(text) 메서드는 텍스트 노드를 생성하여 반환한다.
  • DOM에 요소 노드를 생성할 뿐, 추가하지는 않는다.

텍스트 노드를 요소 노드의 자식 노드로 추가

  • Node.prototype.appendChild(childNode) 메서드는 매개변수 childNode에게 인수로 전달한 노드를 appendChild 메서드를 호출한 노드의 마지막 자식 노드로 추가한다.
  • createTextNode 메서드로 생성한 텍스트 노드를 전달하면 텍스트 노드가 추가 된다.
  • 자식 노드가 없는 경우 textContent 프로퍼티를 사용하는 편이 더욱 간편하다.

요소 노드를 DOM에 추가

  • Node.prototype.appendChild 메서드를 사용하여 텍스트 노드와 부자 관계로 연결한 요소 노드를 타겟 요소 노드의 마지막 자식 요소로 추가한다.
// 요소 노드 생성
const $li = document.createElement('li');

// 텍스트 노드 생성
const textNode = document.createTextNode('Banana');

// 텍스트 노드를 요소 노드의 자식 노드로 추가
$li.appendChild(textNode);
// $li.textContent = 'Banana'; // 위의 코드와 동일하다.

// 요소 노드 추가
$fruits.appendChild($li);
  • 이때 리플로우와 리페인트가 실행된다.

39.6.4 복수의 노드 생성과 추가

  • 복수의 노드를 추가할때 개수마다 추가를 하면, 리플로우 리페인트가 추가할 노드의 개수만큼 반복되기 때문에 성능적으로 좋지 않다.
  • 이럴땐 추가는 단 한번만 진행하도록 작업하는 것이 가장 좋은 방법이다.
  • 컨테이너 요소는 createDocumentFragment 를 사용 하는 것이 더 효율적이다.
<ul id="fruits"></ul>

<script>
const $fruits = document.getElementById('fruits');

// 컨테이너 요소 노드 생성
const $container = document.createElement('div');
// div 컨테이터 태그가 불필요할 경우(ul 안에 들어가는 경우 등등)
const $fragment = document.createDocumentFragment();

['Apple', 'Banana', 'Orange'].forEach(text => {
  // 요소 노드 생성
  const $li = document.createElement('li');
  
  // 텍스트 노드 생성
  const textNode = document.createTextNode(text);
  
  // 텍스트 노드를 li 요소 노드의 자식 노드로 추가
  $li.appendChild(textNode);

  // li 요소 노드를 컨테이너 요소의 마지막 자식 노드로 추가
  $container.appendChild($li);
  // 혹은 프래그먼트 요소의 마지막 자식 노드로 추가
  $fragment.appendChild($li);
});

// 컨테이너 요소 노드를 타겟 요소 노드의 마지막 자식 노드로 추가
$fruits.appendChild($container);
// 혹은 프래그먼트 요소 노드를 타겟 요소 노드의 마지막 자식 노드로 추가
$fruits.appendChild($fragment);

</script>

39.6.5 노드 삽입

마지막 노드로 추가

  • Node.prototype.appendChild 메서드는 인수로 전달받은 노드를 자신을 호출한 노드의 마지막 자식 노드로 DOM에 추가한다.
const $li = document.createElement('li');
$li.appendChild(document.createTextNode('Orange'));
// fruits 요소 노드의 마지막에 li 추가
document.getElementById('fruits').appendChild($li);

지정한 위치에 노드 삽입

  • Node.prototype.insertBefore(newNode, childNode) 메서드는 첫 번째 인수로 전달받은 노드를 두 번째 인수로 전달받은 노드 앞에 삽입한다.
  • 두 번째 인수로 전달받은 노드는 반드시 insertBefore 메서드를 호출한 노드의 자식 노드이어야 한다.
  • 만약, 두 번쨰 인수가 null 이면 appendChild 메서드와 같이 동작한다.
const $fruits = document.getElementById('fruits');
const $li = document.createElement('li');
$li.appendChild(document.createTextNode('Orange'));
// fruis 요소 노드의 마지막 자식 요소 앞에 li 삽입
$fruits.insertBefore($li, $fruits.lastElementChild);

39.6.6 노드 이동

  • DOM에 이미 존재하는 노드를 appendChild 나 insertBefore 메서드를 사용하여 DOM에 다시 추가하면 현재 위치에서 노드를 제거하고 새로운 위치에 노드를 추가한다. 즉, 노드가 이동된다.
<ul>
  <li>Apple</li>
  <li>Banana</li>
  <li>Orange</li>
</ul>
const $fruits = document.getElementById('fruits');
const [$apple, $banana, ] = $fruits.children;
$fruits.appendChild($apple); // Banana - Orange - Apple
$fruits.insertBefore($banana, $fruits.lastElementChild); // Orange - Banana - Apple

39.6.7 노드 복사

  • Node.prototype.cloneNode([deep: true | false]) 메서드는 노드의 사본을 생성하여 반환한다.
  • 인수로 true 전달시, 모든 자손 노드가 포함된 사본 생성 되는 깊은 복사
  • 인수로 false 전달하거나 생략시, 노드 자신만의 사본을 생성하는 얕은 복사 (텍스트 노드도 제외)
const $fruits = document.getElementById('fruits');
const $apple = $fruits.firstElementChild;
// 얕은 복사
const $shallowClone = $apple.cloneNode();
// 사본 요소 노드에 텍스트 추가
$shallowClone.textContent = 'Banana';
// #fruits 요소 노드의 마지막 노드로 추가
$fruits.appendChild($shallowClone);

// 깊은복사
const $deepClone = $fruits.clondNode(true);
// $fruits 요소 노드의 마지막 노드로 추가
$fruits.appendCHild($deepClone);

39.6.8 노드 교체

  • Node.prototype.replaceChild(newChild, oldChild) 메서드는 자신을 호출한 노드의 자식 노드를 다른 노드로 교체한다.
  • 이때 oldChild 노드는 DOM에서 제거된다.
const $fruits = document.getElementById('fruits');

const $newChild = document.createElement('li');
$newChild.textContent = 'Banana';

// #fruits 요소 노드의 첫번째 자식 요소 노드를 newChild 요소 노드로 교체
$fruits.replaceChild($newChild, $fruits.firstElementChild);

39.6.9 노드 삭제

  • Node.prototype.removeChild(child) 메서드는 child 매개변수에 인수로 전달한 노드를 DOM에서 삭제한다.
const $fruits = document.getElementById('fruits');
// #fruits 요소 노드의 마지막 요소를 DOM에서 삭제
$fruits.removeChild($fruits.lastElementChild);

39.7 어트리뷰트

39.7.1 어트리뷰트 노드와 attributes 프로퍼티

  • HTML 요소는 여러 개의 어트리뷰트를 가질 수 있다.
  • HTML 문서가 파싱될 때 HTMl 요소의 어트리뷰트는 어트리뷰트 노드로 변환되어 요소 노드의 형제 노드로 추가된다.
  • HTML 어트리뷰트당 하나의 어트리뷰트 노드가 생성된다.
  • 모든 어트리뷰트 노드의 참조는 유사 배열 객체이자 이터러블인 NamedNodeMap 객체에 담겨서 요소 노드의 attributes 프로퍼티에 저장된다.
  • 요소 노드의 모든 어트리뷰트 노드는 요소 노드의 Element.prototype.attributes 프로퍼티로 취득할 수 있다.
  • attributes 프로퍼티는 getter만 존재하는 읽기 전용 접근자 프로퍼티다.
<input id="user" type="text" value="ungmo2">
<script>
const { attributes } = document.getElementById('user');
console.log(attributes.id.value); // user
console.log(attributes.type.value); // text
console.log(attributes.value.value); // ungmo2
</script>

39.7.2 HTML 어트리뷰트 조작

  • 아래의 메서드들을 사용하면 attributes 프로퍼티를 사용하지 않고 직접 HTMl 어트리뷰트 를 조작할 수 있다.
  • Element.prototype.getAttribute(attributeName) : 어트리뷰트 값 참조
  • Element.prototype.setAttribute(attributeName, attributeValue) : 어트리뷰트 값 변경
  • Element.prototype.hasAttribute(attributeName) : 어트리뷰트 값 존재 확인
  • Element.prototype.removeAttribute(attributeName) : 어트리뷰트 삭제
<input id="user" type="text" value="ungmo2">

<script>
const $input = document.getElementById('user');
// attribute 속성 값 참조
const inputValue = $input.getAttribute('value');
console.log(inputValue); // ungmo2

// attribute 속성 값 변경
$input.setAttributes('value', 'foo');
console.log($input.getAttribute('value')); // foo

// attribute 값 존재 확인
console.log($input.hasAttribute('value')); // true

// attribute 삭제
$input.removeAttribute('value');
</script>

39.7.3 HTML 어트리뷰트 vs. DOM 프로퍼티

  • HTML 어트리뷰트는 DOM에서 중복 관리되고 있는 것처럼 보인다.
    • 요소 노드의 attributes 프로퍼티에서 관리하는 어트리뷰트 노드
    • HTMl 어트리뷰트에 대응하는 요소 노드의 프로퍼티(DOM 프로퍼티)
  • HTML 어트리뷰트의 역할은 HTML 요소의 초기 상태를 지정하는 것이다. 즉, HTML 어트리뷰트 값은 HTML 요소의 초기 상태를 의미하며 이는 변하지 않는다.
  • <input id="user" type="text" value="ungmo2">를 예로 들자면,
    • value 어트리뷰트는 어트리뷰트 노드로 변환되어 요소 노드의 attributes 프로퍼티에 저장된다.
    • value 어트리뷰트의 값은 요소 노드의 value 프로퍼티에 할당된다.
    • 따라서 첫 렌더링이 끝난 시점까지 어트리뷰트 노드의 어트리뷰트 값과 요소 노드의 value 프로퍼티에 할당된 값은 HTML 어트리뷰트 값과 동일하다.
  • 다만, 요소 노드는 상태를 가지고 있다. 사용자의 입력에 의해 변화하는것을 말한다.
    • 예를 들어, input은 사용자가 입력 필드에 입력한 값을 상태로 가지고 있으며, checkbox는 체크 여부를 상태로 가지고 있다.
  • input에 사용자가 foo 라는 값을 입력한 경우, input 요소는 최신 상태(foo)도 관리 해야하고, 초기 상태(ungmo2)도 관리해야 한다.
  • 요소 노드는 2개의 상태, 즉 초기 상태와 최신 상태를 관리해야 한다.
    • 요소 노드의 초기 상태는 어트리뷰트 노드가 관리하며,
    • 요소 노드의 최신 상태는 DOM 프로퍼티가 관리한다.

어트리뷰트 노드

  • HTML 어트리뷰트로 지정한 HTML 요소의 초기 상태는 어트리뷰트 노드에서 관리한다.
  • getAttribute 메서드를 사용하면 어트리뷰트 노드에서 관리하는 HTML 요소에 지정한 어트리뷰트 값(초기 상태 값)을 취득할 수 있다.
  • setAttribute 메서드를 사용하면 어트리뷰트 노드에서 관리하는 HTML 요소에 지정한 어트리뷰트 값(초기 상태 값)을 변경할 수 있다.
<input id="user" type="text" value="ungmo2">

// 초기값 취득
document.getElementById('user').getAttribute('value'); // ungmo2
// 초기값 foo로 변경
document.getElementById('user').setAttribute('value', 'foo');

DOM 프로퍼티

  • 사용자가 입력한 최신 상태는 HTML 어트리뷰트에 대응하는 요소 노드의 DOM 프로퍼티가 관리한다. DOM 프로퍼티는 사용자의 입력에 의한 상태 변화에 반응하여 언제나 최신 상태를 유지한다.
  • DOM 프로퍼티에 값을 할당하면, HTML 요소의 최신 상태 값을 변경할 수 있다.
const $input = document.getElementById('user');

// foo로 최신 상태 변경
$input.value = 'foo';
console.log($input.value); // foo

// HTML 어트리뷰트 값(초기 상태 값)에는 영향을 주지 않는다.
console.log($input.getAttribute('value')); // ungmo2
  • 단, id 어트리뷰트와 id 프로퍼티는 사용자 입력과 관계 없이 항상 동일한 값을 유지한다.
const $input = document.getElementById('user');
// id 값 foo로 변경
$input.id = 'foo';
console.log($input.id); // foo
console.log($input.getAttribute('id')); // foo

HTML 어트리뷰트와 DOM 프로퍼티의 대응 관계

  • id 어트리뷰트와 id 프로퍼티는 1:1 대응하며, 동일한 값으로 연동한다
  • input 요소의 value 어트리뷰트는 value 프로퍼티와 1:1 대응한다. 하지만 value 어트리뷰트 초기 상태를, value 프로퍼티는 최신 상태를 갖는다.
  • class 어트리뷰트는 className, classList 프로퍼티와 대응한다.
  • for 어트리뷰트는 htmlFor 프로퍼티와 1:1 대응한다.
  • td 요소의 colspan 어트리뷰트는 대응하는 프로퍼티가 존재하지 않는다.
  • textContent 프로퍼티는 대응하는 어트리뷰트가 존재하지 않는다.
  • 어트리뷰트 이름은 대소문자를 구별하지 않지만 대응하는 프로퍼티 키는 카멜케이스를 따른다.(maxlength -> maxLength)

DOM 프로퍼티 값의 타입

  • getAttribute 메서드로 취득한 어트리뷰트 값은 언제나 문자열이다.
  • DOM 프로퍼티로 취득한 최신 상태 값은 문자열이 아닐 수도 있다. 예를 들어 checkbox 요소의 checked 어트리뷰트 값은 문자열이지만 checked 프로퍼티 값은 불리언 타입이다.
<input type="checkbox" checked>

const $checkbox = document.querySelector('input[type="checkbox"]');

console.log($checkbox.getAttribute('checked')); // ''
console.log($checkbox.checked); // true

39.7.4 data 어트리뷰트와 dataset 프로퍼티

  • data어트리뷰트와 dataset 프로퍼티를 사용하면 HTML 요소에 정의한 사용자 정의 어트리뷰트와 자바스크립트 간에 데이터를 교환할 수 있다.
  • data 어트리뷰트는 data- 접두사 다음에 임의의 이름을 붙여 사용한다.
  • data 어트리뷰트의 값은 HTMLElement.dataset 프로퍼티로 취득할 수 있다. dataset 프로퍼티는 모든 data 어트리뷰트의 정보를 제공하는 DOMStringMap 객체를 반환한다.
  • DOMStringMap 객체는 data 어트리뷰트의 data- 접두사 다음에 붙인 임의의 이름을 카멜 케이스로 변환한 프로퍼티를 가지고 있다.
  • 이 프로퍼티로 data 어트리뷰트의 값을 취득하거나 변경할 수 있다.
  • 존재하지 않는 키를 사용하여 dataset 프로퍼티에 값을 할당하면 data 어트리뷰트가 추가된다.
<ul class="users">
  <li id="1" data-user-id="7621" data-role="admin">Lee</li>
  <li id="2" data-user-id="9524" data-role="subscriber">Kim</li>
</ul>

const users = [...document.querySelector('.user').children];

// user-id가 '7621'인 요소 노드를 취득
const user = users.find(user => user.dataset.userId === '7621');
console.log(user.dataset.role); // admin

// data-role 값 변경
user.dataset.role = 'subscriber';
console.log(user.dataset); // DOMStringMap {userId: '7621', role: 'subscriber'}

// data-test-name 추가
user.dataset.testName = 'test';
console.log(user.dataset); // DOMStringMap {userId: '7621', role: 'subscriber', testName: 'test'}
// <li id="1" data-user-id="7621" data-role="subscriber" data-test-name="test">Lee</li>

39.8 스타일

39.8.1 인라인 스타일 조작

  • HTMLElement.prototype.style 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티로서 요소 노드의 인라인 스타일을 취득하거나 추가 또는 변경한다.
  • CSS 프로퍼티는 케밥 케이스를 따른다. CSSStyleDeclaration 객체의 프로퍼티는 카멜 케이스를 따른다.
  • 단위 지정이 필요한 CSS 프로퍼티의 값은 반드시 단위를 지정해야 한다.
// 카멜 케이스
$div.style.backgroundColor = 'yellow';
// 케밥 케이스
$div.style['background-color'] = 'yellow';
// 단위 지정 필수
$div.style.width = '100px';

39.8.2 클래스 조작

className

  • Element.prototype.className 프로퍼티는 getter와 setter 모두 존재하는 접근자 프로퍼티로서 HTML 요소의 class 어트리뷰트 값을 취득하거나 변경한다.
  • getter 로 참조하면, class 값을 문자열로 반환한다
  • setter 로 문자열을 할당하면 class 어트리뷰트 값을 할당한 문자열로 변경한다
  • className 프로퍼티는 문자열로 반환하므로 공백으로 구분된 여러 개의 클래스를 반환하는 경우 다루기가 불편하다.
const $box = document.querySelector('.box');

console.log($box.className); // 'box red'

// red -> blue 클래스명 변경
$box.className = $box.className.replace('red', 'blue');

classList

  • Element.prototype.classList 프로퍼티는 class 어트리뷰트의 정보를 담은 DOMTokenList 객체를 반환한다.
  • DOMTokenList 객체는 유사 배열 객체이며 이터러블이다.
    • add(...className) : add 메서드는 인수로 전달한 1개 이상의 문자열을 class 어트리뷰트 값으로 추가한다.
    • remove(...className) : remove 메서드는 인수로 전달한 1개 이상의 문자열과 일치하는 클래스를 class 어트리뷰트에서 삭제한다. 인수로 전달한 문자열과 일치하는 클래스가 class 어트리뷰트에 없으면 에러 없이 무시된다.
    • item(index) : index 메서드는 인수로 전달한 index에 해당하는 클래스를 class 어트리뷰트에서 반환한다. 인덱스가 0이면 첫번째 클래스를, 1이면 두번째 클래스를 반환.
    • contains(className) : contains 메서드는 인수로 전달한 문자열과 일치하는 클래스가 class 어트리뷰트에 포함되어 있는지 확인한다.
    • replace(oldClassName, newClassName) : replace 메서드는 class 어트리뷰트에서 첫 번째 인수로 전달한 문자열을 두 번째 인수로 전달한 문자열로 변경한다.
    • toggle(className[, force]) : toggle 메서드는 class 어트리뷰트에 인수로 전달한 문자열과 일치하는 클래스가 존재하면 제거하고, 존재하지 않으면 추가한다. 두번째 인수로 불리언 값으로 평가되는 조건식을 전달할 수 있다. 이때 조건식의 평가 결과가 true이면 class 어트리뷰트에 강제로 첫 번째 인수로 전달받은 문자열을 추가하고, false이면 class 어트리뷰트에서 강제로 첫 번째 인수로 전달받은 문자열을 제거한다.
// add
$box.classList.add('foo'); // class="box red foo"
$box.classList.add('bar', 'baz'); // class="box red foo bar baz"

// remove
$box.classList.remove('foo'); // class="box red bar baz"
$box.classList.remove('bar', 'baz'); // class="box red"
$box.classList.remove('x'); // class="box red"

// item
$box.classList.item(0); // 'box'
$box.classList.item(1); // 'red'

// contains
$box.classList.contains('box'); // true
$box.classList.contains('blue'); // false

// replace
$box.classList.replace('red', 'blue'); // class="box blue"

// toggle
$box.classList.toggle('foo'); // class="box blue foo"
$box.classList.toggle('foo'); // class="box blue"
$box.classList.toggle('foo', true); // class="box blue foo"
$box.classList.toggle('foo', false); // class="box blue"

39.8.3 요소에 적용되어 있는 CSS 스타일 참조

  • HTML 요소에 적용되어 있는 모든 CSS 스타일을 참조해야 할 경우 getComputedStyle 메서드를 사용한다.
  • window.getComputedStyle(element[, pseudo]) 메서드는 첫 번째 인수로 전달할 요소 노드에 적용되어 있는 평가된 스타일을 CSSStyleDeclaration 객체에 담아 반환한다.
  • 평가된 스타일이란 요소 노드에 적용되어 있는 모든 스타일(링크 스타일, 임베딩 스타일, 인라인 스타일, 자바스크립트에서 적요한 스타일, 상속된 스타일, 기본 스타일, ...)을 말한다.
  • 두번 째 인수(pseudo)로 :after, :before와 같은 의사 요소를 지정하는 문자열을 전달할 수 있다.
<style>
  body {color: red;}
  .box {width: 100px; height: 50px; background-color: red; border: 1px solid black;}
  .box::before {content: 'Hello';}
</style>
<div class="box">Box</div>
<script>
const $box = document.querySelector('.box');

// box 요소에 적용된 모든 CSS 스타일 취득
const computedStyle = window.getComputedStyle($box);
console.log(computedStyle); // CSSStyleDeclaration

// 임베딩 스타일
console.log(computedStyle.width); // 100px;
console.log(computedStyle.height); // 50px;
console.log(computedStyle.backgroundColor); // rgb(255, 0, 0);
console.log(computedStyle.border); // 1px solid rgb(0, 0, 0);

// 상속 스타일
console.log(computedStyle.color); // rgb(255, 0, 0)

// 기본 스타일
console.log(computedStyle.display); // block

// 의사 요소
const computedStyleBefore = window.getComputedStyle($box, ':before');
console.log(computedStyleBefore.content); // 'Hello'
</script>

0개의 댓글