모던 자바스크립트 Deep Dive 39장 정리 - DOM

Hyodduru ·2022년 8월 28일
0
post-thumbnail

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

1. 노드

HTML 요소와 노드 객체

HTML 요소는 HTML 문서를 구성하는 개별적인 요소를 의미한다.

HTML 요소는 렌더링 엔진에 의해 파싱되어 DOM을 구성하는 요소로 노드 객체로 변환된다. 이때 HTML 요소의 어트리뷰트는 어트리뷰트 노드로, HTML 요소의 텍스트 콘텐츠는 텍스트 노드로 변환된다.

트리 자료구조

트리 자료구조는 노드들의 계층 구조로 이뤄짐.

노드 객체들로 구성된 트리 자료구조를 DOM이라고 함

노드 객체의 타입

  • 문서 노드(document node)
    DOM 트리의 최상위에 존재하는 루트 노드로서 document 객체를 가리킴.

  • 요소 노드(element node)
    HTML 요소를 가리키는 객체. 문서의 구조를 표현한다. HTML 요소 간의 중첩에 의해 부자 관계를 가지며, 이 부자 관계를 통해 정보를 구조화함.

  • 어트리뷰트 노드(attribute node)
    HTML 요소의 attribute를 가리키는 객체. attribute가 지정된 HTML 요소의 요소 노드와 연결되어 있음. 단, 요소 노드는 부모 노드와 연결되어 있지만 어트리뷰트 노드는 부모 노드 와 연결되어 있지 않고 요소 노드에만 연결되어있음!

  • 텍스트 노드(text node)
    HTML 요소의 텍스트를 가리키는 객체. 요소 노드의 자식노드이며 자식 노드를 가질 수 없는 리프노드(leaf node). 즉 텍스트 노드는 DOM 트리의 최종단. 텍스트 노드에 접근하려면 먼저 부모 노드인 요소노드에 접근해야함.

노드 객체의 상속 구조

DOM을 구성하는 노드 객체는 ECMAScript 사양에 정의된 표준 빌트인 객체가 아니라 브라우저 환경에서 추가적으로 제공하는 호스트 객체(host objects).

하지만 노드 객체도 자바스크립트 객체이므로 프로토타입에 의한 상속 구조를 갖는다!

Object - EventTarget - Node - (Document, Element, Attr, CharacterData) - ... - ...

ex) Input 노드 객체의 상속 구조
Object - EventTarget - Node - Element - HTMLElement - HTMLInputElement

👏 결론

DOM은 HTML 문서의 계층적 구조와 정보를 표현하는 것은 물론 노드 객체의 종류, 즉 노드 타입에 따라 필요한 기능을 프로퍼티와 메서드의 집합인 DOM API(Application Programming Interface)로 제공함. 이 DOM API를 통해 HTML 구조나 내용 또는 스타일 등을 동적으로 조작할 수 있다.

2. 요소 노드 취득

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

id를 이용한 요소 노드 취득

const something = document.getElementById('banana');

something.style.color = 'red';

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

인수로 전달한 태그 이름을 갖는 모든 요소 노드들을 탐색하여 반환함. DOM 컬렉션 객체인 HTMLCollection 객체를 반환한다!

🔖 HTMLCollection 객체는 유사 배열 객체이면서 이터러블.

const something = document.getElementsByTagName('li');

HTML 문서의 모든 요소 노드를 취득하려면 메서드의 인수로 '*'를 전달한다.

// 모든 요소 노드를 탐색하여 반환 
const all = document.getElementByTagName('*');

class를 이용한 요소 노드 취득

여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 HTMLCollectioin 객체를 반환

const elems = document.getElementsByClassName('fruit');

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

CSS 선택자는 스타일을 적용하고자 하는 HTML 요소를 특정할 때 사용하는 문법.

/* 전체 선택자 : 모든 요소 선택*/
*{...}
/* 태그 선택자 : 모든 p 태그 요소 선택*/
p{...}
/* id 선택자 : id 값이 'foo'인 요소 선택*/
#foo{...}
/* 자식 선택자 : div 요소의 자식 요소 중 p 요소를 모두 선택*/
div > p {...}
/* 일반 형재 선택자 : p 요소의 형제 요소 중에 p 요소 뒤에 위치하는 ul 요소를 모두 선택*/
p ~ ul {...}
/* 가상 클래스 선택자 : hover 상태인 a 요소를 모두 선택*/
a:hover {...}
/* 가상 요소 선택자 : p 요소의 콘텐츠 앞에 위치하는 공간을 선택. 일반적으로 content 프로퍼티와 함께 사용된다.*/
p::before{...}

querySelector

인수로 전달한 CSS 선택자를 만족시키는 요소 노드가 여러 개인 경우 첫번째 요소 노드만 반환함.

document.querySelector('banana');

querySelectorAll

모든 요소 노드 반환. DOM 컬렉션 객체인 NodeList 객체를 반환한다. 유사 배열 객체이면서 이터러블.

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

Element.prototype.matches 메서드

<body>
  <ul id ="fruits">
    <li class = "apple">Apple</li>
  </ul>
  
 </body>
<script>
const apple = document.querySelector('.apple'); 
// apple 노드는 #fruits > li.apple로 취득할 수 있다. 
console.log(apple.matches('#fruits > li.apple'));  //true
</script>

HTMLCollection과 NodeList

HTMLCollection과 NodeList의 중요한 특징은 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는 객체라는 것.

HTMLCollection은 언제나 live 객체로 동작한다. 단 NodeList는 대부분의 경우 노드 객체의 상태 변화를 실시간으로 반영하지 않고 과거의 정적 상태를 유지하는 non-live 객체로 동작하지만 경우에 따라 live 객체로 동작할 때가 있음.

HTMLCollection

getElementsByTagName, getElementsByClassName 메서드가 반환한다. 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는 DOM 컬렉션 객체.

NodeList

QuerySelectorAll이 반환.
NodeList.prototype.forEach 메서드를 상속받아 사용할 수 있음.

노드 객체의 상태 변경과 상과없이 안전하게 DOM 컬렉션을 사용하려면 HTMLCollection이나 NodeList 객체를 배열로 변환하여 사용하는 것을 권장함.

ex) 스프레드 문법, Array.from 메서드르 사용하여 변환 가능

노드 탐색

요소 노드를 취득한 다음, 취득한 요소 노드를 기점으로 DOM 트리의 노드를 옮겨 다니며 부모, 형제, 자식 노드 등을 탐색해야할 때가 있음.
👉 DOM 트리 상의 노드를 탐색할 수 있도록 Node, Element 인터페이스는 트리 탐색 프로퍼티를 제공한다.

Node.prototype 가 제공: parentNode, previoustSibling, firstChild, childNodes 프로퍼티
Element.prototype 가 제공 : previousElementSibling, nextElementSibling, children 프로퍼티

공백 텍스트 노드

HTML요소 사이의 스페이스, 탭, 줄바꿈 등의 공백 문자는 공백 테스트 노드를 생성 (DOM트리 내에 생성된다.)

자식 노드 탐색

자식 노드를 탐색하기 위해 사용하는 노드 탐색 프로퍼티들

  • Node.prototype.childNodes
    childNodes 프로퍼티가 반환한 NodeList에는 요소 노드뿐만 아니라 텍스트 노드도 포함되어 있을 수 있다.

  • Element.prototype.children
    children 프로퍼티가 반환한 HTMLCollection에는 텍스트 노드가 포함되지 않는다.

  • Node.prototype.firstChild
    텍스트 노드이거나 요소노드

  • Node.prototype.lastChild
    텍스트 노드이거나 요소노드

  • Element.prototype.firstElementChild
    요소노드만 반환

  • Element.prototype.lastElementChild
    요소노드만 반환

ex)

  <ul id ="fruits">
   	<li class ="apple">Apple</li>
   	<li class ="banana">Banana</li>
   	<li class ="orange">Orange</li>
  </ul>

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

console.log(fruits.childNodes);
// fruits요소의 모든 자식 노드 탐색 (텍스트 노드 포함)
// NodeList(7) [text, li.apple, text, li.banana, text, li.orange, text]] 

console.log(fruits.children);
// fruits 요소의 모든 자식 노드를 탐색함 (요소 노드만 포함)
// HTMLCollection [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 

</script>
      
  

자식 노드 존재 확인

👉 Node.prototype.hasChildNodes 메서드 사용
텍스트 노드를 포함하여 자식 노드의 존재를 확인


const fruits = document.getElementById('fruits');

console.lg(fruits.hasChildNodes); // true 

👉 children, childElementCount
텍스트 노드가 아닌 요소 노드가 존재하는지 확인

console.log(fruits.children.length); // 0 => false
console.log(fruits.childElementCout); // 0 => false 

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

요소노드의 텍스트 노드는 요소 노드의 자식 노드다. 따라서 요소 노드의 텍스트 노드는 firstChild 프로퍼티로 접근할 수 있다. firstChild 프로퍼티는 첫 번째 자식 노드를 반환. firstChild 프로퍼티가 반환한 노드는 텍스트 노드이거나 요소 노드.

console.log(document.getElementsById('foo').firstChild); // #text 

부모 노드 탐색

Node.prototype.paraneNode 사용
텍스트 노드는 DOM 트리의 최종단 노드인 리프 노드이므로 부모 노드가 텍스트 노드인 경우는 없음.

형제 노드 탐색

Node.prototype.previousSibling, Node.prototype.NextSibling, Element.prototype.previousElementSibling, Element.prototype.nextElementSibling 사용

어트리뷰트 노드는 요소 노드와 연결되어 있지만, 부모 노드가 같은 형제 노드가 아니기 때문에 반환되지 않음. previousElementSibling, nextElementSibling의 경우 요소 노드만 반환

노드 정보 취득

  • Node.prototype.nodeType : 상수 반환
    Node.ElEMENT_NODE : 요소 노드 타입을 나타내는 상수 1을 반환
    Node.TEXT_NODE : 텍스트 노드 타입을 나타내는 상수 3반환
    Node.DOCUMENT_NODE :문서 노드 타입을 나타내는 상수 9 반환

  • Node.prototype.nodeName
    요소 노드 : 대문자 문자열로 태그 이름 ("UL" 등) 반환
    텍스트 노드 : "#text" 반환
    문서 노드 : "#document" 반환

요소 노드의 텍스트 조작

nodeValue

Node.prototype.nodeValue 프로퍼티 : setter와 getter 모두 존재하는 접근자 프로퍼티. 즉 참조와 할당 모두 가능.

<div id = "foo">Hello</div>

<script>
const textNode = foo.firstNode; 

console.log(textNode.nodeValue); // Hello
</script>
const textNode = document.getElementsById('foo').firstChild;

// nodeValue 프로퍼티를 사용하여 텍스트 노드의 값을 변경 
textNode.nodeValue = 'World'; 

console.log(textNode.nodeValue); // World

textContent

Node.prototype.textContext 프로퍼티 : setter와 getter 모두 존재.
요소 노드의 콘텐츠 영역 내의 텍스트를 모두 반환. HTML 마크업은 무시된다.

<div id="foo">Hello <span> world!</span> </div>
  <script>
  // #foo 요소노드는 텍스트 노드가 아님. 
  console.log(document.getElementsById('foo').nodeValue); // null
// #foo 요소노드의 자식 노드인 텍스트 노드의 값을 취득함  
console.log(document.getElementsById('foo').firstChild.nodeValue); // Hello
// span 요소 노드의 자식 노드인 텍스트 노드의 값을 취득함 
 console.log(document.getElementsById('foo').nodeValue); // world! 
  </script>
// 요소 노드의 콘텐츠 영역에 다른 요소 노드가 없고 텍스트만 존재 
<div id ="foo">Hello </div>

const foo = document.getElementById('foo');
console.log(foo.textContext === foo.firstChild.nodeValue); // true

textContext 는 HTML 마크업이 파싱되지 않는다는 특징이 있다.

<div id ="foo">Hello <span>world!</span></div>

<script>
// foo 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가된다. 
document.getElementById('foo').textContext = 'Hi <span>there!</span>'.
</script>

🚨 textContext와 유사한 동작을 하는 innerText 프로퍼티가 있음. 사용하지 않는 것이 좋음.

  • innerText 프로퍼티는 CSS에 순종적. 예를 들어 innerText 프로퍼티는 CSS에 의해 비표시(visibility : hidden)로 지정된 요소 노드의 텍스트를 반환하지 않음
  • innerText 프로퍼티는 CSS를 고려해야 하므로 textContext보다 느림.

DOM 조작

innerHTML

Element.prototype.innerHTML 프로퍼티 : getter와 setter 모두 존재하는 접근자 프로퍼티.

textContent 프로퍼티를 참조하면 HTML마크업을 무시하고 텍스트만 반환하지만 innerHTML 프로퍼티는 HTML 마크업이 포함된 문자열을 그대로 반환함.

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

✔️ 사용자로부터 입력받은 데이터(untrusted input data)를 그대로 innerHTML 프로퍼티에 할당하는 것은 크로스 사이트 스크립팅 공격(XSS : Cross-Site Scripting Attacks)에 취약하므로 위험함.
HTML 마크업 내에 자바스크립트 악성 코드가 포함되어 있다면 파싱 과정에서 그대로 실행될 가능성이 있기 때문!

HTML5는 innerHTML 프로퍼티로 삽입된 script 요소 내의 자바스크립트 코드를 실행하지 않지만, script 없이도 사이트 스크립트 공격은 가능.
ex)

// 에러 이벤트를 강제로 발생시켜서 자바스크립트 코드가 실행되도록 함. 
document.getElementsById('foo').innerHTML = `<img src ="x">`;

🔖 HTML 새니티제이션 (HTML sanitization)
HTML 새니티제이션은 사용자로부터 입력받은 데이터에 의해 발생할 수 있는 크로스 사이트 스크립팅 공격 예방하기 위해 잠재적 위험 제거하는 기능. DOMPurify 라이브러리를 사용하는 것을 권장

DOMPurify.sanitize('<img src ="x">');
// => <img src="x" />

✔️ innerHTML 또다른 단점 : 요소 노드의 innerHTML 프로퍼티에 HTML 마크업 문자열을 할당하는 경우 요소 노드의 모든 자식 노드를 제거하고 할당한 HTML 마크업 문자열을 파싱하여 DOM을 변경.

fruits.innerHTML += '<li class="banana">banana</li>'

위의 코드는 기존에 존재한 코드를 제거하고 다시 처음부터 새롭게 자식 노드를 생성하여 DOM에 반영함. 효율적.

👉 innerHTML은 복잡하지 않은 요소를 새롭게 추가할 때 유용하지만 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입해야 할 때는 사용하지 않는 것이 좋음

insertAdjacentHTML

Elemnt.prototype.insertAdjacentHTML(position, DOMString) 메서드는 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입한다.

첫번째 인수로 전달할 수 있는 문자열 : 'beforebegin', 'afterbegin', 'beforeend', 'afterend'

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

<script>
  foo.insertAdjacentHTML('beforebegin', '<p>hi</p>');
                   
  </script>
  

👉 기존 요소에 영향 주지 않고 새롭게 삽입될 요소만을 파싱하여 자식 요소로 추가하므로 innerHTML 프로퍼티보다 효율적이고 빠름. 단, innerHTML 프로퍼티와 마찬가지로 HTML 마크업 문자열을 파싱하므로 크로스 사이트 스크립칭 공격에 취약하다는 점은 동일

노드 생성과 추가

  • Document.prototype.createElement(tagName) - 요소 노드 생성
  • Document.prototype.createTextNode(text) - 텍스트 노드 생성
  • Node.prototype.appendChild(childNode) - 텍스트 노드르 자식의 노드로 추가
// 텍스트 노드를 생성하여 요소 노드의 자식 노드로 추가 
li.appendChild(document.createTextNode('Banana'));

// li 요소 노드에 자식 노드가 하나도 없는 위 코드와 동일하게 동작 
li.textContext = 'Banana';
  • Node.prototype.appendChild - 요소 노드를 DOM에 추가

복수의 노드 생성과 추가

DocumentFragment 노드를 사용하는 것이 효율적.
👉 문서, 요소, 어트리뷰트, 텍스트 노드와 같은 노드 객체의 일종으로 부모 노드가 없어서 기존 DOM과는 별도로 존재한다는 특징이 있음. (자식 노드를 추가한다고 하여 기존 DOM에 어떠한 변경도 발생하지 않음)

const fruits = document.getElementsById('fruits'); 

// DocumentFragment 노드 생성 
const fragment = document.createDocumentFragment();

['Apple', 'Banana', 'Orange'].forEach(text => {
  // 1. 요소 노드 생성
  const li = document.createElement('li');
  // 2. 텍스트 노드 생성 
  const textNode = document.createTextNode(text);
  // 3. 텍스트 노드를 li 요소 노드의 자식 노드로 추가 
  li.appendChild(textNode);
  // 4. li 요소 노드를 DocumentFragment 노드의 마지막 자식 노드로 추가 
  fragment.appendChild(li);
});;


// DocumentGfament 노드를 fruits 요소 노드의 마지막 자식 노드로 추가 
fruits.appendChild(fragment);

위를 통해 실제로 DOM 변경이 발생하는 것은 한 번뿐이며 리플로우와 리페인트도 한 번만 실행된다. 따라서 여러 개의 요소 노드를 DOM에 추가하는 경우 DocumentFragment노드 사용하는 것이 더 효율적!

노드 삽입

  • Node.prototype.appendChild
  • node.protoype.insertBefore(newNode, childNode) - 지정한 위치에 노드 삽입
const fruits = document.getElementsById('fruits'); 
const li = document.createElement('li');

// 텍스트 노드를 li 요소 노드의 마지막 자식 노드로 추가 
li.appendChild(document.createTextNode('Orange')); 

// li 요소 노드를 fruits 요소 노드의 마지막 자식 요소 앞에  삽입 
fruits.insertBefore(li, fruits.lastElementChild); 

두 번째 인수로 전달받은 노드는 반드시 insertBefore 메서드를 호출한 노드의 자식 노드이어야 함!

노드 이동

appendChild, insertBefore 메서드 사용

노드 복사

Node.prototype.cloneNode([deep : true | false]) 메서드는 노드의 사본을 생성하여 반환.

// 텍스트 노드가 없는 사본이 생성됨 
const shallowClone = apple.cloneNode(); 

shallowClone.textContent = 'Banana'; 

fruits.appendChild(shallowClone);

// 모든 자손 노드가 포함된 사본을 생성
const deepClone = fruits.cloneNode(true);

fruits.appendChild(deepClone); 

노드 교체

Node.prototype.replaceChild(newChild, oldChild) 메서드는 자신을 호출한 노드의 자식 노드를 다른 노드로 교체.

const fruits = document.getElementById('fruits');

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

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

노드 삭제

Node.prototype.removeChild(child) 메서드는 child 매개변수에 인수로 전달한 노드를 DOM에서 삭제.
인수로 전달한 노드는 removeChild 메서드를 호출한 노드의 자식 노드이어야함.

// fruits 요소 노드의 마지막 요소를 DOM에서 삭제 
fruits.removeChild(fruits.lastElementChild); 
profile
꾸준히 성장하기🦋 https://hyodduru.tistory.com/ 로 블로그 옮겼습니다

0개의 댓글