본문 바로가기
Javascript

DOM

by 안자바먹지 2020. 11. 25.
728x90

Document Object Model은 HTML 문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API, 즉 프로퍼티와 메서드를 제공하는 트리 자료구조 이다. HTML 문서의 계층적 구조와 정보를 표현하는 것은 물론 노드 객체의 종류에 따라 필요한 기능을 프로퍼티와 메서드의 집합인 DOM API 로 제공한다.

 


요소취득

  • document.getElementById( 'id' ) : 인수로 전달한 id 값을 갖는 하나의 요소 노드를 탐색하여 반환
  • document.getElementsByTagName( 'div' ) : 인수로 전달한 태그 이름을 갖는 모든 요소 노드들을 탐색하여 반환한다. 반환값은 여러개의 노드 객체를 갖는 DOM 컬렉션 객체인 HTMLCollection 객체를 반환한다. HTMLCollection 객체는 유사 배열 객체이면서 이터러블 이다. getElementsByTagName 메서드는 Document.property와 Element.property에 정의된 메서드가 있다. Element.property의 getElementsByTagName 메서드는 특정 요소 노드를 통해 호출한다.
  • document.getElementByClassName( 'class' ) : 인수로 전달한 class 값을 갖는 모든 요소들을 탐색하여 반환한다. 반환값은 HTMLCollection 객체이다.
  • document.queryselector( 'css selector' ) : 인수로 전달한 CSS 선택자를 만족시키는 하나의 요소 노드를 탐색하여 반환한다. 
  • document.queryselectorAll( 'css selector' ) : 인수로 전달한 CSS 선택자를 만족시키는 모든 요소 노드를 탐색하여 반환한다. 반환값은 여러개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 NodeList객체를 반환한다. NodeList는 유사배열 객체이면서 이터러블 이다.

 


 

HTMLCollection / NodeList

이 둘은 모두 유사 배열 객체이면서 이터러블 이다. 따라서 for...of 문으로 순회할 수 있으며 스프레드 문법을 사용하여 간단히 배열로 변환할 수 있다. 이 둘의 중요한 특징은 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는 객체라는 것이다. HTMLCollection은 언제나 live로 동작하지만, NodeList 같은 경우 대부분 실시간으로 반영하지 않고 과거의 정적 상태를 유지하는 non-live 객체로 동작하지만 경우에 따라 live 객체로 동작할 때가 있다.

 


 

HTMLCollection

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

 

... 생략
<style>
  .red { color: red; }
  .blue { color: blue; }
</style>
... 생략
<body>
  <ul id="fruits">
    <li class="red">Apple</li>
    <li class="red">Banana</li>
    <li class="red">Orange</li>
  </ul>
  <script>
    // class가 red인 노드를 모두 탐색하여 HTMLCollection 객체에 담는다
    const $elems = document.getElemnetsByClassName('red');
    // 이 시점에 HTMLCollection 객체에는 3개의 요소 노드가 담겨있다.
    console.log($elems) // HMTLCollection(3) [li.red, li.red, li.red]
    
    for(let i = 0; i < $elems.length; i ++) {
      $elems[i].className = 'blue';
    }
    // 3개에서 1개로 변경되고, 첫번째와 세번째 항목만 파란색으로 변경되었다.
    console.log($elems) // HMTLCollection(1) [li.red]
    
    
  </script>
</body>

 

왜 두번째 li 만 변경되지 않았을까? 바로 HTMLCollection이 살아있는 객체 이기 때문이다. for문이 첫번째 반복할때 $elems의 첫번째 요소의 클래스를 blue로 바꾼다 그러면 HTMLCollection은 실시간으로 2개가 된다. 다음 두번째 반복문에서는 i의 값이 1이므로 Orange의 요소가 blue로 바뀌고 2번째 요소인 Banana는 변경되지 않는다. 그래서 HTMLCollection 객체는 for 문으로 순회하면서 노드 객체의 상태를 변경할때 주의해야 한다. 해결 방안은 for 문을 역방향으로 순회하면 된다. 아니면 while 문을 사용하여 HTMLCollection 객체에 노드가 남아 있지 않을 때까지 무한 반복하는 방법으로 회피할 수도 있다.

 

더 간단한 방법은 부작용을 발생시키는 HTMLCollection 객체를 사용하지 않는것이다. 유사 배열이기때문에 HTMLCollection 객체를 배열로 변환하면 부작용도 사라지고 배열의 고차함수 (map, reduce, filter등등)를 사용할 수 있다.

 


 

NodeList

HTMLCollection 객체의 부작용을 해결하기 위해 getElementsByTagName, getElementsByClassName 메서드 대신 querySelectorAll 메서드를 사용하는 방법도 있다. NodeList 객체를 반환하기 때문이다. NodeList 객체는 실시간으로 노드 객체의 상태 변경을 반영하지 않는 객체다. 또한 NodeList.prototype.forEach 메서드를 상속받아 사용할 수 있다. Array의 forEach와 사용방법은 동일하다. 주의할 점으로는 childNodes 프로퍼티가 반환하는 NodeList 객체는 실시간으로 객체의 상태 변경을 반영하는 라이브 객체로 동작한다.  이렇게 HTMLCollection 객체와 NodeList객체는 예상과 다르게 동작할 수 있으므로 안전하게 배열로 변환하여 사용하는것이 좋다. 유사배열이기 때문에 스프레드 문법이나 Array.from 메서드를 사용하면 된다.

 


 

공백 텍스트 노드

HTML 요소 사이의 스페이스, 탭, 개행 등의 공백 문자는 텍스트 노드를 생성한다. 이를 공백 텍스트 노드라 한다. 그래서 노드를 탐색할 때는 공백 문자가 생성한 공백 텍스트 노드에 주의해야 한다. 

 


 

자식 노드 탐색

Node.prototype.childNoes

자식 노드를 모두 탐색하여 DOM 컬렉션 객체인 NodeList에 담아 반환한다. childNodes 프로퍼티가 반환한 NodeList에는 요소 노트뿐만 아니라 텍스트 노드도 포함되어 있을 수 있다.

 

Element.prototype.children

자식 노드 중에서 요소 노드만 모두 탐색하여 DOM 컬렉션 객체인 HTMLCollection에 담아 반환한다. children 프로퍼티가 반환한 HTMLCollection에는 텍스트 노드가 포함되지 않는다.

 

Node.prototype.firstChild

첫 번째 자식 노드를 반환한다. 반환된 노드는 텍스트 노드이거나 요소 노드이다.

 

Node.prototype.lastChild

마지막 자식 노드를 반환한다. 반환된 노드는 텍스트 노드이거나 요소 노드이다.

 

Element.prototype.firstElementChild

첫 번째 자식 요소 노드를 반환한다. 반환된 노드는 요소 노드이다.

 

Element.prototype.firstElementChild

마지막 자식 요소 노드를 반환한다. 반환된 노드는 요소 노드이다.

 

 

<body>
  <ul id="fruits">
    <li class="apple">Apple</li>
    <li class="banana">Banana</li>
    <li class="orange">Orange</li>
  </ul>
</body>
<script>
  // 노드 탐색의 기점이 되는 #fruits 요소 노드를 취득한다.
  const $fruits = document.getElementById('fruits')
  
  // #fruits 요소의 모든 자식 노드를 탐색한다.
  // childNodes 프로퍼티가 반환한 값은 텍스트 노드도 포함되어 있다.
  console.log($fruits.childNodes); // NodeList(7) [text, li.apple........]
  
  // children 프로퍼티가 반환한 값은 요소 노드만 포함되어 있다.
  console.log($fruits.children); // HTMLCollection(3) [li.apple, li.banana, .......]
  
  // firstChild, lastChild 프로퍼티는 텍스트 노드를 반환할 수도 있다.
  console.log($fruits.firstChild); // #text
  console.log($fruits.lastChild); // #text
  
  // firstElementChild, lastElementChild 프로퍼티는 요소 노드만 반환한다.
  console.log($fruits.firstElementChild); // li.apple
  console.log($fruits.lastElementChild); // li.orange
</script>

 


 

자식 노드 존재 확인

자식 노드가 존재하는지 확인하려면 Node.prototype.hasChildNodes 메서드를 사용한다. 반환값은 불리언 값이다. 이 메서드 또한 텍스트 노드를 포함하여 자식 노드의 존재를 확인한다.

 


 

부모 / 형제 노드 탐색

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

형제 노드 탐삭하려면 다음과 같은 프로퍼티를 사용한다.

 

  • Node.prototype.previousSibling : 부모 노드가 같은 형제 노드 중에서 자신의 이전 형제 노드를 탐색한다. 요소 노드 뿐만아니라 텍스트 노드를 반환할 수도 있다.
  • Node.prototype.nextSibling : 부모 노드가 같은 형제 노드 중에서 자신의 다음 형제 노드를 탐색하여 반환한다. 마찬가지로 요소 노드 뿐만아니라 텍스트 노드를 반환할 수도 있다.
  • Element.prototype.previousElementSibling : 부모 노드가 같은 형제 노드 중에서 자신의 이전 형제 요소 노드만 반환한다.
  • Element.prototype.nextElementSibling : 부모 노드가 같은 형제 노드 중에서 자신의 다음 형제 요소 노드만 반환한다.

 

textContent

Node.prototype.textContent 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티로서 요소 노드의 텍스트와 모든 자손 노드의 텍스트를 모두 취득하거나 변경한다. 이때 HTML 마크업은 무시된다.

 

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

<script>
  console.log(document.getElementById('foo').textContent) // Hello world!
</script>

 

textContent와 유사한 동작을 하는 innerText 프로퍼티가 존재하는데 CSS에 의해 visibility: hidden 으로 지정된 요소 노드의 텍스트를 반환하지 않는다. 또한 CSS를 고려해야 하므로 textContent보다 느려서 사용하지 않는 것이 좋다.

 


 

innerHTML

setter와 getter 모두 존재하는 접근자 프로퍼티로서 요소 노드의 HTML 마크업을 취득하거나 변경한다. 요소 노드의 innerHTML 프로퍼티에 할당한 HTML 마크업 문자열은 렌더링 엔진에 의해 파싱되어 요소 노드의 자식으로 DOM에 반영된다. 이 때 사용자로부터 입력받은 데이터를 그대로 innerHTML에 할당하는 것은 크로스 사이트 스크립팅 공격에 취약하므로 위험하다. 

 

또한 innerHTML에 마크업 문자열을 할당하는 경우 노드의 모든 자식 노드를 제거하고 할당한 HTML 마크업 문자열을 파싱하여 DOM을 변경하기 때문에 효율적이지 않다. 또한 삽입될 위치를 지정할 수 없다. 

 


 

insertAdjacentHTML

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

 

'beforebegin' <div> 'afterbegin' text 'beforeend'</div> 'afterend' 

 

기존 요소에 영향을 주지않고 새롭게 삽입될 요소만을 파싱하여 자식 요소를 추가하므로 innerHTML보다 효율적이고 빠르다. 단, 크로스 사이트 스크립팅 공격에 취약하다는 점은 동일하다.

 


 

복수의 노드 생성과 추가

예를들어 forEach를 통해 3개의 요소노드를 생성하여 DOM에 추가하면 DOM이 3번 변경된다. 이때 리플로우 리페인트가 3번 실행된다. DOM을 변경하는 것은 높은 비용이 드는 처리이므로 가급적 횟수를 줄이는 편이 성능에 유리하다. 그래서 컨테이너 요소 (예를들면 div) 를 하나 만들고 새로 추가될 요소를 컨테이너에 추가하고, 이 컨테이너를 추가할 요소에 추가하면 DOM은 한 번만 변경된다. 하지만 이는 불필요한 컨테이너 요소가 추가되므로 그렇게 바람직 하진 않다.

 

이러한 문제는 DocumentFragment 노드를 통해 해결할 수 있다. 이것은 기존 DOM과는 별도로 존재하고, 별도의 서브 DOM을 구성하여 기존 DOM에 추가하기 위한 용도로 사용한다. Document.prototype.createDocumentFragment 메서드는 비어있는 DocumentFragment를 생성한다.

 

<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. 텍스트노드 생성
    const textNode = document.createTextNode(text)
    
    $li.appenChild(textNode)
    $fragment.appendChild($li)
  })
  
  $fruits.appendChild($fragment)
</script>

 

이렇게 되면 실제 DOM 변경은 1번이고 리플로우와 리페인트도 한 번만 실행된다.

 


 

Node.prototype.insertBefore(newNode, childNode)

첫 번째 인수로 전달받은 노드를 두 번째 인수로 전달받은 노드 앞에 삽입한다. 두 번째 인수로 전달받은 노드는 반드시 insertBefore 메서드를 호출한 노드의 자식 노드이어야 한다. 아니면 DOM Exception 에러가 발생한다. 만약 두 번째 인수가 null 이라면 appendChild 처럼 마지막 자식 노드로 추가된다. 만약 이미 존재하는 노드를 appendChild나 insertBefore를 사용하여 다시 추가하면 현재 위치의 노드를 제거하고 새로운 위치에 노드를 추가한다. 즉 이동이 된다.

 

<body>
  <ul id="frutis">
    <li>Apple</li>
    <li>Banana</li>
  </ul>
</body>

<script>
  const $fruits = document.getElementById('fruits')
  const $li = document.createElement('li')
  
  $li.appendChild(document.createTextNode('Orange'))
  
  // $li 요소를 fruits 요소 노드의 마지막 자식 요소 앞에 삽입
  $fruits.insertBefore($li, $fruits.lastElementChild)
</script>

 


 

Node.prototype.cloneNode([deep: true | false])

노드의 사본을 생성하여 반환한다. 매개변수에 true를 전달하면 깊은 복사하여 모든 자손 노드가 포함된 사본을 생성하고 false를 전달하거나 생략하면 얕은 복사하여 노드 자신만의 사본을 생성한다. 얕은 복사로 생성된 노드는 자손 복사를 하지 않으므로 텍스트 노드도 없다.

 


 

Node.prototype.replaceChild(newChild, oldChild)

자신을 호출한 노드의 자식 노드를 다른 노드로 교체한다. newChild에는 교체할 새로운 노드를 인수로 전달하고, 두 번째 매개변수는 이미 존재하는 교체될 노드를 인수로 전달한다. oldChild 매개변수는 해당 메서드를 호출한 노드의 자식 노드이어야 한다.

 


 

Node.prototype.removeChild(child)

인수로 전달한 노드를 DOM에서 삭제한다. 인수로 전달한 노드는 해당 메서드를 호출한 노드의 자식 노드 이어야 한다.

 


 

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

요소 노드 객체에는 HTML 어트리뷰트에 대응하는 프로퍼티가 존재한다. 이 DOM 프로퍼티들은 HTML 어트리뷰트 값을 초기값으로 가지고 있다. DOM 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티다.  

 

HTML 어트리뷰트의 역할은 HTML 요소의 초기 상태를 지정하는 것이다. 즉, HTML 어트리뷰트 값은 HTML 요소의 초기 상태를 의미하며 이는 변하지 않는다. 변하지 않는다는 의미는 예를들어 DOM 프로퍼티 $input.value = '변경된 값'  으로 변경하여도 $input.getAttribute('value') 를 출력해 보면 초기 세팅했던 값은 변하지 않는다. setAttribute를 해야 변경 된다. 하지만 사용자 입력에 의한 상태 변화와 관계없는 id 어트리뷰트와 id 프로퍼티는 사용자 입력과 관계없이 항상 동일한 값을 유지한다. id 어트리뷰트 값이 변하면 id 프로퍼티 값도 변한다. 이름은 대소문자를 구별하지 않는다. 이에 대응하는 프로퍼티 키는 카멜 케이스를 따른다. maxlength -> maxLength

 

이와 다르게 DOM 프로퍼티는 요소 노드의 최신 상태를 관리한다. 즉, 사용자의 입력에 의한 상태 변화에 반응하여 언제나 최신 상태를 유지한다. 

728x90

'Javascript' 카테고리의 다른 글

디바운스와 쓰로틀  (0) 2020.11.27
이벤트  (0) 2020.11.25
브라우저의 렌더링  (0) 2020.11.25
이터러블  (0) 2020.11.24
String  (0) 2020.11.24

댓글