Javascript - DOM

fgStudy·2022년 5월 12일
0

자바스크립트

목록 보기
16/26
post-thumbnail

해당 포스팅에서는 DOM의 정의와 DOM을 동적으로 수정할 수 있는 DOM API에 대해 설명한다.


1. DOM (Domcoument Object Model)

텍스트 파일로 만들어져 있는 웹 문서를 브라우저에 렌더링하려면 웹 문서를 브라우저가 이해할 수 있는 구조로 메모리에 올려야 한다. 브라우저의 렌더링 엔진은 HTML 문서를 로드한 후, 파싱하여 웹 문서를 브라우저가 이해할 수 있는 자료 구조인 DOM을 생성해 메모리에 적재한다.

DOM(Document Object Model)은 HTML 문서의 계층적 구조와 정보뿐만 아니라 HTML의 요소와 스타일링을 변경할 수 있는 프로그래밍 인터페이스로서 DOM API를 제공한다. 따라서 DOM API를 사용해 이미 생성된 DOM을 동적으로 조작할 수 있으며 변경된 DOM은 렌더링에 반영된다.


2. 노드

2.1 HTML 요소와 노드 객체

HTML 요소는 HTML 문서를 구성하는 개별적인 요소를 의미한다.

- div: tag
- class: attribute name
- greeting: attribute value
- Hello: contents

<div class="greeting">Hello/<div>

HTML 요소는 렌더링 엔진에 의해 파싱되어 DOM을 구성하는 요소 노드 객체로 변환된다. 이 때 HTML 요소의 attribute는 attribute node로, HTML 요소의 텍스트 콘텐츠는 text node로 변환된다.



2.2 노드 객체 타입

아래의 HTML 문서를 렌더링 엔진이 파싱한다고 가정하자.

<!DOCTYPE html>
<html>
  <head>
    <style>
      .red  { color: #ff0000; }
      .blue { color: #0000ff; }
    </style>
  </head>
  <body>
    <div>
      <h1>Cities</h1>
      <ul>
        <li id="one" class="red">Seoul</li>
        <li id="two" class="red">London</li>
        <li id="three" class="red">Newyork</li>
        <li id="four">Tokyo</li>
      </ul>
    </div>
  </body>
</html>

렌더링 엔진의 파싱결과로 아래의 DOM이 생성된다. DOM은 노드 객체의 트리로 계층적 구조화되어 있기 때문에 DOM 트리라고 불린다. 아래 사진은 DOM 트리 예시이다.

DOM에서 모든 요소, 어트리뷰트, 텍스트는 하나의 객체이며 Document 객체의 자식이다. 요소의 상속관계는 객체의 트리로 구조화하여 부자관계를 표현한다. 노드 객체는 총 12 종류로, 가장 중요한 노드 타입은 다음 4가지다.

1. 문서 노드(Document Node)

트리의 최상위에 존재하며 각각 요소, 어트리뷰트, 텍스트 노드에 접근하려면 문서 노드를 통해야 한다. 즉, DOM tree에 접근하기 위한 시작점(entry point)이다.

2. 요소 노드(Element Node)

요소 노드는 HTML 요소를 표현한다. HTML 요소는 중첩에 의해 부자 관계를 가지며 이 부자 관계를 통해 정보를 구조화한다. 따라서 요소 노드는 문서의 구조를 서술한다고 말 할 수 있다. 어트리뷰트, 텍스트 노드에 접근하려면 먼저 요소 노드를 찾아 접근해야 한다. 모든 요소 노드는 요소별 특성을 표현하기 위해 HTMLElement 객체를 상속한 객체로 구성된다.

3. 어트리뷰트 노드(Attribute Node)

어트리뷰트 노드는 HTML 요소의 어트리뷰트를 표현한다. 어트리뷰트 노드는 해당 어트리뷰트가 지정된 요소의 자식이 아니라 해당 요소의 일부로 표현된다. 따라서 해당 요소 노드를 찾아 접근하면 어트리뷰트를 참조, 수정할 수 있다.

4. 텍스트 노드(Text Node)

텍스트 노드는 HTML 요소의 텍스트를 표현한다. 텍스트 노드는 요소 노드의 자식이며 자신의 자식 노드를 가질 수 없다. 즉, 텍스트 노드는 DOM tree의 최종단이다.



2.3 노드 객체의 상속 구조

DOM을 구성하는 노드 객체는 브라우저 환경에서 추가적으로 제공하는 호스트 객체이다. 하지만 노드 객체도 자바스크립트 객체이므로 프로토타입에 의한 상속 구조를 갖는다. 노드 객체의 상속 구조는 아래와 같다.

이러한 상속 구조로 각 노드 객체는 프로토타입 체인에 있는 모든 프로토타입의 DOM API를 상속받아 사용할 수 있다.

정리하자면 DOM은 HTML 문서의 계층적 구조와 정보를 표현하는 것은 물론 노드 객체 타입에 따라 필요한 기능을 프로퍼티와 메서드의 집합인 DOM API로 제공한다. 이 DOM API를 통해 HTML의 구조나 내용 또는 스타일을 동적으로 조작할 수 있다.



2.4 요소 노드 취득

2.4.1 하나의 요소 노드 취득(DOM Query)

  1. Document.prototype.getElementById(id)

    • 인수로 전달받은 id 어트리뷰트 값으로 요소 노드를 한 개 선택한다. 복수개가 선택된 경우, 첫번째 요소만 반환한다. 만약 인수로 전달한 id값을 갖는 HTML 요소가 없을 시 null을 반환한다.
    • Return: HTMLElement를 상속받은 객체
    // id로 하나의 요소를 선택한다.
    const elem = document.getElementById('one');
    // 클래스 어트리뷰트의 값을 변경한다.
    elem.className = 'blue';
  2. Document.prototype/Element.prototype.querySelector(cssSelector)

    • 인수로 전달한 CSS 선택자를 만족시키는 요소 노드가 여러 개인 경우 첫 번째 요소 노드만 반환한다. 만약 인수로 전달된 CSS 선택자가 존재하지 않는 경우 null을 반환한다.
    • 인수로 전달한 CSS 선택자가 문법에 맞지 않는 경우 DOMException 에러가 발생한다.
    • Return: Return: HTMLElement를 상속받은 객체
    // CSS 셀렉터를 이용해 요소를 선택한다
    const elem = document.querySelector('li.red');
    // 클래스 어트리뷰트의 값을 변경한다.
    elem.className = 'blue';

2.4.2 여러 개의 요소 노드 선택(DOM Query)

  1. Document.prototype/Element.prototype.getElementByTagName(tag)

    • 인수로 전달받은 tag 이름을 갖는 모든 요소 노드를 탐색해 반환한다. 만약 인수로 전달된 태그 이름을 갖는 요소가 존재하지 않는 경우 빈 HTMLCollection 객체를 반환한다.
    • Return: HTMLCollection
    // HTMLCollection을 반환한다.
    const elems = document.getElementsByTagName('li');
  2. Document.prototype/Element.prototype.getElementByClassName(class)

    • 인수로 전달받은 class 어트리뷰트 값을 갖는 모든 요소 노드들을 탐색하여 반환한다. 만약 인수로 전달한 id값을 갖는 HTML 요소가 없을 시 빈 HTMLCollection을 반환한다.
    • Return: HTMLCollection (live)
    // HTMLCollection을 반환한다. HTMLCollection은 live하다.
    const elems = document.getElementsByClassName('red');
  3. Document.prototype/Element.prototype.querySelectorAll(cssSelector)

    • 인수로 전달한 CSS 선택자를 만족시키는 요소 노드가 여러 개인 경우 모든 요소를 반환한다.
    • Return: NodeList (non-live)
    // querySelectorAll는 Nodelist(non-live)를 반환한다. IE8+
    const elems = document.querySelectorAll('.red');


2.5 HTMLCollection과 NodeList

DOM 컬렉션 객체인 HTMLCollection과 NodeList는 DOM API가 여러 개의 결과값을 반환하기 위한 DOM 컬렉션 객체이다. HTMLCollection과 NodeList는 모두 유사배열 객체이면서 이터러블이다.

이 둘의 차이점은 노드 객체의 상태 변화를 실시간으로 반영할 수 있는지 여부이다. HTMLCollection의 경우 언제나 live 객체로 노드 객체의 상태 변화를 실시간으로 반영한다. 반면 NodeList는 대부분의 경우 실시간으로 반영하지 않고 과거의 정적 상태를 유지하는 non-liv 객체로 동작한다. 하지만 childNodes 프로퍼티가 반환하는 NodeList는 live 객체로 동작한다.

live로 노드 객체의 변화를 실시간으로 변화한다면, 컬렉션의 결과값을 for문으로 변화시킬 때 index 문제가 발생한다. NodeList는 기본적으로 non-live이지만 앞서 말했듯이 live 객체로 동작할 때가 있다.

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



2.6 노드 탐색

Node, Element 인터페이스는 DOM 트리 상의 노드를 탐색할 수 있도록 트리 탐색 프로퍼티를 제공한다.

노드 탐색 프로퍼티는 getter만 존재하는 읽기 전용 접근자 프로퍼티이다. 따라서 프로퍼티에 값을 할당 시 에러없이 무시된다.


2.6.1 자식 노드 탐색

  1. Node.prototype.childNodes
    • 자식노드를 전부 탐색하여 NodeList에 담아 반환
    • NodeList에는 요소 노드 뿐만 아니라 텍스트 노드도 포함될 수 있다.
  1. Element.prototype.children
    • 자식노드중 요소 노드만을 모두 탐색하여 HTMLCollection에 담아 반환
    • HTMLCollection에는 텍스트 노드도 포함되지 않는다.
  1. Node.prototype.firstChild / lastChild
    • 자식 노드를 반환한다.
    • 텍스트 노드거나 요소 노드이다.
  2. Node.prototype.firstElementChild / lastElementChild
    • 자식 요소 노드를 반환한다.
    • 요소 노드이다.

2.6.2 자식 노드 존재 확인

  1. Node.prototype.hasChildNodes
    • 자식 노드가 존재하면 true, 존재하지 않으면 false를 반환한다.
    • childNodes 프로퍼티처럼 텍스트 노드르 포함하여 자식 노드를 확인한다.
  1. Element.prototype.children.length / Element.prototype.childrenElementCount
    • 자식 노드 중에 텍스트 노드가 아닌 요소 노드가 존재하는지 확인하기 위해서는 Element.prototype.children.length / Element.prototype.childrenElementCount를 사용한다.

2.6.3 부모 노드 탐색

  • Node.prototype.parentNode

2.6.4 형제 노드 탐색

  1. Node.prototype.previousSibling, nextSibling

    • 형제 노드를 반환한다.
    • 텍스트 노드를 포함한 모든 형제 노드를 탐색후 HTMLElement를 반환
  2. Node.prototype.previousElementSibling, nextElementSibling

    • 형제 노드 중 요소 노드만을 탐색해 HTMLElement를 반환한다.

2.6.5 요소 노드의 텍스트 조작

  1. nodeValue

    • setter와 getter가 모두 존재하는 접근자 프로퍼티다. 따라서 값을 할당해 텍스트를 조작할 수 있다.
    • 텍스트를 변경할 요소 노드를 취득한다. 텍스트 노드는 요소 노드의 자식노드이므로 firstChild 프로퍼티를 사용하여 탐색한다. 탐색한 텍스트 노드를 nodeValue를 이용해 값을 변경한다.
    <!DOCTYPE html>
    <html>
      <body>
        <div id="foo">Hello</div>
      </body>
      <script>
        // 1. 텍스트를 변경할 요소 노드를 취득한다
      	const $foo = document.getElementById('foo');
        console.log($foo.nodeValue); // null
        
        // 2. firstChild 프로퍼티를 사용해 텍스트 노드를 탐색한다.
        const $textNode = $foo.fisrtChild;
        // 3. 탐색한 텍스트 노드의 값을 nodeValue 프로퍼티를 사용해 참조한다.
        console.log($textNode.nodeValue); // Hello
        
        // 4. 텍스트 노드의 값을 nodeValue 프로퍼티를 사용해 텍스트 노드의 값을 변경한다.
        $textNode.nodeValue = 'World';
        console.log($textNode.nodeValue); // World
      </script>
    </html>
  2. textContent

    • setter와 getter가 모두 존재하는 접근자 프로퍼티다. 따라서 값을 할당해 텍스트를 조작할 수 있다.
    • 요소 노드의 textContent 프로퍼티를 참조하면 요소 노드 콘텐츠 영역 내의 텍스트를 모두 반환한다. 이 때 HTML 마크업은 무시된다.
    • textContent를 통해 요소에 새로운 텍스트를 할당하면 텍스트를 변경할 수 있다. 이때 순수한 텍스트만 지정해야 하며 마크업을 포함시키면 문자열로 인식되어 그대로 출력된다.
    <!DOCTYPE html>
    <html>
      <body>
        <div id="foo">Hello <span>World!</span></div>
      </body>
      <script>
        // #foo 요소 노드의 텍스트를 모두 취득한다. 이 때 HTML 마크업은 무시된다.
        console.log(document.getElementById('foo').textContent); // Hello World!
        
        // #foo 요소 노드의 모든 자식 노드가 제거되고 할당된 문자열이 텍스트로 추가된다.
        // 이 때 HTML 마크업이 파싱되지 않는다.
        document.getElementById('foo').textContent = `Hi <span>there!</span>`;
        console.log(document.getElementById('foo').textContent); // Hi <span>there!</span>
      </script>
    </html>

7. 어트리뷰트

7.1. 어트리뷰트 노드와 어트리뷰트 프로퍼티

  • HTML 문서의 구성 요소인 HTML 요소는 여러 개의 어트리뷰트를 가질 수 있다.

  • HTML 요소의 동작을 제어하기 위한 추가적인 정보를 제공하는 HTML 어트리뷰트는 HTML 요소의 시작 태그에 정의한다.

    <input id="user" type="text" value="ungmo2">
  • HTML 문서가 파싱될 때 HTML 요소의 어트리뷰트는 어트리뷰트 노드에 변환되어 요소 노드와 연결된다. 이 때 HTML 어트리뷰트당 하나의 어트리뷰트가 생성된다.

  • 모든 어트리뷰트 노드의 참조는 유사배열 객체이자 이터러블인 NamedNodeMap 객체에 담겨 요소 노드의 attributes 프로퍼티에 저장된다.


7.2 HTML 어트리뷰트 조작

  • Element.prototype.getAttribute/setAttribute(attributeName) 메서드를 이용하면 요소 노드에서 메서드를 통해 직접 HTML 어트리뷰트 값을 취득하거나 변경할 수 있다.

    <!DOCTYPE html>
    <html>
    <head>
    </head>
    <body>
      <input id="user" type="text" value="ungmo2">My First CSS Example</input>
      <script>
        const $input = document.getElementById('user');
        const inputValue = $input.getAttribute('value');
        console.log(inputValue); // ungmo2
    
        $input.setAttribute('value', 'foo');
        console.log($input.getAttribute('value')); // foo
      </script>
    </body>
    </html>
  • 특정 어트리뷰트가 존재하는지 확인하려면 Element.prototype.hasAttribute(attributeName) 메서드를 이용하고, 특정 어트리뷰트를 삭제하려면 Element.prototype.removeAttribute(attributeName) 메서드를 이용한다.

<!DOCTYPE html>
<html>
<head>
</head>
<body>
  <input id="user" type="text" value="ungmo2">My First CSS Example</input>
  <script>
  	const $input = document.getElementById('user');
	// value 어트리뷰트 존재 확인
	if ($input.hasAttribute('value')) {
		// value 어트리뷰트 삭제
		$input.removeAttribute('value');
	}
	console.log($input.hasAttribute('value')); // false
  </script>
</body>
</html>

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

  • HTML 어트리뷰트는 HTML 요소의 초기 상태를 지정한다. HTML 어트리뷰트 값은 HTML 요소의 초기 상태를 의미하며 이는 변하지 않는다. 이 때 HTML 어트리뷰트로 지정한 HTML 요소의 초기 상태는 어트리뷰트 노드에서 관리한다.

  • 반면 요소 노드는 상태를 가지고 있으며, 사용자가 DOM을 조작할 시 상태가 변경된다.

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


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

  • data 어트리뷰트와 dataset 프로퍼티를 사용하면 HTML 요소에 정의한 사용자 정의 어트리뷰트와 자바스크립트 간에 데이터를 교환할 수 있다.

  • data 어트리뷰트는 data-접두사 다음에 임의의 이름을 붙여 사용한다. (ex. data-user-id, data-role)

  • data 어트리뷰트 값은 HTMLElement.dataset 프로퍼티로 취득할 수 있다. dataset 프로퍼티는 HTML 요소의 모든 data 어트리뷰트 정보를 제공하는 DOMStringMap 객체를 반환한다.

  • DOMStringMap 객체는 data 어트리뷰트의 data-접두사 다음에 붙인 임의의 이름을 카멜 케이스로 변환한 프로퍼티를 가지고 있다. 이 프로퍼티로 data 어트리뷰트의 값을 취득하거나 변경할 수 있다.

    <!DOCTYPE html>
    <html>
    <head>
    </head>
    <body>
      <ul class="users">
        <li id="1" data-user-id="1" data-role="admin">Lee</li>
        <li id="2" data-user-id="2" data-role="subscriber">Kim</li>
      </ul>
      <script>
        const users = [...document.querySelector('.users').children];
        const user = users.find(user => user.dataset.userId === '1');
        console.log(user.dataset.role); // "admin"
    
        user.dataset.role = "subscriber";
        console.log(user.dataset); // [object DOMStringMap] { role: "subscriber",userId: "1"}
      </script>
    </body>
    </html>

8. style

8.1. 인라인 스타일 조작

  • HTMLElement.prototype.style 프로퍼티는 setter와 getter가 모두 존재하는 접근자 프로퍼티로, 요소 노드의 인라인 스타일을 취득하거나 추가, 변경한다.
const four = document.getElementById('four');

// inline 스타일 선언을 생성
four.style.color = 'blue';

// font-size와 같이 '-'으로 구분되는 프로퍼티는 카멜케이스로 변환하여 사용한다.
// 만약 CSS 프로퍼티 이름을 그대로 사용하려면 대괄호 표기법을 이용한다.
four.style.fontSize = '2em';
// four.style['font-size'] = '2em';

8.2. 클래스 조작

  1. className
  • Element.prototype.className 프로퍼티는 setter와 getter가 모두 존재하는 접근자 프로퍼티로, HTML 요소의 class 어트리뷰트 값을 취득하거나 변경한다.

  • className 프로퍼티를 참조하면 class 어트리뷰트의 값을 문자열로 변환

  • 요소 노드의 className 프로퍼티에 문자열을 할당하면 class 어트리뷰트 값을 할당한 문자열로 변경

    <!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 어트리뷰트 값 취득
        console.log($box.className); // "box red"
        $box.className = $box.className.replace('red', 'blue');
        console.log($box.className); // "box blue"
      </script>
    </body>
    </html>

  1. classList
  • Element.prototype.classList 프로퍼티는 HTML 요소의 class 어트리뷰트의 정보를 담은 DOMTokenList 객체를 반환한다.

    <!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 객체 취득
        // DOMTokenList는 노드 객체의 상태 변화를 실시간으로 반영하는 live 객체다.
        console.log($box.classList);
    
        // .box 요소의 class 어트리뷰트 값 중에서 red만 blue로 변경
        $box.classList.replace('red', 'blue');
        console.log($box.classList);
      </script>
    </body>
    </html>

  • DOMTokenList 객체 메서드

    • add(...className)
      add 메서드는 인수로 전달한 1개 이상의 문자열을 class 어트리뷰트 값으로 추가한다.

    • remove(...className)
      remove 메서드는 인수로 전달한 1개 이상의 문자열과 일치하는 클래스를 class 어트리뷰트에서 삭제한다. 인수로 전달한 문자열과 일치하는 클래스가 class 어트리뷰트에 없으면 에러없이 무시된다.

    • item(index)
      item 메서드는 인수로 전달한 index에 해당하는 클래스를 class 어트리뷰트에서 반환한다.

    • contains(className)
      contains 메서드는 인수로 전달한 문자열과 일치하는 클래스가 class 어트리뷰트에 포함되어 있는지 확인한다.

    • replace(oldClassName, newClassName)
      replace 메서드는 class 어트리뷰트에서 첫 번째 인수로 전달한 문자열을 두 번째 인수로 전달한 문자열로 변경한다.

    • toggle(classNmae[, force])
      - toggle 메서드는 class 어트리뷰트에 첫 번째 인수로 전달한 문자열과 일치하는 클래스가 존재하면 제거하고, 존재하지 않으면 추가한다.
      - 두 번재 인수로 불리언 값으로 평가되는 조건식을 전달할 수 있다. 조건식이 true일 시 class 어트리뷰트에 첫 번째 인수로 전달한 문자열을 추가하고, false일 시 제거한다.


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

  • style 프로퍼티는 인라인 스타일만을 반환하므로, HTML 요소에 적용되어 있는 모든 CSS 스타일을 참조해야 할 경우 getComputedStyle 메서드를 사용한다.

  • window.getComputedStyle(element, [, pseudo]) 메서드는 첫 번째 인수(element)로 전달한 요소 노드에 적용되어 있는 평가된 스타일을 CSSStyleDeclaration 객체에 담아 반환한다.

  • 두 번째 인수(pseudo)로 :after, :before와 같은 의사 요소를 지정하는 문자열을 전달할 수 있다. 이때 의사 요소가 아닌 일반 요소를 지정할 시 두 번째 인수는 생략된다.

    <!DOCTYPE html>
    <html>
    <head>
      <style>
        .box:before {
          content: 'Hello';
        }
      </style>
    </head>
    <body>
      <div class="box">Box</div>
      <script>
        const $box = document.querySelector('.box');
        const computedStyle = window.getComputedStyle($box, ':before');
        console.log(computedStyle.content); // Hello
      </script>
    </body>
    </html>

(docs) 모던 자바스크립트 딥다이브 39강 DOM

profile
지식은 누가 origin인지 중요하지 않다.

0개의 댓글