DOM

렌더링 엔진에 의해 HTML 문서를 브라우저가 이해할 수 있는 자료구조인 DOM을 생성한다. DOM이란, HTML문서의 계층적인 구조와 정보를 표현하며 이를 제어할 수 있는 API, 즉 프로퍼티와 메서드를 제공하는 트리 자료구조이다.

노드

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

노드 객체들로 구성된 트리 자료구조를 DOM이라한다.

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

주의사항

상속관계를 아는 것보다 어떤 DOM API를 사용하여 동적으로 변경하고 조작할 수 있는지를 알아야한다.

querySelector, querySelectorAll 메서드가 다소 느리긴 하더라도 CSS 선택자로 요소 노드 취득시 구체적인 조건과 일관된 방식으로 요소 노드 취득할 수 있으므로 id 어트리뷰트가 있는 요소는 getElementById 메서드를 사용하고 그 외의 경우에는 querySelector, querySelectorAll 메서드를 사용하자.

HTMLCollection과 NodeList

DOM 컬렉션 객체이 두 객체는 DOM API가 여러 개의 결과값을 반환하기 위한 객체이다. 둘 다 유사 배열 객체이면서 이터러블이다. 그러므로 for…of문으로 순회할 수 있으며 스프레드 문법을 사용하여 간단히 배열로 변환할 수 있다.

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

결론

HTMLCollection, NodeList 객체를 사용하지 말아라. 유사 배열 객체이면서 이터러블인 NodeList, HTMLCollection 객체를 배열로 변환하면 부작용을 제거할 수 있다. 유용한 배열 고차함수 forEach, map, filter 등을 사용할 수 있다.

DOM 조작

DOM 조작은 새로운 노드를 생성하여 DOM에 추가하거나 기존 노드 삭제 또는 교체 하는 것을 말한다. 이 경우 리플로우와 리페인트가 발생한다.

innerHTML

시작 태그와 종료 태그 사이의 모든 마크업을 문자열로 반환한다. HTML 마크업도 포함된 문자열을 반환하는 것이 textContent 프로퍼티와 차이점이다.

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello <span>world!</span></div>
</body>
<script>
// #foo 요소의 콘텐츠 영역 내의 HTML 마크업을 문자열로 취득한다.
console.log(document.getElementById('foo').innerHTML);
// "Hello <span>world!</span>"
</script>
</html>
  • 요소 노드의 innerHTML 프로퍼티에 문자열 할당하면 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가된다. 이 때 HTML 마크업이 포함되어 있으면 파싱되어 요소 노드의 자식 노드로 DOM에 반영된다.

복수의 노드 생성과 추가 ⭐️

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<body>
<ul id="fruits"></ul>
</body>
<script>
const $fruits = document.getElementById('fruits');

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

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

// 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
$li.appendChild(textNode);

// 4. $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
$fruits.appendChild($li);
});
</script>
</html>
  • 위 예제는 3개의 요소 노드가 생성하여 DOM에 3번 추가하여 DOM이 3번 변경된다. 이러한 방법은 리플로우를 많이 발생 시키므로 피해야한다.

⇒ 컨테이너 요소를 미리 생성한 후 DOM에 추가할 3개 요소 노드를 컨테이너 요소의 자식 노드로 추가한 뒤 컨테이너 요소를 #fruits 요소에 자식으로 추가한다. 하지만 이 또한, 불필요한 컨테이너 요소(div)가 DOM에 추가되어 바람직 하지 않다.

어트리뷰트

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

HTML 문서의 요소는 여러 개의 어트리뷰트를 가질 수 있다. ex) class, checked, aria-label…

  • 모든 HTML 요소에 공통적인 것부터 해당 요소만 사용할 수 있는 어트리뷰트가 있다.
  • HTML 요소의 어트리뷰트는 어트리뷰트 노드로 변환되어 요소 노드와 연결된다. 이는 NamedNodeMap 객체에 담겨 요소 노드의 attributes 프로퍼티에 저장된다.
  • attributes 프로퍼티는 getter만 존재하는 접근자 프로퍼티이다.

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

  • HTML 어트리뷰트는 초기값 (변하지 않는다.)
  • DOM 프로퍼티는 HTML 프로퍼티를 초기값으로 가지고 변경될 수 있다.

첫렌더링 까지 어트리뷰트 노드의 어트리뷰트 값과 요소 노드의 value 프로퍼티에 할당된 값은 HTML 어트리뷰트 값과 동일하다. 하지만 첫 렌더링 이후 사용자가 input 요소에 무언가 입력시 바뀌게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<body>
<input id="user" type="text" value="ungmo2">
<script>
const $input = document.getElementById('user');

// attributes 프로퍼티에 저장된 value 어트리뷰트 값
console.log($input.getAttribute('value')); // ungmo2

// 요소 노드의 value 프로퍼티에 저장된 value 어트리뷰트 값
console.log($input.value); // ungmo2
</script>
</body>
</html>
  • input 요소 노드는 상태를 가지고 있고 사용자의 입력에 의한 변경된 최신 상태를 관리해야 할뿐더러 HTML 어트리뷰트로 지정한 초기 상태도 관리해야한다.

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

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

대부분은 HTML 어트리뷰트는 HTML 어트리뷰트 이름과 동일한 DOM 프로퍼티와 1:1로 대응한다. 반드시는 아니다.

  • id 어트리뷰트와 id 프로퍼티는 1:1 대응, 동일한 값으로 연동한다.
  • input 요소의 value 어트리뷰트는 value 프로퍼티와 1:1 대응, value 어트리뷰트는 초기상태를, value 프로퍼티는 최신 상태를 갖는다.
  • class 어트리뷰트에 대응하는 DOM 프로퍼티는 className, classList 프로퍼티이다.

DOM 프로퍼티 값의 타입

getAttribute 메서드로 취득한 어트리뷰트 값은 언제나 문자열이지만 DOM 프로퍼티로 취득한 최신 상태 값은 문자열이 아닐 수 있다. boolean 타입일 수 있다.

ex) checkbox요소의 checked 어트리뷰트 값은 문자열 이지만 checked 프로퍼티 값은 불리언이다.