Languages/JavaScript

[HUFS/GnuVil] #19 DOM

성중 2022. 11. 11. 15:11

DOMHTML 문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API, 즉 프로퍼티와 메서드를 제공하는 트리 자료구조

 

노드

HTML 문서를 구성하는 개별적인 요소인 HTML 요소(element)는 렌더링 엔진에 의해 파싱되어 DOM을 구성하는 노드 객체로 변환된다. 이 때 어트리뷰트는 어트리뷰트 노드로, 텍스트 콘텐츠는 텍스트 노드로 변환된다

 

HTML 요소와 노드 객체

노드들은 계층 구조로, 비선형 자료구조인 트리 자료구조를 이룬다

 

트리 자료구조 (루트 노드, 리프 노드)

 

HTML 문서를 파싱한 DOM

DOM을 구성하는 노드 객체는 표준 빌트인 객체가 아닌 브라우저 환경에서 추가적으로 제공되는 호스트 객체이며, 프로토타입에 의해 다음과 같은 상속 구조를 갖는다

 

노드 객체의 상속 구조

DOM API는 이러한 노드 타입에 따라 필요한 기능을 DOM API로 제공하고, 이를 통해 HTML의 구조나 내용, 스타일 등을 동적으로 조작할 수 있다

 

input 요소 노드 객체의 프로토타입 체인

요소 노드 취득

  1. id를 이용한 요소 노드 취득: getElementByld
  2. 태그 이름을 이용한 요소 노드 취득: getElementsByTagName
  3. class를 이용한 요소 노드 취득: getElementsByClassName
  4. CSS 선택자를 이용한 요소 노드 취득: queryselector, querySelectorAll
  5. 특정 요소 노드를 취득할 수 있는지 확인: matches

이 때, 여러 요소 노드가 반환된다면 DOM 컬렉션 객체로 반환되는데, for…of문으로 순회할 수 있으며 스프레드 문법을 사용해 배열로 변환해 사용할 수 있다

 

노드 탐색

취득한 요소 노드를 기점으로 DOM 트리 상의 부모/형제/자식 노드 등을 탐색할 수 있다

 

트리 노드 탐색 프로퍼티
자식 노드 탐색
형제 노드 탐색

요소 노드의 텍스트 조작

요소 노드의 텍스트를 조작하기위해 nodeValue / textContent / innerText 프로퍼티를 사용할 수 있는데, 다음과 같은 이유로 textContent를 사용하는 것이 좋다

  • nodeValue 프로퍼티는 텍스트 노드가 아닌 객체에 대해서 null을 반환하며 더 복잡하다
  • innerText 프로퍼티는 CSS에 의해 비표시(visibility: hidden)된 텍스트는 반환하지 않는다
  • innerText 프로퍼티는 CSS도 고려하므로 textContent 프로퍼티보다 속도가 느리다

 

DOM 조작

<!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 마크업을 문자열로 반환한다

 

<!DOCTYPE html>
<html>
  <body>
    <div id="foo">Hello</div>
  </body>
  <script>
    // 에러 이벤트를 강제로 발생시켜서 자바스크립트 코드가 실행되도록 한다.
    document.getElementById('foo').innerHTML
      = `<img src="x" onerror="alert(document.cookie)">`;
  </script>
</html>

이를 활용하면 마크업 문자열로 간단히 DOM 조작을 할 수 있지만 입력 받은 데이터를 그대로 innerHTML 프로퍼티에 할당하는 것은 크로스 사이트 스크립팅 (XSS) 공격에 취약하다. HTML5부터는 마크업 내의 script 태그 실행은 방지되지만 위와 같이 악성 코드가 실행될 수 있는 방법은 남아있기 때문이다

 

<!DOCTYPE html>
<html>
  <body>
    <!-- beforebegin -->
    <div id="foo">
      <!-- afterbegin -->
      text
      <!-- 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>

또한, innerHTML을 덮어 씌우는 것은 삽입 위치를 조정할 수 없으며 기존의 자식 노드까지 모두 제거하고 다시 처음부터 새롭게 자식 노드를 생성해 DOM에 반영하기 때문에 비효율적이다. 이를 방지하기 위해 다음과 같이 insertAdjacentHTML 메서드를 사용하는 것이 좋다

 

<!DOCTYPE html>
<html>
  <body>
    <!-- beforebegin -->
    <div id="foo">
      <!-- afterbegin -->
      text
      <!-- 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>

HTML 마크업 문자열을 파싱해 노드를 생성하는 방식이 아닌, 다음과 같이 DOM에서 노드를 직접 생성/삽입/삭제/치환하는 메서드도 제공된다

 

어트리뷰트

HTML 요소는 다음과 같이 여러 개의 어트리뷰트(속성)을 가질 수 있다

 

<input id="user" type="text" value="ungmo2">
  • 글로벌 어트리뷰트: id, class, style, title, lang, tabindex, draggable, hidden 등
  • 이벤트 핸들러 어트리뷰트: onclick, onchange, onfocus, onblur, oninput, onkeypress, onkeydown, onkeyup, onmouseover, onsubmit, onload 등
  • 특정 HTML 요소에만 사용할 수 있는 어트리뷰트: type, value, checked 등

 

<!DOCTYPE html>
<html>
<body>
  <input id="user" type="text" value="ungmo2">
  <script>
    const $input = document.getElementById('user');

    // value 어트리뷰트 값을 취득
    const inputValue = $input.getAttribute('value');
    console.log(inputValue); // ungmo2

    // value 어트리뷰트 값을 변경
    $input.setAttribute('value', 'foo');
    console.log($input.getAttribute('value')); // foo
  </script>
</body>
</html>

getAttributesetAttribute로 어트리뷰트 값을 가져오거나 조작할 수 있다

 

<!DOCTYPE html>
<html>
<body>
  <input id="user" type="text" value="ungmo2">
  <script>
    const $input = document.getElementById('user');

    // 요소 노드의 value 프로퍼티 값을 변경
    $input.value = 'foo';

    // 요소 노드의 value 프로퍼티 값을 참조
    console.log($input.value); // foo
  </script>
</body>
</html>

DOM 프로퍼티는 HTML 어트리뷰트를 초기값으로 가지며 참조와 변경이 가능하다

 

<!DOCTYPE html>
<html>
<body>
  <input id="user" type="text" value="ungmo2">
  <script>
    const $input = document.getElementById('user');

    // DOM 프로퍼티에 값을 할당하여 HTML 요소의 최신 상태를 변경한다.
    $input.value = 'foo';
    console.log($input.value); // foo

    // getAttribute 메서드로 취득한 HTML 어트리뷰트 값, 즉 초기 상태 값은 변하지 않고 유지된다.
    console.log($input.getAttribute('value')); // ungmo2
  </script>
</body>
</html>

HTML 어트리뷰트는 DOM 프로퍼티로 중복 관리되고 있는 것처럼 보이지만 HTML 어트리뷰트는 페이지를 새로고침하면 보여줄 요소 노드의 초기 상태를 관리하고 DOM 프로퍼티는 사용자 입력에 의해 변경된 최신 상태를 관리한다. 즉 setAttribute 메서드로 인한 변경은 아예 초기 상태 값을 변경한다

 

<!DOCTYPE html>
<html>
<body>
  <ul class="users">
    <li id="1" data-user-id="7621" data-role="admin">Lee</li>
    <li id="2" data-user-id="9524" data-role="subscriber">Kim</li>
  </ul>
</body>
</html>

data- 접두사를 붙여 사용자 정의 data 어트리뷰트를 지정할 수 있다

 

<!DOCTYPE html>
<html>
<body>
  <ul class="users">
    <li id="1" data-user-id="7621" data-role="admin">Lee</li>
    <li id="2" data-user-id="9524" data-role="subscriber">Kim</li>
  </ul>
  <script>
    const users = [...document.querySelector('.users').children];

    // user-id가 '7621'인 요소 노드를 취득한다.
    const user = users.find(user => user.dataset.userId === '7621');
    // user-id가 '7621'인 요소 노드에서 data-role의 값을 취득한다.
    console.log(user.dataset.role); // "admin"

    // user-id가 '7621'인 요소 노드의 data-role 값을 변경한다.
    user.dataset.role = 'subscriber';
    // dataset 프로퍼티는 DOMStringMap 객체를 반환한다.
    console.log(user.dataset); // DOMStringMap {userId: "7621", role: "subscriber"}
  </script>
</body>
</html>

data 어트리뷰트의 값은 dataset 프로퍼티로 취득/추가/변경할 수 있다

* 이 때 변수명은 카멜케이스에서 케밥케이스로 자동 변환

 

스타일

<!DOCTYPE html>
<html>
<body>
  <div style="color: red">Hello World</div>
  <script>
    const $div = document.querySelector('div');

    // 인라인 스타일 취득
    console.log($div.style); // CSSStyleDeclaration { 0: "color", ... }

    // 인라인 스타일 변경
    $div.style.color = 'blue';

    // 인라인 스타일 추가
    $div.style.width = '100px';
    $div.style.height = '100px';
    $div.style.backgroundColor = 'yellow';
  </script>
</body>
</html>

style 프로퍼티로 요소 노드의 인라인 스타일을 취득/추가/변경할 수 있다

 

<!DOCTYPE html>
<html>
<head>
  <style>
    .box {
      width: 100px; height: 100px;
      background-color: antiquewhite;
    }
    .red { color: red; }
    .blue { color: blue; }
  </style>
</head>
<body>
  <div class="box red">Hello World</div>
  <script>
    const $box = document.querySelector('.box');

    // .box 요소의 class 어트리뷰트 정보를 담은 DOMTokenList 객체를 취득
    // classList가 반환하는 DOMTokenList 객체는 HTMLCollection과 NodeList와 같이
    // 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는(live) 객체다.
    console.log($box.classList);
    // DOMTokenList(2) [length: 2, value: "box blue", 0: "box", 1: "blue"]

    // .box 요소의 class 어트리뷰트 값 중에서 'red'만 'blue'로 변경
    $box.classList.replace('red', 'blue');
  </script>
</body>
</html>

class 어트리뷰트를 조작할 때는 classList를 사용해주자

* add / remove / item / contains / replace / toggle 등의 메서드 사용

 

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      color: red;
    }
    .box {
      width: 100px;
      height: 50px;
      background-color: cornsilk;
      border: 1px solid black;
    }
  </style>
</head>
<body>
  <div class="box">Box</div>
  <script>
    const $box = document.querySelector('.box');

    // .box 요소에 적용된 모든 CSS 스타일을 담고 있는 CSSStyleDeclaration 객체를 취득
    const computedStyle = window.getComputedStyle($box);
    console.log(computedStyle); // CSSStyleDeclaration

    // 임베딩 스타일
    console.log(computedStyle.width); // 100px
    console.log(computedStyle.height); // 50px
    console.log(computedStyle.backgroundColor); // rgb(255, 248, 220)
    console.log(computedStyle.border); // 1px solid rgb(0, 0, 0)

    // 상속 스타일(body -> .box)
    console.log(computedStyle.color); // rgb(255, 0, 0)

    // 기본 스타일
    console.log(computedStyle.display); // block
  </script>
</body>
</html>

인라인 스타일만 반환하는 style 프로퍼티와 달리 getComputedStyle 메서드는 요소 노드에 적용된 모든 CSS 스타일을 참조해 가져온다

 

 내용은 위키북스의 '모던 자바스크립트 Deep Dive' 바탕으로 작성되었습니다.