DOM 39

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

노드

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

요소 노드 취득

1. id를 이용한 요소 노드 취득

const $elem = document.getElementById('banana');
$elem.style.color = 'red';

2. 태그 이름을 이용한 요소 노드 취득

const $elems = document.getElementsByTagName('li');
[...$elems].forEach(elem => {elem.style.color = 'red';});

const $all = document.getElementsByTagName('*');

3. class를 이용한 요소 노드 취득

const $elems = document.getElementsByClassName('fruit apple');
[...$elems].forEach(elem => {elem.style.color = 'red';});

4. CSS 선택자를 이용한 요소 노드 취득

document.querySelector('input[type=text]');
document.querySelectorAll('p + ul');
document.querySelectorAll('p ~ ul');
document.querySelector('a:hover');
document.querySelector('p::before');

5. 특정 요소 노드를 취득할 수 있는지 확인

Element.prototype.matches 메서드는 인수로 전달한 CSS 선택자를 통해 특정 요소 노드를 취득할 수 있는지 확인한다.
Element.prototype.matches 메서드는 이벤트 위임을 사용할 때 유용하다.

<!DOCTYPE html>
<html>
    <body>
        <ul id="fruits">
            <li class="apple">apple</li>
            <li class="banana">banana</li>
        </ul>
        <script>
            const $apple = document.querySelector('.apple');
            console.log($apple.matches('#fruits > li.apple')); // true
            console.log($apple.matches('#fruits > li.banana')); // false
        </script>
    </body>    
</html>

6. HTML Collection과 NodeList

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

querySelectorAll 메서드는 DOM 컬렉션 객체인 NodeList 객체를 반환한다. 이때 NodeList 객체는 실시간으로 노드 객체의 상태 변경을 반영하지 않는 객체다.

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

노드 객체의 상태 변경과 상관없이 안전하게 DOM 컬렉션을 사용하려면 HTMLCollection 이나 NodeList 객체를 배열로 변환하여 사용하는 것을 권장한다.

스프레드 문법이나 Array.from 메서드를 사용하여 간단히 배열로 변환할 수 있다.

<!DOCTYPE html>
<html>
    <body>
        <ul id="fruits">
            <li>apple</li>
            <li>banana</li>
        </ul>
        <script>
            const $fruits = document.getElementById('fruits');
            const {childNodes} = $fruits;
            [...childNodes].forEach(childNode => {$furits.removeChild(childNode);});
            console.log(childNodes); // NodeList []
        </script>
    </body>    
</html>

노드 탐색

자식 노드 탐색

Element.prototype.children
Element.prototype.firstElementChild
Element.prototype.lastElementChild

자식 노드 존재 확인

children.length 나 childElementCount 프로퍼티를 사용한다.

부모 노드 탐색

Node.prototype.parentNode

형제 노드 탐색

Element.prototype.previousElementSibling
Element.prototype.nextElementSibling

노드 정보 취득

프로퍼티 설명
Node.prototype.nodeType 노드 타입을 나타내는 상수를 반환한다.
- Node.ELEMENT_NODE:요소 노드 타입을 나타내는 상수 1을 반환
- Node.TEXT_NODE:텍스트 노드 타입을 나타내는 상수 3을 반환
- Node.DOCUMENT_NODE:문서 노드 타입을 나타내는 상수 9을 반환
Node.prototype.nodeName 노드의 이름을 문자열로 반환한다.
- 요소 노드:대문자 문자열로 태그 이름("UL","LI"등)을 반환
- 텍스트 노드:문자열 "#text"를 반환
- 문서 노드:문자열 "#document"를 반환

요소 노드의 텍스트 조작

console.log(document.getElementById('foo').textContent);
document.getElementById('foo').textContent = 'Hi <span>there!</span>'; // HTML 마크업이 파싱되지 않는다.

innterText 프로퍼티 보다 textContent 프로퍼티를 사용하는 것이 더 좋다.
innterText 프로퍼티는 CSS에 순종적이다.

DOM 조작

1. innerHTML

textContent 프로퍼티 에 할당된 문자열에 HTML 마크업이 포함되어 있더라도 이 HTML 마크업은 파싱되지 않고 텍스트로 취급된다.
반면에, innerHTML 프로퍼티 에 할당된 문자열에 포함된 HTML 마크업은 파싱되어 요소 노드의 자식 노드로 DOM에 반영된다.

사용자로부터 입력받은 데이터를 그대로 innerHTML 프로퍼티에 할당하는 것은 크로스 사이트 스크립팅 공격에 취약하므로 위험하다.
HTML5는 innerHTML 프로퍼티로 삽입된 script 요소 내의 자바스크립트 코드를 실행하지 않는다.

HTML 새니티제이션(HTML sanitization)

HTML 새니티제이션은 사용자로부터 입력받은 데이터에 의해 발생할 수 있는 크로스 사이트 스크립팅 공격을 예방하기 위해 잠재적 위험을 제거하는 기능을 말한다.
새니티제이션 함수를 직접 구현할 수도 있겠지만, DOMPurity 라이브러리를 사용하는 것을 권장한다.
DOMPurity 는 다음과 같이 잠재적 위험을 내포한 HTML 마크업을 새니티제이션(살균)하여 잠재적 위험을 제거한다.

DOMPurity.sanitize('<img src="x" onerror="alert(document.cookie)">'); // => <img src="x">

2. insertAdjacentHTML 메서드

https://docs.microsoft.com/en-us/previous-versions/office/developer/office-2003/images/aa170712.beforeafter_za01018765(en-us,office.11).gif

<!DOCTYPE html>
<html>
    <body>
        <!-- beforebegin -->
        <div id="foo">
            <!-- afterbegin -->
            text
            <!-- beforeend -->
        </div>
        <!-- afterend -->

        <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>
    </body>    
</html>

3. 노드 생성과 추가

<!DOCTYPE html>
<html>
    <body>
        <ul id="fruits">
            <li>apple</li>
        </ul>
        <script>
            const $fruits = document.getElementById('fruits');

            // 1. 요소 노드 생성
            const $li = document.createElement('li');

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

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

            // 4. $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
            $fruits.appendChild($li);
        </script>
    </body>    
</html>

4. 복수의 노드 생성과 추가

<!DOCTYPE html>
<html>
    <body>
        <ul id="fruits">
        </ul>
        <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);

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

                // 4. $li 요소 노드를 DocumentFragment 노드의 마지막 자식 노드로 추가
                $fragment.appendChild($li);
            });
            //5. DocumentFragment 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
            $fruits.appendChild($fragment);
        </script>
    </body>    
</html>

5. 노드 삽입

마지막 노드로 추가

Node.prototype.appendChild 메서드는 인수로 전달받은 노드를 자신을 호출한 노드의 마지막 자식 노드로 DOM에 추가한다.

지정한 위치에 노드 삽입

Node.prototype.insertBefore(newNode, childNode) 메서드는 첫 번째 인수로 전달받은 노드를 두 번째 인수로 전달받으 노드 앞에 삽입한다.

<!DOCTYPE html>
<html>
    <body>
        <ul id="fruits">
            <li>apple</li>
            <li>banana</li>
        </ul>
        <script>
            const $fruits = document.getElementById('fruits');

            const $li = document.createElement('li');

            $li.appendChild(document.createTextNode('orange'));

            $fruits.insertBefore($li, $fruits.lastElementChild);
            // apple - orange - banana
        </script>
    </body>    
</html>

6. 노드 이동

<!DOCTYPE html>
<html>
    <body>
        <ul id="fruits">
            <li>apple</li>
            <li>banana</li>
            <li>orange</li>
        </ul>
        <script>
            const $fruits = document.getElementById('fruits');

            const [$apple, $banana,] = $fruits.children;

            $fruits.appendChild($apple); 
            // banana - orange - apple

            $fruits.insertBefore($banana, $fruits.lastElementChild);
            // orange - banana - apple
        </script>
    </body>    
</html>

7. 노드 복사

Node.prototype.cloneNode([deep:true|false]) 메서드는 노드의 사본을 생성하여 반환한다.
매개변수 deep에 true를 인수로 전달하면 노드를 깊은 복사 하여 모든 자손 노드가 포함된 사본을 생성하고,
false를 인수로 전달하거나 생략하면 노드를 얕은 복사하여 노드 자신만의 사본을 생성한다.
얕은 복스로 생성된 요소 노드는 자손 노드를 복사하지 않으므로 텍스트 노드도 없다.

8. 노드 교체

Node.prototype.replaceChild(newChild, oldChild) 메서드는 자신을 호출한 노드의 자식 노드를 다른 노드로 교체한다.

<!DOCTYPE html>
<html>
    <body>
        <ul id="fruits">
            <li>apple</li>
        </ul>
        <script>
            const $fruits = document.getElementById('fruits');

            const $newChild = document.createElement('li');
            $newChild.textContent = 'banana';

            $fruits.replaceChild($newChild, $fruits.firstElementChild) ;

        </script>
    </body>    
</html>

9. 노드 삭제

Node.prototype.removeChild(child) 메서드는 child 매개변수에 인수로 전달한 노드를 DOM에서 삭제한다.

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

어트리뷰트

attributes 프로퍼티, HTML 어트리뷰트 조작

<!DOCTYPE html>
<html>
    <body>
        <input type="text" id="user" value="mary">
        <script>
            const $input = document.getElementById('user');
            const {attributes} = $input;
            console.log(attributes);
            console.log($input.getAttribute('value')); // mary
            $input.setAttribute('value','foo');
            console.log($input.getAttribute('value')); // foo

            if($input.hasAttribute('value')){
                $input.removeAttribute('value');
            }
            console.log($input.hasAttribute('value')); // false
        </script>
    </body>    
</html>

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

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

<!DOCTYPE html>
<html>
    <body>
        <input type="text" id="user" value="mary">
        <script>
            const $input = document.getElementById('user');
            $input.value = 'foo';
            console.log($input.value);
            console.log($input.getAttribute('value'));
        </script>
    </body>    
</html>
  • id 어트리뷰트와 id 프로퍼티는 1:1 대응하며, 동일한 값으로 연동한다.
  • input 요소의 value 어트리뷰트는 value 프로퍼티와 1:1 대응한다. value 어트리뷰트는 초기 상태를, value 프로퍼티는 최신 상태를 갖는다.
  • class 어트리뷰트는 className, classList 프로퍼티와 대응한다.
  • for 어트리뷰트는 htmlFor 프로퍼티와 1:1 대응한다.

data 어트리뷰트와 dataset 프로퍼티

data 어트리뷰터의 값은 HTMLElement.dataset 프로퍼티로 취득할 수 있다.

<!DOCTYPE html>
<html>
    <body>
        <ul class="users">
            <li id="1" data-user-id="1234" data-role="admin">lee</li>
            <li id="2" data-user-id="5678" data-role="subscriber">kim</li>
        </ul>
        <script>
            const users = [...document.querySelector('.users').children];
            const user = users.find(user => user.dataset.userId === '1234');
            console.log(user.dataset.role); // admin
            user.dataset.role = 'subscriber';
            console.log(user.dataset.role); // subscriber
        </script>
    </body>    
</html>

스타일

1. 인라인 스타일 조작

$div.style.backgroundColor = 'yellow';
$div.style['background-color'] = 'red';
$div.style.width = '100px';

2. 클래스 조작

className

className 프로퍼티는 문자열을 반환하므로 공백으로 구분된 여러 개의 클래스를 반환하는 경우 다루기가 불편하다.

<!DOCTYPE html>
<html>
    <head>
        <style>
            .box{width:100px;height:100px;}
            .red{color:red;}
            .blue{color:blue;}
        </style>
    </head>
    <body>
        <div class="box red">hello</div>
        <script>
            const $box = document.querySelector('.box');
            console.log($box.className); // 'box red'
            $box.className = $box.className.replace('red', 'blue');
        </script>
    </body>    
</html>

classList

<!DOCTYPE html>
<html>
    <head>
        <style>
            .box{width:100px;height:100px;}
            .red{color:red;}
            .blue{color:blue;}
        </style>
    </head>
    <body>
        <div class="box red">hello</div>
        <script>
            const $box = document.querySelector('.box');
            console.log($box.classList);
            $box.classList.replace('red', 'blue');
        </script>
    </body>    
</html>
$box.classList.add('foo');
$box.classList.add('bar', 'baz'); // class="foo bar baz"

$box.classList.remove('foo'); // class="bar baz"
$box.classList.remove('bar', 'baz'); // class=""

$box.classList.add('box', 'red');
$box.classList.item(0); // 'box'
$box.classList.item(1); // 'red'

$box.classList.contains('box'); // true
$box.classList.contains('x'); // false

$box.classList.replace('red', 'blue'); // class="box blue"

$box.classList.toggle('foo'); // class="box blue foo"
$box.classList.toggle('foo'); // class="box blue"

// 강제로 'foo' 클래스 추가
$box.classList.toggle('foo', true); // class="box blue foo"
// 강제로 'foo' 클래스 제거
$box.classList.toggle('foo', false); // class="box blue"

3. 요소에 적용되어 있는 CSS 스타일 참조

<!DOCTYPE html>
<html>
    <head>
        <style>
            body{color:red;}
            .box{width:100px;height:100px;}
            .box::before{content:'hi';}
        </style>
    </head>
    <body>
        <div class="box">hello</div>
        <script>
            const $box = document.querySelector('.box');
            const computedStyle = window.getComputedStyle($box);
            console.log(computedStyle.color); // rgb(255, 0, 0)
            console.log(computedStyle.width); // 100px
            console.log(computedStyle.display); // block
            
            const computedStyle2 = window.getComputedStyle($box, ':before');
            console.log(computedStyle2.content); // "hi"
        </script>
    </body>    
</html>

DOM 표준

레벨 표준 문서 URL
DOM Level1 https://www.w3.org/TR/REC-DOM-Level-1/
DOM Level2 https://www.w3.org/TR/DOM-Level-2-Core/
DOM Level3 https://www.w3.org/TR/DOM-Level-3-Core/
DOM Level4 https://dom.spec.whatwg.org/