DOM

seeen·2023년 2월 2일
0
post-thumbnail

이 글은 '이웅모'님의 '모던 자바스크립트 Deep Dive' 책을 통해 공부한 내용을 정리한 글입니다. 저작권 보호를 위해 책의 내용은 요약되었습니다.

DOM (Document Object Model)

브라우저의 렌더링 엔진은 웹 문서를 로드한 후, 파싱하여 웹 문서를 브라우저가 이해할 수 있는 구조로 구성하여 메모리에 적재하는데 이를 DOM이라 한다.

즉, 모든 요소와 요소의 어트리뷰트, 텍스트를 각각의 객체로 만들고 이 객체들간 관계를 표현할 수 있는 트리 구조로 구성한 것이 DOM이다. 자바스크립트로 DOM을 동적으로 제어할 수 있으며 변경된 DOM은 렌더링에 반영된다.

DOM Tree

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .active {
        color: red;
      }
      .non-active {
        color: blue;
      }
    </style>
  </head>
  <body>
    <h1>Tree</h1>
    <section>
      <div>
        <h2 class="active">A</h2>
        <h2 class="non-active">B</h2>
        <h2 class="non-active">C</h2>
      </div>
    </section>
  </body>
</html>

위 코드를 DOM Tree 그림으로 나타내면 다음과 같다.

DOM에서 모든 요소, 어트리뷰트, 텍스트는 하나의 객체이며 Document 객체의 자식이다. 요소의 중첩관계는 객체의 트리로 구조화하여 부자관계를 표현한다. DOM Tree는 다음과 같이 네 종류로 구분할 수 있다.

  • 문서노드 (Document Node) : 트리의 최상위에 존재, DOM Tree의 시작점
  • 요소노드 (Element Node) : HTML 요소를 표현, 어트리뷰트, 텍스트 노드에 접근하려면 이 노드에 먼저 접근해야한다.
  • 어트리뷰트노드 (Attribute Node) : HTML 요소의 어트리뷰트를 표현, 요소의 자식이 아닌 요소의 일부로 표현된다.
  • 텍스트노드 (Text Node) : HTML 요소의 텍스트를 표현, 요소의 자식이며 자식 노드를 가질 수 없다. DOM Tree의 종착점

DOM API

자바스크립트 등의 프로그래밍 언어가 DOM에 접근하고 수정할 수 있는 방법을 제공하는데 일반적으로 프로퍼티와 메서드를 갖는 자바스크립트 객체로 제공된다. 이를 DOM API라 부른다.

DOM Query

하나의 요소를 선택하는 메서드의 이름은 보통 Element로, 복수의 요소를 선택하는 메서드의 이름은 보통 Elements로 지어져있다.

document.getElementById

  • 기능 : id 어트리뷰트 값으로 요소 노드를 한 개만 선택 (복수인 경우 첫 번째 요소만)
  • 반환 : HTMLElement를 상속받는 객체
// id가 hello인 요소를 선택
const el = document.getElementById('hello');

document.querySelector

  • 기능 : CSS 셀렉터를 통해 요소 노드 한 개만 선택 (복수인 경우 첫 번째 요소만)
  • 반환 : HTMLElement를 상속받는 객체
  • 주의 : IE8 이상에서만 동작
// section -> div -> li 요소 중 class가 active인 요소를 선택
const el = document.querySelector('section div li.active');

document.getElementsByTagName

  • 기능 : 태그명으로 요소 노드를 모두 선택
  • 반환 : HTMLCollection (*live) (유사배열)
  • 주의 : 실시간 노드 상태 변경
<div>
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
// 태그 네임이 h2인 요소들을 복수 선택 -> 유사배열의 형태로 반환
const el = document.getElementsByTagName('h2');
// HTMLCollection(3) [h2.active, h2.non-active, h2.non-active]
// 0: h2.active
// 1: h2.non-active
// 2: h2.non-active
// length: 3

[...el].forEach(el => el.className = 'hello');

document.getElementsByClassName

  • 기능 : class 어트리뷰트 값으로 요소 노드를 모두 선택
  • 반환 : HTMLCollection (*live) (유사배열)
  • 주의 : 실시간 노드 상태 변경
<div>
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const el = document.getElementsByClassName('non-active');
// HTMLCollection(2) [h2.non-active, h2.non-active]
// 0: h2.non-active
// 1: h2.non-active
// length: 2

[...el].forEach(el => el.className = 'active');

document.querySelectorAll

  • 기능 : 지정된 CSS 선택자를 사용하여 요소 노드를 모두 선택
  • 반환 : NodeList (non-live)
  • 주의 : IE8 이상에서만 동작
<div>
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const el = document.querySelectorAll('.non-active');
// NodeList(2) [h2.non-active, h2.non-active]
// 0: h2.non-active
// 1: h2.non-active
// length: 2

[...el].forEach(el => el.className = 'active');

*live 특징

getElementsByTagNamegetElementsByClassName 은 유사배열의 형태로 값을 반환하고 실시간으로 노드의 상태 변경을 반영한다. 따라서 다음과 같은 코드는 의도한대로 어트리뷰트 값 변경이 되지 않는다.

<div class="a"></div>
<div class="a"></div>
<div class="a"></div>
const el = document.getElementsByClassName('a');
// el
// HTMLCollection(3) [div.a, div.a, div.a]
// 0: div.a
// 1: div.a
// 2: div.a
// length: 3


for (let i = 0; i < elems.length; i++) {
  el[i].className = 'b';
}
// 의도한 값
// <div class="b"></div>
// <div class="b"></div>
// <div class="b"></div>

// 실제로 반영된 값
// <div class="b"></div>
// <div class="a"></div>
// <div class="b"></div>

위 코드를 보면 의도한 값과 달리 실제로 반영된 값이 다른 것을 확인할 수 있다. 그 이유는 다음과 같다. 코드의 동작 순서를 보자.

  1. el.length === 3 이므로 3번의 반복문이 실행될 예정이다.
  2. i === 0 일 때, 첫 요소 div.a의 class 어트리뷰트 값이 a에서 b로 변경된다. 이때 el 은 실시간으로 노드의 상태 변경을 반영하는 HTMLCollection 객체이므로 getElementsByClassName 메서드 인자로 지정한 조건과 더 이상 부합하지 않아 반환값에서 실시간으로 제거된다. 즉, el.length === 2 가 된다.
  3. i === 1 일 때, 첫 요소가 제거되어 el[1]은 세 번째 요소의 div.a의 class 어트리뷰트 값이 a에서 b로 변경된다. 이 역시 HTMLCollection 에서 제거된다.
  4. i === 2 일 때, 두 번째 요소만 남아 있지만, el.length === 1 이므로 반복문을 종료하게 된다. 따라서 두 번째 요소의 class 어트리뷰트 값은 변경되지 않는다.

위와 같은 특성이 있기에, 반복문을 사용할 경우 주의가 필요하다. 위 문제는 아래와 같은 방법으로 해결할 수 있다.

  • 반복문을 역방향으로
  • index를 0으로 고정 후 length 값을 조건으로 무한 반복
  • HTMLCollection을 배열로 변경
  • querySelectorAll 메서드를 사용하여 non-live인 NodeList를 반환

DOM Traversing

parentNode

  • 기능 : 부모 노드 탐색
  • 반환 : HTMLElement를 상속받는 객체
<div>
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const el = document.querySelector('.active');

console.log(el.parentNode);
// <div>
//   <h2 class="active">A</h2>
//   <h2 class="non-active">B</h2>
//   <h2 class="non-active">C</h2>
// </div>

firstChild, lastChild

  • 기능 : 자식 노드 탐색
  • 반환 : HTMLElement를 상속받는 객체
  • 주의 : IE9 이상에서 동작, IE 제외한 대부분의 브라우저들은 요소 사이 공백 및 줄바꿈 문자를 텍스트 노드로 취급
<div>
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');

console.log(el.firstChild); // #text (#text.data = "\n        ")
console.log(el.lastChild); // #text (#text.data = "\n        ")

firstElementChild, lastElementChild

  • 기능 : 자식 노드 탐색 (firstChild, lastChild 대안)
  • 반환 : HTMLElement를 상속받는 객체
  • 주의 : IE9 이상에서 동작
<div>
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');

console.log(el.firstElementChild); // <h2 class="active">A</h2>
console.log(el.lastElementChild); // <h2 class="non-active">C</h2>

hasChildNodes()

  • 기능 : 자식 노드 유무 파악
  • 반환 : Boolean 값
<div>
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');

console.log(el.hasChildNodes()); // true

childNodes

  • 기능 : 자식 노드 컬렉션 반환 (텍스트 요소 포함한 모든 자식 요소)
  • 반환 : NodeList (non-live)
<div>
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');

console.log(el.childNodes);
// NodeList(7) [text, h2.active, text, h2.non-active, text, h2.non-active, text]
// 0: text
// 1: h2.active
// 2: text
// 3: h2.non-active
// 4: text
// 5: h2.non-active
// 6: text
// length: 7

children

  • 기능 : 자식 노드의 컬렉션 반환 (Element type 요소)
  • 반환 : HTMLCollection (live)
  • 주의 : IE9 이상에서만 동작
<div>
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');

console.log(el.children);
// HTMLCollection(3) [h2.active, h2.non-active, h2.non-active]
// 0: h2.active
// 1: h2.non-active
// 2: h2.non-ac
// length: 3

previousSibling, nextSibling

  • 기능 : 형제 노드 탐색 (텍스트 요소 포함한 모든 형제 노드)
  • 반환 : HTMLElement를 상속받는 객체

previousElementSibling, nextElementSibling

  • 기능 : 형제 노드 탐색 (Element type 요소)
  • 반환 : HTMLElement를 상속받는 객체
  • 주의 : IE9 이상에서만 동작

DOM Manipulation

nodeValue

  • 기능 : 노드 값을 반환
  • 반환 : 텍스트 노드의 경우는 문자열, 요소 노드의 경우 null
  • 주의 : IE6 이상에서만 동작
  • 기타 : nodeName, nodeType
<div>
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const el = document.querySelector('.active');

console.log(el.nodeValue); // null
console.log(el.nodeName); // H2
console.log(el.nodeType); // 1 (element)

const elText = el.firstChild;

console.log(elText.nodeValue); // A
console.log(elText.nodeName); // #text
console.log(elText.nodeType); // 3 (text)

className

  • 기능 : class 어트리뷰트 값 취득 또는 변경
  • 반환 : 공백으로 구분된 문자열 또는 빈 문자열
<div class="abc def">
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');

console.log(el.className); // abc def
console.log(el.className.split(' '); // ['abc', 'def']

classList

  • 기능 : class 어트리뷰트 값 취득 또는 변경
  • 반환 : DOMTokenList
  • 기타 : add, remove, item, toggle, contains, replace 메소드를 제공
<div class="abc def">
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');

console.log(el.classList);
// DOMTokenList(2) ['abc', 'def', value: 'abc def']
// 0: "abc"
// 1: "def"
// length: 2
// value: "abc def"

id

  • 기능 : id 어트리뷰트 값 취득 또는 변경
  • 반환 : 문자열

hasAttribute(attribute)

  • 기능 : 지정한 어트리뷰트를 가지고 있는지 검사
  • 반환 : Boolean 값
  • 주의 : IE8 이상에서만 동작

getAttribute(attribute), setAttribute(attribute, value), removeAttribute(attribute)

  • 기능 : 지정한 어트리뷰트 값을 취득, 설정, 제거
  • 반환 : 문자열 | undefined | undefined

HTML Manipulation

마크업 콘텐츠를 추가하는 행위는 XSS(Cross-Site Scripting Attacks)에 위챡하므로 주의가 필요하다.

textContent

  • 기능 : 요소의 텍스트 콘텐츠를 취득 또는 변경, 마크업은 무시
  • 반환 : 마크업을 제외한 텍스트 문자열
<div class="abc def">
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');
console.log(el.textContent);
//
//      A
//      B
//      C
//    

const el = document.querySelector('h2.active');
console.log(el.textContent); // A

innerText

  • 기능 : 요소의 텍스트 콘텐츠에만 접근
  • 반환 : 텍스트 문자열
  • 주의 : 비표준, CSS에 순종적(visibility:hidden 값 있을 시 텍스트 반환 X), 속도 상대적으로 느림
<div class="abc def">
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');
console.log(el.innerText);
// A
// B
// C   

const el = document.querySelector('h2.active');
console.log(el.innerText); // A

innerHTML

  • 기능 : 해당 요소의 모든 자식 요소를 포함하는 모든 콘텐츠를 하나의 문자열로 취득, 마크업 포함
  • 반환 : 마크업이 포함된 텍스트 문자열
<div class="abc def">
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const el = document.querySelector('div');
console.log(el.innerHTML);
//
//      <h2 class="active">A</h2>
//      <h2 class="non-active">B</h2>
//      <h2 class="non-active">C</h2>
//    

const el = document.querySelector('h2.active');
console.log(el.innerHTML); // A

// 마크업이 포함된 컨텐츠를 직접 추가하는 것은 XSS에 취약
el.innerHTML = '';
el.innerHTML += "<script>alert('XSS');</script>";

innerHTML 대체 - DOM 직접 조작

createElement(tagName)

  • 기능 : 태그이름을 인자로 전달하여 요소를 생성
  • 반환 : HTMLElement를 상속받는 객체

createTextNode(text)

  • 기능 : 텍스트를 인자로 전달하여 텍스트 노드를 생성
  • 반환 : Text 객체

appendChild(node)

  • 기능 : 인자로 전달한 노드를 마지막 자식 요소로 DOM Tree에 추가
  • 반환 : 추가한 노드

removeChild(node)

  • 기능 : 인자로 전달한 노드를 DOM Tree에서 제거
  • 반환 : 제거한 노드

조작 순서는 다음과 같다.

  1. createElement(tagName) 메서드로 새로운 요소 노드 생성
  2. createTextNode(text) 메서드로 새로운 텍스트 노드 생성, 이 과정을 생략한다면 콘텐츠가 비어 있는 요소가 된다.
  3. 생성된 요소를 appendChild(node) 메서드를 사용하여 DOM Tree에 추가
  4. removeChild(node) 메서드르 사용하여 DOM Tree에서 제거 가능
<div class="abc def">
  <h2 class="active">A</h2>
  <h2 class="non-active">B</h2>
  <h2 class="non-active">C</h2>
</div>
const newEl = document.createElement('h2'); // <h2></h2>
const newText = document.createTextNode('D'); // "D"
const parentEl = document.querySelector('div');

newEl.appendChild(newText);
parentEl.appendChild(newEl);
// parentEl.removeChild(newEl);

innerHTML 대체 - insertAdjacentHTML(position, string)

  • 기능 : 인자로 전달한 텍스트를 HTML로 파싱하고 그 결과로 생성된 노드를 DOM 트리의 지정된 위치에 삽입
  • 반환 : undefined
  • 인자 : 첫번째 인자는 삽입 위치, 두번째 인자는 삽입할 요소를 표현한 문자열
  • position 값
    • beforebegin
    • afterbegin
    • beforeend
    • afterend

const el = document.querySelector('h2.active');
el.insertAdjacentHTML('beforeend', '<p>is from Korea</p>')

innerHTML vs DOM 직접 조작 vs insertAdjacentHTML()

innerHTMLinsertAdjacentHTML()XSS에 취약하다. 텍스트를 추가 또는 변경시에는 textContent, 새로운 요소의 추가 또는 삭제시에는 DOM 직접 조작 방식을 사용할 것을 권장한다.

Style

style 프로퍼티를 사용하면 inline 스타일 선언을 생성할 수 있다.

const el = document.querySelector('h2.active');

el.style.color = 'yellow';

window.getComputedStyle

  • 기능 : style 프로퍼티 값 취득
  • 반환 : CSS 프로퍼티 값
  • 인자 : 첫번째 인자는 속성값을 얻으려하는 Element, 두번째 인자는 일치시킬 의사요소 (보통 null)
const el = document.querySelector('h2.active');

el.style.color = 'yellow';
console.log(window.getComputedStyle(el, null).getPropertyValue('color')); // rgb(255, 255, 0)
profile
woowacourse FE 5th, depromeet Web 15th

0개의 댓글