개발 관련 도서

모던 자바스크립트 - DOM(2)

mrban 2024. 7. 30. 23:13

1. 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)

 

 

2. 어트리뷰트

어트리뷰트 노드와 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'); //namedNodeMap객체 반환
      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.getAttribute('checked')); // ''
      console.log(input.checked); // true
    </script>
  </body>
</html>

 

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

data어트리뷰트와 dataset 프로퍼티를 사용하면 HTML요소에 정의한 사용자 정의 어트리뷰트와 자바스크립트 간에 데이터를 교환할 수 있다. 
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(여자.dataset.userId); // 2
    </script>
  </body>
</html>

 

 

3. 스타일

 

인라인 스타일 조작

  • HTMLElement.prototype.style 프로퍼티로 조작 가능
  • getter,setter 둘다 있는 접근자 프로퍼티
  • 요소 노드의 인라인 스타일(HTML의 style 속성)을 취득
  • style 프로퍼티 참조시 CSSStyleDeclaration 객체 반환
  • CSSStyleDeclaration 객체의 프로퍼티는 카멜케이스, CSS 프로퍼티는 케밥 케이스
  • 프로퍼티에 값 할당시 인라인 스타일로 추가됨
  • 단위 지정이 필요한 CSS 프로퍼티에 단위 없으면 적용X -> 예) $div.style.width = '13px'
<!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)