모던 자바스크립트 Deep Dive 39장-DOM

HustleKang·2022년 5월 12일

HTML 문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API를 제공하는 트리 자료구조

노드

HTML 요소와 노드 객체

HTML 요소는 렌더링 엔진에 의해 파싱되어 DOM을 구성하는 요소 노드 객체가 됨
태그사이에 다른 태그가 있을 수 있으므로 각각의 요소들은 중첩관계를 갖을 수 있다
요소 간의 부모-자식 관계를 반영하여 트리 자료 구조를 구성한다
노드 객체들로 구성된 트리 자료구조를 DOM이라 한다

노드 객체의 타입

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <ul>
      <li id="apple">Apple</li>
      <li id="banana">Banana</li>
      <li id="orange">Orange</li>
    </ul>
    <script src="app.js"></script>
  </body>
</html>

렌더링 엔진은 위 코드를 파싱하여 아래와 같은 DOM을 생성한다

  • 문서 노드 (document node)
    DOM 트리의 최상단에 존재하는 루트노드, document 객체를 가르킨다
    document 객체는 브라우저가 렌더링한 HTML 문서 전체를 가르킴, 전역 객체의 document프로퍼티에 바인딩되어 있다-> document로 참조 가능
    최상단에 있기 때문에 DOM 트리의 노드들에 접근하기 위한 진입점 역할을 함

  • 요소 노드 (element node)
    HTML 요소를 가르키는 객체
    HTML 요소들의 중첩관계를 통해 문서의 구조를 표현한다

  • 어트리뷰트 노드 (attribute node)
    HTML 요소의 어트리뷰트를 가르키는 객체
    어트리뷰트 노드는 해당 요소 노드에만 연결되어 있다

  • 텍스트 노드 (text node)
    HTML 요소의 텍스트를 가르키는 객체
    해당 요소 노드의 자식 노드이며 말단 노드임

노드 객체의 상속 구조

DOM을 구성하는 노드 객체는 프로토타입에 의한 상속 구조를 갖는다

요소 노드 취득

id를 이용한 요소 노드 취득

Document.prototype.getElementById 메서드로 취득
인수로 전달한 id 어트리뷰트 값과 일치하는 요소 노드를 반환
만약 해당 id를 갖는 요소가 여러개면 맨 처음 노드를 반환
일치하는 요소 노드가 없으면 null 반환

HTML 요소에 id 어트리뷰트를 부여하면 id값과 동일한 이름의 전역 변수가 암묵적으로 생성되고 해당 노드 객체가 할당되는 부수 효과 발생
만약 id값과 동일한 전역 변수가 이미 있으면 이 변수에 노드 객체가 할당되지는 않음

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="apple">Apple</li>
      <li id="banana">Banana</li>
      <li id="orange">Orange</li> <!--글씨 색깔 오렌지 -->
      <li id="orange">Orange2</li>
    </ul>
    <script>
      const $elem = document.getElementById('orange');
      $elem.style.color = 'orange';
      let apple = '사과';
      console.log(orange); // orange 노드 객체 
      console.log(apple); // '사과'
      console.log(banana); // banana 노드 객체
      
    </script>
  </body>
</html>

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

Document.prototype.getElementsByTagName 메서드로 취득
인수로 전달한 태그인 모든 요소 노드를 갖는 HTMLCollection 객체를 반환
해당 요소 노드가 없으면 빈 HTMLCollection 객체 반환
HTMLCollection 객체는 유사 배열 객체,이터러블임
모든 노드 선택시 '*'

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="apple">Apple</li> <!--글씨 색깔 보라색 -->
      <li id="banana">Banana</li> <!--글씨 색깔 보라색 -->
      <li id="orange">Orange</li> <!--글씨 색깔 보라색 -->
    </ul>
    <script>
      const $elems = document.getElementsByTagName('li');
      console.log($elems); // 모든 li 요소 노드를 갖는 HTMLCollection 객체 
      [...$elems].forEach(x=>x.style.color = 'purple');
    </script>
  </body>
</html>

Element.prototype.getElementsByTagName 메서드는
특정 요소 노드에서 호출하여 해당 요소 노드의 자식들 중에서 탐색하여 반환

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li id="apple">Apple</li> <!--글씨 색깔 토마토 -->
      <li id="banana">Banana</li> <!--글씨 색깔 토마토 -->
      <li id="orange">Orange</li> <!--글씨 색깔 토마토 -->
    </ul>
    <ul>
      <li>li태그</li> <!--글씨 색깔 그대로 -->
    </ul>
    <script>
      const $fruits = document.getElementById('fruits');
      const $listFromFruits = $fruits.getElementsByTagName('li');
      [...$listFromFruits].forEach(x=>x.style.color = 'tomato');
    </script>
  </body>
</html>

class를 이용한 요소 노드 취득

인수로 전달한 class 값을 갖는 모든 요소들을 갖는 HTMLCollection 객체를 반환

인수로 전달하는 class 값들은 공백으로 여러개의 class 지정 가능
document.getElementsByClassName('food instant');
-> class가 food인 요소 노드 + class가 instant인 요소 노드가 아니고
class가 food 와 instant 둘다 가지는 요소 노드를 탐색

  • Document.prototype.getElementsByClassName
  • Element.prototype.getElementsByClassName

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

인수로 전달한 CSS 선택자를 만족시키는 하나의 요소 노드를 반환
없으면 null 반환

  • Document.prototype.querySelector
  • Element.prototype.querySelector

인수로 전달한 CSS 선택자를 만족하는 모든 노드를 구하려면

  • Document.prototype.querySelectorAll
  • Element.prototype.querySelectorAll

모든 요소 노드를 담은 NodeList 객체를 반환
NodeList 객체는 유사 배열 객체,이터러블임

id 어트리뷰트가 있는 요소 노드를 취득하는 경우는 getElementById 사용하고 그 외에만 querySelector 사용 ( querySelector가 더 느림)

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li>pizza</li>
      <li>noddle</li>
      <li>rice</li>
    </ul>
    <script>
      const $elems=document.querySelectorAll('ul > li');
      [...$elems].forEach(x=>x.style.color = 'blue')
    </script>
  </body>
</html>

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

Element.prototype.matches 메서드
인수로 전달한 CSS 선택자로 해당 요소 노드를 취득할 수 있는지 true,false 반환
이벤트 위임을 사용할 때 유용

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="pizza">pizza</li>
      <li>noddle</li>
      <li>rice</li>
    </ul>
    <script>
      const $elems=document.querySelector('ul > li');
      console.log($elems.matches('#pizza')); // true
      console.log($elems.matches('li')); // true
      console.log($elems.matches('.pizza')); // false
    </script>
  </body>
</html>

HTMLCollection과 NodeList

HTMLCollection

HTMLCollection은 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는 객체
NodeList는 경우에 따라 live 객체로 동작, 대부분은 non-live

<!DOCTYPE html>
<head>
  <style>
    .red{
      color : red;
    }
    .blue{
      color : blue;
    }
  </style>
</head>
<html>
  <body>
    <ul>
      <li class="red">pizza</li>
      <li class="red">noddle</li>
      <li class="red">rice</li>
    </ul>
    <script>
      const $elems=document.getElementsByClassName('red');
      for(let i = 0;i<$elems.length;i++){
        $elems[i].className='blue';
      }
    </script>
  </body>
</html>

$elems에는 class명이 red인 li요소 3개가 담겨있는 HTMLCollection 객체가 할당
for문을 통해 각각 li요소들의 class명을 blue로 바꿔줄 때 발생하는 일

  1. 첫번째 li요소의 class명을 blue로 바꾼다
    그 이후 더이상 클래스명이 red가 아니기 때문에 해당 요소는 $elems에서 실시간으로 제거됨
  2. $elems에는 li 두번째와, 세번째 요소만 있으니 i=1이 되면 세번째 요소가 제거됨
    그 후 해당 li요소 역시 $elems에서 제거
  3. i=2인데 $elems.length는 1이므로 for문 종료

이런 일을 방지하려면 HTMLCollection 객체를 배열로 변환하여 사용하는게 좋다

NodeList

querySelectorAll 메서드는 NodeList 객체를 반환
NodeList 객체는 실시간으로 노드 객체의 상태 변경을 반영하지 않는 non-live 객체
하지만 childNodes 프로퍼티가 반환하는 NodeList 객체는 live 객체임

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>pizza</li>
      <li>noddle</li>
    </ul>
    <script>
      const $fruits= document.getElementById('fruits');
      const {childNodes} = $fruits;
      console.log(childNodes);
      for(let i = 0;i<childNodes.length;i++){
        $fruits.removeChild(childNodes[i]);
      }
      console.log(childNodes);
    </script>
  </body>
</html>

$fruits는 live 객체이므로 자식노드를 제거할 때마다 새롭게 갱신되어 모든 자식노드가 삭제되지 않는다
결국 배열로 변환해서 사용하는 것이 좋다

노드 탐색

노드 탐색 프로퍼티는 모두 접근자 프로퍼티이다
getter만 있는 읽기 전용 접근자 프로퍼티

공백 텍스트 노드

공백 문자(탭,줄바꿈 등)는 공백 텍스트 노드를 생성함
노드를 탐색할 때 공백 문자에 의해 생성된 공백 텍스트 노드 주의

자식 노드 탐색

  • Node.prototype.childNodes : 텍스트 노드를 포함한 NodeList 객체 반환
  • Element.prototype.children : 텍스트 노드 제외한 HTMLCollection 객체 반환
  • Node.prototype.firstChild : 첫 번째 자식 노드 반환, 텍스트 노드 일수도
  • Node.prototype.lastChild : 마지막 자식 노드 반환, 텍스트 노드 일수도
  • Element.prototype.firstElementChild : 첫 번째 요소 자식 노드 반환
  • Element.prototype.lastElementChild : 마지막 요소 자식 노드 반환

자식 노드 존재 확인

Node.prototype.hasChildNodes 메서드로 확인
true,false 반환
텍스트 노드도 포함해서 자식 노드의 존재를 확인함
텍스트 노드 제외 하려면 children.length, childElementCount 프로퍼티로

<!DOCTYPE html>
<html>
  <body>
    <ul id="foods">
    </ul>
    <script>
      const $foods = document.getElementById('foods');
      console.log($foods.hasChildNodes()); // true
      console.log($foods.children.length); // 0
      console.log($foods.childElementCount); // 0
    </script>
  </body>
</html>

요소 노드의 텍스트 노드 탐색

텍스트 노드는 요소 노드의 자식이기 때문에 firstChild 프로퍼티로 접근 가능

부모 노드 탐색

Node.prototype.parentNode 프로퍼티로 탐색

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

형제 노드 탐색

  • Node.prototype.previousSibling : 텍스트 노드 포함 이전 형제 노드
  • Node.prototype.nextSibling : 텍스트 노드 포함 다음 형제 노드
  • Node.prototype.previousElementSibling : 이전 형제 요소 노드
  • Node.prototype.nextElementSibling : 다음 형제 요소 노드

노드 정보 취득

  • Node.prototype.nodeType : 노드 타입을 상수로 반환
    요소 노드 = 1
    텍스트 노드 = 3
    문서 노드 = 9
  • Node.prototype.nodeName : 노드 이름을 문자열로 반환
    요소 노드 = 'LI','DIV' 태그 이름 대문자로
    텍스트 노드 = #text
    문서 노드 = #document
<!DOCTYPE html>
<html>
  <body>
    <div id="box">안녕하세요</div>
    <script>
      const $box= document.getElementById('box');
      const $textNode = $box.firstChild;
      console.log($box.nodeName,$box.nodeType); // DIV 1
      console.log(document.nodeName,document.nodeType); // #document 9
      console.log($textNode.nodeName,$textNode.nodeType); // #text 3
    </script>
  </body>
</html>

요소 노드의 텍스트 조작

nodeValue

Node.prototype.nodeValue 프로퍼티는 setter,getter 둘 다 존재하는 접근자 프로퍼티 -> 값 갱신이 가능
Node.prototype.nodeValue는 노드 객체의 값을 반환
노드 객체의 값 = 텍스트 노드의 텍스트
즉 텍스트 노드가 아니면 null 반환

<!DOCTYPE html>
<html>
  <body>
    <div id="box">안녕하세요</div>
    <script>
      const $box= document.getElementById('box');
      const $textNode = $box.firstChild;
      console.log($box.nodeValue); // null
      console.log($textNode.nodeValue); // '안녕하세요'
      setTimeout(()=>{
        $textNode.nodeValue = '안녕히계세요';
        console.log($textNode.nodeValue); // '안녕히계세요'
      },3000);
    </script>
  </body>
</html>

textContent

Node.prototype.textContent 프로퍼티도 setter,getter 있는 접근자 프로퍼티
요소 노드의 텍스트와 모든 자손 노드의 텍스트를 취득,변경
즉 요소 노드의 콘텐츠 영역 내에 있는 모든 텍스트를 반환
요소 노드의 textContent 프로퍼티에 새로운 문자열을 할당 시 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가됨

<!DOCTYPE html>
<head>
  <style>
    .box1{
      background-color: red;
      width: 100px;
      height: 100px;
    }
    .box2{
      background-color: blue;
      height: 80px;
    }
    .box3{
      background-color: green;
      height: 60px;
    }
  </style>
</head>
<html>
  <body>
    <div class="box1">
      첫번째
      <div class="box2">
        두번째
        <div class="box3">
          젤 안쪽
        </div>
      </div>
    </div>
    <script>
      const $box = document.querySelector('.box1');
      setTimeout(()=>$box.textContent = '새로운 내용',3000);
    </script>
  </body>
</html>

innerText 프로퍼티는 textContent 프로퍼티와 유사하게 동작
but 사용하지 않는 것이 좋다

  • innerText 프로퍼티는 CSS에 순종적
  • innerText 프로퍼티는 CSS를 고려해야 해서 textContent보다 느림

DOM 조작

새로운 노드를 생성하여 DOM에 추가, 기존 노드를 삭제 or 교체하는 것
-> 리플로우와 리페인트가 발생한다

innerHTML

  • Element.prototype.innerHTML
  • getter,setter 모두 있는 접근자 프로퍼티
  • 해당 요소의 콘텐츠 영역에 있는 모든 HTML 마크업을 문자열로 반환
  • 문자열을 할당 시 요소의 모든 자식 노드가 제거되고 할당한 문자열의 HTML 마크업이 자식 노드가 된다
  • 크로스 사이트 스크립팅 공격에 취약
<!DOCTYPE html>
<html>
  <body>
    <div id="box">
    </div>
    <script>
      const box = document.getElementById('box');
      box.innerHTML = '<ul> <li>사과</li><li>바나나</li></ul>';
    </script>
  </body>
</html>

아래와 같이 에러 이벤트를 발생시켜 자바스크립트 코드가 실행되게 할 수 있음

<!DOCTYPE html>
<html>
  <body>
    <div id="box">
    </div>
    <script>
      const box = document.getElementById('box');
      box.innerHTML = '<img src="x">';
    </script>
  </body>
</html>

innerHTML의 문제점들

  • 모든 노드의 자식을 제거하고 새롭게 할당하므로 비효율적
    만약 ul태그안에 li요소를 한개 추가하고 싶을 때, 새롭게 할당하면 기존의 li요소들이 사라지게되므로
ul태그.innerHTML = '<li> 사과 </li>'; // 이렇게 하면 원래 있던 li태그들은 삭제됨
ul태그.innerHTML += '<li> 사과 </li>'; // 이렇게 해야됨
  • 또한 삽입될 위치를 정할 수도 없다
    1번과 3번 사이에 2번을 넣고 싶어도 불가능
<ul>
	<li> 1번</li>
    <li> 3번</li>
</ul>

기존의 요소를 제거하지 않고 위치를 지정해 요소를 삽입할 때는 innerHTML 비추천

insertAdjacentHTML 메서드

  • Element.prototype.insertAdjacentHTML(position,DOMString)
  • 기존 요소를 제거하지 않고 위치를 지정해 새로운 요소를 삽입
  • position은 총 4 가지 : 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
  • innerHTML 프로퍼티보다 효율적이고 빠르다
  • 크로스 사이트 스크립팅 공격에 취약한건 동일
<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="1">1번</li>
      <li id="3">3번</li>
    </ul>
    <script>
      document.getElementById('1').insertAdjacentHTML("afterend",'<li id="2">2번</li>');
    </script>
  </body>
</html>

노드 생성과 추가

노드를 직접 생성/삽입/삭제/치환할 수 있는 메서드를 DOM은 제공한다

  • 요소 노드 생성 : document.prototype.createElement(tagName)
  • 텍스트 노드 생성 : document.prototype.createTextNode(text)
  • 마지막 자식 노드로 추가 : Node.prototype.appendChild(childNode)
<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="1">1번</li>
      <li id="2">2번</li>
    </ul>
    <script>
      const table =document.querySelector('ul');
      const liNode = document.createElement('li'); // 요소 노드 생성
      const textNode = document.createTextNode('3번'); // 텍스트 노드 생성
      liNode.appendChild(textNode); // 요소 노드에 자식으로 텍스트 노드 추가
      table.appendChild(liNode); // table에 자식으로 요소 노드 추가 (이 때만 리플로우,리페인트 발생)
    </script>
  </body>
</html>

복수의 노드 생성과 추가

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="1">1번</li>
      <li id="2">2번</li>
    </ul>
    <script>
      const table =document.querySelector('ul');
      ['3번','4번','5번'].forEach(text=>{
        const newNode=document.createElement('li');
        newNode.textContent=text;
        table.appendChild(newNode); // 리플로우,리페인트 3번이나 발생
      });
    </script>
  </body>
</html>

컨테이너 요소를 미리 만들고 거기에 새롭게 생성한 노드를 추가하고 마지막에 컨테이너 요소를 추가하면
리플로우,리페인트는 한번만 발생하게 할 수 있다

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="1">1번</li>
      <li id="2">2번</li>
    </ul>
    <script>
      const table =document.querySelector('ul');
      const container = document.createElement('div'); // 컨테이너 요소 생성
  
      ['3번','4번','5번'].forEach(text=>{
        const newNode=document.createElement('li');
        newNode.textContent=text;
        container.appendChild(newNode); // 컨테이너 요소에 추가 
      });
      table.appendChild(container); // 리플로우,리페인트 1번만 발생
    </script>
  </body>
</html>

하지만 li요소만 추가되는 것이 아니고 이를 감싸고 있는 div요소도 같이 추가되는 문제가 생김
컨테이너 요소를 만들 때 DocumentFragment 노드를 만들면 해결 가능
DocumentFragment를 DOM에 추가하면 자신은 제거되고 자식 노드만 DOM에 추가됨
Document.prototype.createDocumentFragment 메서드로 생성

const container = document.createElement('div'); // 일반 노드 만들지 말고
const container = document.createDocumentFragment(); // DocumentFragment로 만들자

노드 삽입

  • Node.prototype.appendChild(newNode) : newNode를 마지막 자식 노드로 추가
  • Node.prototype.insertBefore(newNode,childNode) : childNode 앞에 newNode 삽입
    두번째 인수인 childNode는 반드시 insertBefore를 호출한 노드의 자식이어야 함, 아닐 시 DOMException 에러
    두번째 인수가 null이면 마지막 노드로 추가됨
<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="1">1번</li>
      <li id="3">3번</li>
    </ul>
    <script>
      const table = document.querySelector('ul');
      const newNode = document.createElement('li');
      newNode.textContent = '2번';
      table.insertBefore(newNode,table.lastElementChild);
      
      const newNode2 = document.createElement('li');
      newNode2.textContent = '4번';
      table.insertBefore(newNode2,null);
    </script>
  </body>
</html>

노드 이동

DOM에 이미 존재하는 노드를 appendChild,insertBefore 메서드로 DOM에 추가하면 원래 있던 위치의 노드는 제거되고 새로운 위치로 노드를 추가함 = 노드가 이동

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li>맨 위</li>
      <li>맨 아래</li>
    </ul>
    <script>
      const table = document.querySelector('ul');
      const firstNode = table.firstElementChild;
      table.appendChild(firstNode);
    </script>
  </body>
</html>

노드 복사

  • Node.prototype.cloneNode(true | false)
  • true : 깊은 복사, 모든 자식 노드가 포함된 노드의 사본
  • false | 생략 : 얕은 복사, 노드 자신만의 사본 -> 자식이 없으니 텍스트 노드도 없음

노드 교체

  • Node.prototype.replaceChild(newChild,oldChild)
<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li>원본</li>
    </ul>
    <script>
      const table = document.querySelector('ul');
      const newNode = document.createElement('li');
      newNode.textContent = '교체본';
      table.replaceChild(newNode,table.firstElementChild);
    </script>
  </body>
</html>

노드 삭제

  • Node.prototype.removeChild(child)

어트리뷰트

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

<input id="user" type="text" value="hustlekang">

HTML 요소의 시작 태그에 어트리뷰트 이름 = "어트리뷰트 값" 형식으로 정의
HTML 문서가 파싱될 때 각각의 어트리뷰트는 각각의 어트리뷰트 노드로 변환됨
HTML 요소의 어트리뷰트가 3개이면 어트리뷰트 노드도 3개
모든 어트리뷰트 노드의 참조는 NamedNodeMap 객체에 담겨 요소 노드의 attributes 프로퍼티에 저장
attributes 프로퍼티는 getter만 있는 접근자 프로퍼티
값에 접근 하려면 요소.attributes.어트리뷰트명.value로 접근

<!DOCTYPE html>
<html>
  <body>
    <input type="text" id="name" value="Aj">
    <script>
      const {attributes} = document.getElementById('name');
      console.log(attributes.id.value);
      console.log(attributes.type.value);
      console.log(attributes.value.value);
    </script>
  </body>
</html>

HTML 어트리뷰트 조작

attributes 프로퍼티를 통하지 않고 요소 노드에서 메서드로 바로 HTML 어트리뷰트 값 접근 가능

  • Element.prototype.getAttribute(attributeName)
  • Element.prototype.setAttribute(attributeName,attributeValue)
  • Element.prototype.hasAttribute(attributeName)
  • Element.prototype.removeAttribute(attributeName)
<!DOCTYPE html>
<html>
  <body>
    <input type="text" id="name" value="Aj">
    <script>
      console.log(document.getElementById('name').getAttribute('value'));
      document.getElementById('name').setAttribute('value','changed');
    </script>
  </body>
</html>

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

요소 노드 객체에는 HTML 어트리뷰트에 대응하는 프로퍼티가 존재
이러한 DOM 프로퍼티들은 HTML 어트리뷰트 값을 초기값으로 갖고 있음
DOM 프로퍼티는 setter,getter 둘다 있는 접근자 프로퍼티

<!DOCTYPE html>
<html>
  <body>
    <input type="text" id="name" value="Aj">
    <script>
      const input = document.getElementById('name');
      console.log(input.type, input.id, input.value);
    </script>
  </body>
</html>

- HTML 어트리뷰트 : HTML 요소의 초기 상태를 지정하고 이는 변하지 않는다
- DOM 프로퍼티 : 요소 노드의 최신 상태를 관리

요소 노드의 초기값과 최신값 모두 관리를 해줘야 한다
초기값을 알아야 새로고침 했을 때 OK, 사용자 입력에 의한 최신값은 당연히 알아야 하고

단 모든 DOM 프로퍼티가 사용자 입력에 의해 변경된 최신 상태를 관리하지는 않는다

<input type="text" id="name" value="Aj">

id 프로퍼티는 사용자 입력과 아무런 관계가 없다
id 어트리뷰트와 id 프로퍼티는 항상 동일한 값을 유지한다
하나가 바뀌면 나머지도 바뀜

사용자 입력에 의한 상태변화와 관계있는 DOM 프로퍼티만 최신 상태 값을 관리
그 외의 사용자 입력과 관계없는 어트리뷰트와 DOM 프로퍼티는 항상 동일한 값으로 연동

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

대부분 1:1 대응이지만 아닌 얘들도 있음

  • id 어트리뷰트는 id 프로퍼티와 대응
  • class 어트리뷰트는 className,classList 프로퍼티와 대응
  • for 어트리뷰트는 htmlFor 프로퍼티와 대응
  • 어트리뷰트에 대응하는 프로퍼티는 카멜 케이스임 (htmlFor)

DOM 프로퍼티 타입

getAttribute 메서드로 취득한 어트리뷰트의 값은 항상 문자열
DOM 프로퍼티로 취득한 최신 값은 문자열이 아닐 수도 있음

<!DOCTYPE html>
<html>
  <body>
    <input id="check" type="checkbox" checked>
    <script>
      const input = document.getElementById('check');
      console.log(input.checked); // true
    </script>
  </body>
</html>

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

사용자 정의 어트리뷰트
data-사용자정의 어트리뷰트 이름 = "값" 형식으로 선언
HTMLElement.dataset 프로퍼티는 모든 data 어트리뷰트를 담은 DOMStringMap 객체 반환
DOMStringMap 객체는 사용자정의 어트리뷰트 이름을 카멜케이스로 변환한 프로퍼티를 갖는다

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li data-user-id="1" data-sex="male">John</li>
      <li data-user-id="2" data-sex="female">May</li>
    </ul>
    <script>
      const users = [...document.querySelector('ul').children];
      const 여자 = users.find(x => x.dataset.sex==='female');
      console.log(여자.textContent); // May
      console.log(여자.dataset.userId); // 2
    </script>
  </body>
</html>

스타일

인라인 스타일 조작

  • HTMLElement.prototype.style 프로퍼티로 조작 가능
  • getter,setter 둘다 있는 접근자 프로퍼티
  • 요소 노드의 인라인 스타일을 취득
  • style 프로퍼티 참조시 CSSStyleDeclaration 객체 반환
  • CSSStyleDeclaration 객체의 프로퍼티는 카멜케이스, CSS는 케밥 케이스
  • 프로퍼티에 값 할당시 인라인 스타일로 추가됨
  • 단위 지정이 필요한 CSS 프로퍼티에 단위 없으면 적용X
<!DOCTYPE html>
<html>
  <body>
   <div style="width: 100px; height : 100px; background-color : red;"></div>
    <script>
      const $div = document.querySelector('div');
      console.log($div.style); // CSSStyleDeclaration 객체
      $div.style.backgroundColor = 'blue'; // 파란색으로 변환
      $div.style['background-color'] = 'green'; // css 표기법으로 적용하고 싶을 때
    </script>
  </body>
</html>

클래스 조작

요소의 class 어트리뷰트를 조작하여 다른 스타일을 적용
class 어트리뷰트에 대응하는 DOM 프로퍼티인 className,classList를 통해 적용

className

  • getter,setter
  • 참조시 class 어트리뷰트의 값을 문자열로 반환
  • class가 여러개면 공백으로 구분된 문자열이 반환되어 다루기가 불편
<!DOCTYPE html>
<html>
  <head>
    <style>
      .box{
        width: 100px;
        height: 100px;
      }
      .green{
        background-color: green;
      }
    </style>
  </head>
  <body>
    <div class="box green"></div>
    <script>
      const $div = document.querySelector('div');
      console.log($div.className); // box green
    </script>
  </body>
</html>

classList

  • Element.prototype.classList
  • class 어트리뷰트의 정보를 DOMTokenList 객체에 담아 반환
  • DOMTokenList 객체는 여러가지 메서드 제공

DOMTokenList 객체의 메서드

  • add(...className) : 인수로 전달한 문자열을 class 어트리뷰트에 추가
  • remove(...className) : 삭제, 인수로 전달한 값이 없어도 에러 발생X
  • item(index) : index에 해당하는 클래스를 반환
  • contains(className) : true/false 반환
  • replace(oldClassName,newClassName)
  • toggle(className,[조건식]) : 있으면 삭제, 없으면 추가
    조건식은 선택사항, true면 강제로 추가, false면 강제 삭제

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

  • style 프로퍼티는 인라인 스타일만 반환
  • 클래스나 상속을 통해 적용된 스타일을 알 수 없음
  • 요소에 적용된 모든 스타일을 참조해야 하면 getComputedStyle 메서드로
  • window.getComputedStyle(element)

DOM 표준

HTML과 DOM 표준은 W3C와 WHATWG가 협력으로 공통된 표준을 만들어 왔으나
2018년부터 구글, 애플, 마이크로소프트, 모질라로 구성된 WHATWG가 단일 표준을 제공하기로 두 단체가 합의
DOM은 현재 4개의 버전이 있음
WHATWG(Web Hypertext Application Technology Working Group)

이웅모, 『모던 자바스크립트 Deep Dive』, 위키북스(2021)

profile
grindin'

0개의 댓글