본문 바로가기
웹 프로그래밍 기초

DOM (2)

by oncerun 2022. 3. 12.
반응형

 

DOM 컬렉션 객체인 HTMLCollection과 NodeList는 DOM API가 여러 개의 결괏값을 반환하기 위한 DOM 컬렉션 객체이다.  모두 유사 배열 객체이면서 이 터러블 하다. 따라서 for.. of문으로 순회할 수 있으며 스프레드 문법의 대상이 되어 

간단히 배열로 변환이 가능하다.

 

두 컬렉션 객체의 중요한 특징은 노드 객체의 상태 변화를 실시간으로 반영하는 live 객체라는 것이다. 

 

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

 

<ul>
 <li class="red">1</li>
 <li class="red">2</li>
 <li class="red">3</li>
<ul>

<script>
const $elems = document.getElementsByClassName('red');
console.log($elems);
HTMLCollection에는 3개의 요소 노드가 담겨있다.

for (let i = 0; i < $elems.length; i++){
	$elems[i].className = 'blue';
}

console.log($elems);
//이후 HTMLCollection에는 1개의 요소 노드가 담겨 있다.
</script>

 

왜 3개가 변경되지 않고 1개가 남아있는지 의문이 든다.  반복문의 코드 블록을 보자.

 

* let은 블록 스코프를 가지기 때문에 코드 블럭이 반복될 때마다 실행 콘텍스트가 생성된다. 

 

첫 번째

 첫 번째 li 요소의 클래스 어트리뷰트 값이 blue로 변경된다. 이때 실시간으로 HTMLCollection 요소에서 제거된다. 

 

두 번째

 첫 번째 반복에서 첫번 째 li요소가 HTMLCollection 요소에서 제거되었다. 따라서 길이가 2로 줄었고 

인덱스 1인 경우에 선택되는 요소는 세 번째 li요소가 선택된다. 

 

세 번째

 이제 컬렉션 내부에는 li의 두 번째 요소 노드만 남아있다. i의 값은 2이다. 그런데 조건문에서 false가 되기 때문에 반복이 종료된다. 따라서 두 번째 li 요소의 class값은 변경되지 않는다.

 

 

for문을 역방향으로 순회하거나 while문으로 컬렉션이 비었을 때까지 반복하는 방법도 있지만 HTMLCollection 객체를 사용하지 않는 방법이 가장 좋다. 

컬렉션 객체를 스프레드 문법을 통해 배열로 변환한 후  진행하면 어떠한 문제도 발생하지 않는다.

 

 

NodeList

 

getElementsByTagName, getElementsByClassName은 live 객체인 HTMLCollection 객체를 반환한다.

하지만 querySelectorAll 메서드는 non-live인 NodeList를 반환한다.

 

다만 childNodes 프로퍼티가 반환하는 NodeList 객체는 HTMLCollection 객체와 같이 실시간으로 노드 객체의 상태 변경을 반영하는 live 객체로 동작하므로 주의가 필요하다.

 

<ul id='fruits'>
<li>Apple</li>
<li>Banana</li>
</ul>


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

//NodeList 반환
const { childNodes} = $fruits;

for (let i = 0; i < childNodes.length; i ++){
	$fruits.removeChild(childNodes[i]);
}

//모든 자식 요소 삭제가 안됨

 

따라서 노드 객체의 상태 변경과 상관없이 안전하게 DOM 컬렉션을 사용하려면 두 컬렉션 객체를 배열로 변환하여 사용하는 것을 권장한다. ( 스프레드 문법이나 Array.from 메서드를 사용하여 쉽게 변환하자)

 

 

* 공백 텍스트 노드

 

HTML 요소 사이의 스페이스, 탭, 줄 바꿈, 등의 white space 문자는 텍스트 노드를 생성한다.  따라서 노드를 탐색할 때는 공백 문자가 생성한 공백 텍스트 노드에 주의해야 한다.

 

우리가 탐색 시 Node의 프로토타입 메서드로 노드를 탐색할 시 (childNodes, firstChild, lastChild, hasChildNodes)를 사용하면 텍스트 노드가 포함되어 의도치 않은 결과를 발생시킨다. 

그래서 보통 Element의 프로토타입 메서드인 ( children,  firstElementsChild, lastElementChild, childElementCount)로 대체하여 사용한다.

 

 

DOM 조작 

 

DOM 조작이란 새로운 노드를 생성하여 DOM에 추가하거나 기존 노드를 삭제 또는 교체하는 것을 말한다. 

이 경우 리플로우와 리페인트가 발생하여 성능에 악영향을 준다. 따라서 DOM 조작은 서능 최적화를 위해 주의해서 다루어야 한다.

 

1. innerHTML

 

Element.prototype.innerHTML 프로퍼티는 setter, getter가 모두 존재하는 접근자 프로퍼티이다. 

 

요소 노드의 HTML 마크업을 취득하거나 변경한다. 요소 노드의 innerHTML 프로퍼티를 참조하면 요소 노드의 콘텐츠 영역 내에 포함된 모든 HTML 마크업을 문자열로 반환한다.

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


<script>

document.getElementById('foo').textContent  // Hello world

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

</script>

 

innerHTML 프로퍼티에 문자열을 할당하면 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열에 포함되어 있는 HTML 마크업이 파싱 되어 요소 노드의 자식 노드로 DOM에 반영된다.

 

다만 사용자의 입력 값을 innerHTML 프로퍼티에 할당하는 것은 XSS 공격에 그대로 노출되기 때문에 위험하다.

 

 

2. insertAdjacentHTML 

innerHTML 프로퍼티는 문자열을 전달받으면 자식노드를 삭제하고 다시 만들어 중간에 요소를 넣는 경우 매우 성능적으로 악영향을 줄 수 있다.

 

insertAdjacentHTML 메서드는 두 번째 인수로 전달한 HTML 마크업 문자열을 파싱하고 그 결과로 생성된 노드를 첫 번째 인수로 전달한 위에 삽입하여 DOM에 반영한다. 

 

첫 번째 인수로 전달할 수 있는 문자열을 "beforebegin", "afterbegin", "beforeend", "afterend"이다.

 

<!--beforebegin -->
<div>
<!--afterbegin -->
text
<!--beforeend -->
</div>
<!--afterend -->

 

 

기존 요소에는 영향을 주지 않고 삽입될 요소만 파싱하여 자식 요소로 추가하기 때문에 innerHTML 보다 효율적이고 빠르다. 단 innerHTML 프로퍼티와 마찬가지로 XXS 공격에 취약하다는 단점은 동일하다.

 

 

 

 

* DocumentFragment 노드

 

DocumentFragment 노드는 문서 , 요소, 어트리뷰트, 텍스트 노드와 같은 노드 객체의 일종으로, 부모 노드가 없어서 기존 DOM과는 별도로 존재한다는 특징이 있다. 

 

DOM요소를 추가할 때 가상 컨테이너에 요소를 추가하고 DOM의 요소에 자식으로 추가하는 경우에 사용할 수 있다.

DOM요소에 직접 반복해서 추가하면 리페인팅과 리플로우가 반복횟수만큼일어나기 때문에 1번으로 줄이기 위한 방법이긴 한데 중간에 불필요한 태그가 들어가 버리기 때문이다.

 

const container = document.createElement('div'); // 대신

const $fragment = document.createDocumentFragment(); //를 사용한다.


['1','2','3'].forEach( text => {

	const $li = document.createElement('li');
    const textNode = document.createTextNode(text);
    
    $li.appendChild(textNode);
    $fragment.appendChild($li);
});

 

DocumentFragment 노드는  DOM에 추가할 시 자신은 제거되고 자신의 자식 노드만 DOM에 추가된다.

 

 

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

 

노드의 사본을 생성하여 반환하다. deep에 true인 경우 자손 노드가 포함된 사본을 생성하고 false인 경우 얕은 복사하여 노드 자신만의 사본을 생성한다. 이 경우 자손 노드가 복사되지 않기에 텍스트 노드가 존재하지 않는다.

 

 

4. HTML 어트리뷰트 조작

 

노드의 attributes 프로퍼티( 요소 노드의 모든 어트리뷰트 노드의 참조가 담긴 NamedNodeMap 객체)는 getter만 존재하는 읽기 전용 접근자 프로퍼티이다.  

 

Element.prototype.getAttribute/setAttribute를 사용하면 attributes 프로퍼티를 거치지 않고 요소 노드에서 메서드를 통해 직접 값을 취득하거나 변경할 수 있다.

  •  hasAttribute() : 어트리뷰트 존재 여부
  •  removeAttribute() : 어트리뷰트 삭제

 

 

4.1 HTML 어트리뷰트와 DOM 프로퍼티

 

요소 노드 객체는 NamedNodeMap 객체 이외에도 DOM 프로퍼티가 존재한다. 이 DOM 프로퍼티들은 HTML 어트리뷰트 값을 초기값으로 가지고 있다. 

DOM 프로퍼티는 setter와 getter가 모두 존재하는 접근자 프로퍼티이다. 따라서 참조와 변경이 매우 자유롭다.

<input id="user" type="text" value="userName">

<script>
const $input = document.getElementById('user');
$input.value = 'changeName';
</script>

아니 근데 왜 변수명에 $를 붙이시는거지?

 

그럼 NamedNodeMap 객체를 나타내는 attributes 프로퍼티와  요소 노드가 가지고 있는 DOM 프로퍼티들로 중복되어 관리되고 있는 것처럼 보인다.

이를 구별하기 위해 각각의 역할을 이해해야 한다.

 

HTML 어프리튜브의 역할은 HTML 요소의 초기 상태를 지정하는 것이다. 즉 HTML 어트리뷰트 값은 HTML 요소의 초기 상태를 의미하며 이는 변하지 않는다.

 

렌더 트리가 확정되고 레이아웃 과정을 지나 각 요소들이 렌더링 될 때 표시될 초기값을 지정한다. 이러한 초기값들은 attributes 프로퍼티에 저장된다. 이와 별도로 요소의 프로퍼티 값은 요소 노드의 DOM 프로퍼티에 할당된다. 

 

따라서 요소 노드가 생성되어 첫 렌더링이 끝난 시점까지 어트리뷰트 노드의 어트리뷰트 값과 요소 노드의 DOM 프로퍼티 값은 동일하다.

 

하지만 만약 사용자가 input, textarea의 요소에 값을 입력하기 시작하면 상황이 달라진다.

 

요소 노드는 상태를 가지고 있다. 예를 들어 input요소는  사용자가 입력 필드에 입력한 값을 상태로 가지고 있으며, checkbox 요소는 사용자가 체크 여부를 상태로 가지고 있다.

 

요소 노드는 첫 렌더링 시 가지고 있어야 할 초기 상태가 존재해야 하고 사용자가 입력한 최신 상태, 즉 2개의 상태를 관리해야 한다. 

요소 노드의 초기 상태는 어트리뷰트 노드가 관리하며, 최신 상태는 DOM 프로퍼티가 관리한다.

 

 

5. 인라인 스타일 조작

 

HTMLElement.prototype.style 프로퍼티는 getter, setter 모두 존재하는 접근자 프로퍼티로 요소 노드의 인라인 스타일을 취득하거나 변경한다.

 

style 프로퍼티를 참조하면 CSSStyleDeclaration 타입의 객체를 반환한다. 이는 다양한 CSS 프로퍼티에 대응하는 프로퍼티를 가지고 있으며 값을 할당하면 CSS 프로퍼티가 인라인 스타일로 HTML 요소에 추가되거나 변경된다.

 

CSS 프로퍼티는 케밥 케이스를 따르고 CSSStyleDeclaration 객체의 프로퍼티는 카멜 케이스를 따른다.

따라서 backgroud-color, z-index 등은 모두 카멜 케이스로 변환 후 접근해야 한다.

단위 지정이 필요한 CSS 프로퍼티의 값은 반드시 단위를 지정해야 한다. 생략 시 적용되지 않는다.

 

6. 클래스 조작

 

자바스크립트에서 class는 예약어다. 따라서 class 어트리뷰트에 대응하는 DOM 프로퍼티는 className과 classList이다.

 

className은 문자열을 반환하기에 자주 사용되지 않고 DOMTokenList 객체를 반환하는 classList를 자주 사용하는 걸로 알고 있다.  DOMTokenList는 유사 배열 객체이면서 이터러블이기에 조작이 간단하며 별도의 유용한 메서드를 가지고 있다. (add, remove, item, contains, replace, toggle, forEach, entries, keys, values, supports..) 등등

 

 

 

반응형

'웹 프로그래밍 기초' 카테고리의 다른 글

Grid  (0) 2023.04.10
[CSS] Cascade, specificity, inheritance  (1) 2023.04.10
DOM (1)  (0) 2022.03.12
브라우저의 렌더링 과정  (0) 2022.03.11
[css] z-index  (0) 2022.03.11

댓글