본문 바로가기

Javascript

[모던 자바스크립트 Deep Dive] 36-2. DOM

노드 탐색

그림

  • DOM 트리 상의 노드를 탐색할 수 있도록 Node, Element 인터페이스는 트리 탐색 프로퍼티 제공
  • 노드 탐색 프로퍼티는 모두 접근자 프로퍼티로 getter 만 존재하여 참조만 가능한 읽기 전용 접근자 프로퍼티

1. 공백 텍스트 노드

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

2. 자식 노드 탐색

프로퍼티 설명
Node.prototype.childNodes - 자식 노드를 모두 탐색하여 DOM 컬렉션 객체인 NodeList 에 담아 반환 
- 텍스트 노드 또는 요소 노드 반환
Element.prototype.children - 자식 노드 중에서 요소 노드만 모두 탐색하여 DOM 컬렉션 객체인 HTMLCollection 에 담아 반환
- 텍스트 노드 미포함
Node.prototype.firstChild - 첫 번째 자식 노드 반환
- 텍스트 노드 또는 요소 노드 반환 
Node.prototype.lastChild - 마지막 자식 노드 반환 
- 텍스트 노드 또는 요소 노드 반환
Element.prototype.firstElementChild - 첫 번째 자식 요소 노드 반환
- 요소 노드만 반환
Element.prototype.lastElementChild - 마지막 자식 요소 노드 반환
- 요소 노드만 반환

 

3. 자식 노드 존재 확인

  • 자식 노드가 존재하는지 확인하려면 Node.prototype.hasChildNodes 메서드 사용
  • 자식 노드 존재 여부에 따라 불리언 값 반환
  • childNodes 프로퍼티와 마찬가지로 텍스트 노드를 포함하여 자식 노드 존재 확인
  • 자식 노드 중에 텍스트 노드가 아닌 요소 노드가 존재하는지 확인하려면 children.length 또는 childElementCount 프로퍼티 사용

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

  • 요소 노드의 텍스트 노드는 요소 노드의 자식 노드로 firstChild 프로퍼티로 접근 가능

 

5. 부모 노드 탐색

  • 부모 노드를 탐색하려면 Node.prototype.parentNode 프로퍼티를 사용
  • 텍스트 노드는 DOM 트리의 최종단 노드인 리프 노드로 부모 노드가 텍스트 노드인 경우는 X

6. 형재 노드 탐색

  • 어트리뷰트 노드는 요소 노드와 연결되어 있지만 부모 노드가 같은 형제 노드가 아니기 때문에 반환 X
프로퍼티 설명
Node.prototype.previousSibling - 부모 노드가 같은 형제 노드 중에서 자신의 이전 형제 노드를 탐색하여 반환
- 요소 노드 또는 텍스트 노드 반환
Node.prototype.nextSibiling - 부모 노드가 같은 형제 노드 중에서 자신의 다음 형제 노드를 탐색하여 반환
- 요소 노드 또는 텍스트 노드 반환
Node.prototype.previousElementSibling - 부모 노드가 같은 형제 요소 노드 중에서 자신의 이전 형제 요소 노드를 탐색하여 반환
- 요소 노드 반환
Node.prototype.nextElementSibling - 부모 노드가 같은 형제 요소 노드 중에서 자신의 다음 형제 요소 노드를 탐색하여 반환
- 요소 노드 반환

노드 정보 취득

프로퍼티 설명
Node.prototype.nodeType 노드 객체의 종류, 즉 노드 타입을 나타내는 상수 반환 또는 노드 타입 상수는 Node에 정의 되어 있음

- Node.ELEMENT_NODE : 요소 노드 타입을 나타내는 상수 1 반환
- Node.TEXT_NODE : 텍스트 노드 타입을 나타내느 상수 3 반환
- Node.DOCUMENT_NODE : 문서 노드 타입을 나타내는 상수 9 반환
Node.prototype.nodeName 노드의 이름을 문자열로 반환

- 요소 노드 : 대문자 문자열로 태그 이름 'UL', 'LI' 등을 반환
- 텍스트 노드 : 문자열 #text 반환
- 문서 노드 : 문자열 #document 반환
<!DOCTYPE html>
<html>
  <body>
    <div id="foo">Hello</div>
  </body>
  <script>
  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'
  </script>
</html>

 

요소 노드의 텍스트 조작

1. nodeValue

  • 노드 객체의 nodeValue 프로퍼티를 참조하면 노드 객체의 값 (텍스트 노드의 텍스트) 반환
<!DOCTYPE html>
<html>
  <body>
    <div id="foo">Hello</div>
  </body>
  <script>
  // 문서 노드의 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'
  </script>
</html>

 

1-1. nodeValue 텍스트 변경

  1. 텍스트를 변경할 요소 노드 취득 → 취득한 요소 노드의 텍스트 노드 탐색
  2. 탐색한 노드의 nodeValue 프로퍼티를 사용하여 텍스트 노드 값 변경
<!DOCTYPE html>
<html>
  <body>
    <div id="foo">Hello</div>
  </body>
  <script>
  // 1. #foo 요소 노드의 자식 노드인 텍스트 노드 취득
  const $textNode = document.getElementById('foo').firstChild
  
  // 2. nodeValue 프로퍼티를 사용하여 텍스트 노드의 값 변경
  $textNode.nodeValue = 'World'
  </script>
</html>

 

2. textContent

  • 요소노드의 textContent 프로퍼티를 참조하면 요소 노드의 콘텐츠 영역 (시작 태그와 종료 태그 사이) 내의 텍스트 모두 반환
  • 요소노드의 childeNodes 프로퍼티가 반환한 모든 노드들의 텍스트를 모드 반환 (HTML 마크업은 무시됨)
<!DOCTYPE html>
<html>
  <body>
    <div id="foo">Hello <span>World!</span></div>
  </body>
  <script>
  // 요소 노드의 텍스트를 모두 취득 (HTML 마크업 무시) 
  console.log(document.getElementById('foo').textContent)
  </script>
</html>

 

2-1. textContent 텍스트 변경

<!DOCTYPE html>
<html>
  <body>
    <div id="foo">Hello <span>World!</span></div>
  </body>
  <script>
  // 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가 (HTML 마크업 파싱X)
  document.getElementById('foo').textContet = 'Hi <span>Echichi!</span>'
  // 'Hi <span>Echichi!</span>'
  </script>
</html>

 

textContent 프로퍼티와 유사한 동작을 하는 innerText가 있지만 사용하지 않는 것이 좋다

  • innerText 프로퍼티는 CSS 에 순종적으로 CSS 에 의해 비표시로 지정된 요소 노드의 텍스트를 반환 X
  • innerText 프로퍼티는 CSS 를 고려해야 하므로 textContent 프로퍼티보다 느림

DOM 조작

  • DOM 조작에 의해 새로운 노드가 추가되거나 삭제되면 리플로우와 리페인트가 발생하는 원인이 되므로 성능에 악영향
  • 성능 최적화를 위해 주의해서 다루어야 함

1. innerHTML

  • Element.prototype.innerHTML 프로퍼티는 setter와 getter 가 모두 존재하는 접근자 프로퍼티
  • 요소 노드의 HTML 마크업을 취득하거나 변경
  • 요소 노드의 콘텐츠 영역 내에 포함된 모든 HTML 마크업을 문자열로 변환 
  • textContent 프로퍼티는 HTML 마크업을 무시하고 텍스트만 반환하지만 innerHTML 프로퍼티는 HTML 마크업이 포함된 문자열 그래로 반환
  • 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열에 포함되어 있는 HTML 마크업이 파싱되어 요소 노드의 자식 노드로 DOM에 반영
<!DOCTYPE html>
<html>
  <body>
    <div id="foo">Hello <span>World!</span></div>
  </body>
  <script>
  // HTML 마크업이 파싱되어 요소 노드의 자식 노드로 DOM에 반영
  document.getElementById('foo').innerHTML = 'Hi <span>Echichi!</span>'
  </script>
</html>

 

  • innerHTML 프로퍼티에 할당한 HTML 마크업 문자열은 렌더링 엔진에 의해 파싱되어 요소 노드의 자식으로 DOM에 반영되기 때문에 크로스 사이트 스크립트 공격 (XSS) 에 취약
  • HTML 새니티제이션 : 사용자로부터 입력받은 데이터에 의해 발생할 수 있는 크로스 사이트 스크립팅 공격을 예방하기 위해 잠재적 위험을 제거하는 기능
  • 복잡하지 않은 요소를 새롭게 추가할 때 유용하지만 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입해야 할 때는 사용하지 않는 것이 좋음

2. insertAdjacnetHTML 메서드

  • Element.prototype.insertAdjacentHTML 메서드는 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입
  • 두 번째 인수로 전달한 HTML 마크업 문자열을 파싱하고 그 결과로 생성된 노드를 첫 번째 인수로 전달한 위치에 삽입하여 DOM에 반영
  • 기존 요소에는 영향을 주지 않고 새롭게 삽입될 요소만을 파싱하여 자식 요소로 추가하므로 innerHTML 프로퍼티보다 효율적이고 빠름
  • HTML 마크업 문자열을 파싱하므로 크로스 사이트 스크립팅 공격에 취약
<!DOCTYPE html>
<html>
  <body>
  <!-- beforebegin -->
    <div id="foo">
      <!-- afterbegin -->
    Hello 
     <!-- beforeend -->
    </div>
      <!-- afterend -->
  </body>
  <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>
</html>

 

3. 노드 생성과 추가

  • DOM 노드를 직접 생성/ 삽입/ 삭제/ 치환하는 메서드 제공

3-1. 요소 노드 생성

  • Document.prototype.createElement(tagName) 메서드는 요소 노드를 생성하여 반환
  • 요소 노드를 생성할 뿐 DOM 에 추가하지 않으므로 추가 처리가 별도 필요
// 요소 생성
const $li = document.createElement('li');

 

3-2. 텍스트 노드 생성

  • Document.prototype.createTextNode(text) 메서드는 텍스트 노드를 생성하여 반환 
  • 텍스트 노드를 생성할 뿐 요소 노드에 추가하지 않으므로 추가 처리 필요
// 텍스트 노드 생성
const textNode = document.createTextNode('Banana');

 

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

  • Node.prototype.appendChild(childNode) 메서드는 매개변수 childNode 에게 인수로 전달한 노드를 appendChild 메서드를 호출한 노드의 마지막 자식 노드로 추가
  • 요소 노드에 자식 노드가 하나도 없는 경우 textContent 프로퍼티를 사용하는 편이 더욱 간편
 // = $li.appendChild(textNode);
    $li.textContent = "Banana;";

 

3-4. 요소 노드를 DOM에 추가

  • Node.prototype.appendChild(childNode) 메서드를 사용하여 텍스트 노드와 부자 관계로 연결한 요소 노드를 #fruits 요소 노드의 마지막 자식 요소로 추가
// $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
$fruits.appendChild($li);

 

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById("fruits");
    // 요소 생성
    const $li = document.createElement("li");
    // 텍스트 노드 생성
    const textNode = document.createTextNode("Banana");

    // = $li.appendChild(textNode);
    $li.textContent = "Banana";
    // $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
    $fruits.appendChild($li);
  </script>
</html>

 

4. 복수의 노드 생성과 추가

  • DocumentFragment 노드는 문서, 요소, 어트리뷰트, 텍스트 노드와 같은 노드 객체의 일종으로 부모 노드가 없어서 기존 DOM 과는 별도로 존재한다는 특장
  • 자식 노드들의 부모 노드로서 별도의 서브 DOM 을 구성하여 기존의 DOM에 추가하기 위한 용도로 사용
  • Document.prototype.createDocumentFragment 메서드는 비어있는 DocumentFragment 노드를 생성하여 반환
<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits"></ul>
  </body>
  <script>
    const $fruits = document.getElementById("fruits");
    // Documentfragment 노드 생성
    const fragment = document.createDocumentFragment();

    ["Apple", "Banana", "Orange"].forEach((text) => {
      // 1. 요소 노드 생성
      const $li = document.createElement("li");

      // 2. 택스트 추가
      $li.textContent = text;

      // 3. 요소 노드를 Documentfragment노드에 추가
      fragment.appendChild($li);
    });

    // Documentfragment 노드를 $fruits 요소 노드의 마지막 자식 노드로 추가
    $fruits.appendChild(fragment);
  </script>
</html>

 

5. 노드 삽입

5-1. 마지막 노드로 추가

  • Node.prototype.appendChild 메서드는 인수로 전달받은 노들르 자신을 호출한 노드의 마지막 자식 노드로 DOM 에 추가

5-2. 지정한 위치에 삽입

  • Node.prototype.insertBefore(newNode, childNode) 메서드는 첫 번째 인수로 전달받은 노드를 두 번째 인수로 전달 받은 노드 앞에 삽입
  • 두 번째 인수로 전달받은 노드는 반드시 insertBefore를 호출한 노드의 자식 노드여야함 (DOMException 발생)
  • 두 번째 인수로 전달받은 노드가 null 이면 마지막 자식 노드로 추가됨 (=appendChild)
<!DOCTYPE html>
<html>
  <body>
    <div>test</div>
    <ul id="fruits"></ul>
  </body>
  <script>
    const $fruits = document.getElementById("fruits");
    const fragment = document.createDocumentFragment();
    ["Apple", "Banana", "Orange"].forEach((text) => {
      const $li = document.createElement("li");
      $li.textContent = text;
      fragment.appendChild($li);
    });

    $fruits.appendChild(fragment);
    const $li = document.createElement("li");
    $li.textContent = "appendChild";
    $fruits.appendChild($li);
    const $li2 = document.createElement("li");
    $li2.textContent = "insertBefore";
    $fruits.insertBefore($li2, $fruits.firstChild);
  </script>
</html>

 

6. 노드 이동

  • DOM 에 이미 존재하는 노드를 appendChild 또는 insertBefore 메서드를 사용하여 DOM에 다시 추가하면 현재 위치에서의 Node 를 제거하고 새로운 위치에 노드를 추가하여 노드가 이동됨
<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
      <li>Banana</li>
      <li>Orange</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById("fruits");

    // 이미 존재하는 요소 노드 취득
    const [$apple, $banana] = $fruits.children;

    // 이미 존재하는 $apple 요소 노드를 $fruits 요소 노드의 마지막 노드로 이동
    $fruits.appendChild($apple); // Banana - Orange - Apple

    // 이미 존재하는 $banana 요소 노드를 #fruits 요소의 마지막 자식 노드 앞으로 이동
    $fruits.insertBefore($banana, $fruits.lastElementChild); // Orange - Banana - Apple
  </script>
</html>

 

7. 노드 복사

  • Node.prototype.cloneNode([deep : true | false]) 메서드는 노드의 사본을 생서앟여 반환
  • 매개변수 deep 에 true 를 인수로 전달하면 노드를 깊은 복사하여 모든 자손 노드가 포함된 사본을 생성, false 를 인수로 전달하거나 생략하면 노드를 얕은 복사하여 노드 자신만의 사본 생성
<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById("fruits");
    const $apple = $fruits.firstElementChild;

    // $apple 요소를 얕은 복사하여 사본 생성 (텍스트 노드가 없는 사본 생성)
    const $shallowClone = $apple.cloneNode();

    // 사본 요소 노드에 텍스트 추가
    $shallowClone.textContent = "Banana";
    // 사본 요소 노드를 #fruits 요소 노드 마지막 노드로 추가
    $fruits.appendChild($shallowClone);

    // #fruits 요소를 깊은 복사하여 모든 자손 노드가 포함된 사본 생성
    const $deepClone = $fruits.cloneNode(true);
    $fruits.appendChild($deepClone);
  </script>
</html>

 

8. 노드 교체

  • Node.prototype.replaceChild(newChild, oldChild) 메서드는 자신을 호출한 노드의 자식 노드를 다른 노드로 교체
  • oldChild 매개변수에 인수로 전달한 노드는 replaceChild 메서드를 호출한 노드의 자식 노드여야함
  • 자신을 호출한 노드의 자식 노드인 oldChild 노드를 newChild  노드로 교체 (oldChild 노드는 DOM에서 제거)
<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById("fruits");
    const $apple = $fruits.firstElementChild;

    // 기존 노드와 교체할 요소 노드 생성
    const $newChild = document.createElement("li");
    $newChild.textContent = "Banana";

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

 

9. 노드 삭제

  • Node.prototype.removeChild(child) 메서드는 child 매개변수에 인수로 전달한 노드를 DOM에서 삭제
  • 인수로 전달한 노드는 removeChild 메서드를 호출한 노드의 자식 노드이어야 함
<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
      <li>Banana</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById("fruits");
    // #fruits 요소 노드의 마지막 요소를 DOM에서 삭제
    $fruits.removeChild($fruits.lastElementChild);
  </script>
</html>