8 분 소요

39.6 DOM 조작

  • Dom 조작은 새로운 노드를 생성해서 DOM에 추가하거나 기존 노드를 삭제 또는 교체하는 것을 의미한다.
  • DOM 조작에 의해 DOM에 새로운 노드가 추가되거나 삭제되면 리플로우와 리페인트가 발생하는 원인이 되므로 성능에 영향을 준다. 따라서 주의해서 다루어야 한다.

39.6.1 innerHTML

  • Element.prototype.innerHTML 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티이다.
  • innerHTML 프로퍼티는 요소 노드의 HTML 마크업을 취득하거나 변경하는데, 요소 노드의 innerHTML 프로퍼티를 참조하면 콘텐츠 영역 내에 포함된 모든 HTML 마크업을 문자열로 반환한다. 아래의 코드로 확인 할 수 있다.
<!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 마크업 문자열로 간단히 DOM 조작이 가능하다.
<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li class="apple">Apple</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById('fruits');

    // 노드 추가
    $fruits.innerHTML += '<li class="banana">Banana</li>';

    // 노드 교체
    $fruits.innerHTML = '<li class="orange">Orange</li>';

    // 노드 삭제
    $fruits.innerHTML = '';
  </script>
</html>
  • 하지만 크로스 사이트 스크립팅 공격에 취약하기때문에 사용에 주의하여야 한다!

  • 크로스 사이트 스크립팅 공격에 예방하기위해 HTML 새니티제이션(HTML sanitization)인 DOMPurify 라이브러리의 사용을 권장한다.

XSS(Cross-Site-Scripting)이란? 사이트 간 스크립팅(또는 크로스 사이트 스크립팅, 영문 명칭 cross-site scripting, 영문 약어 XSS)은 웹 애플리케이션에서 많이 나타나는 취약점의 하나로 웹사이트 관리자가 아닌 이가 웹 페이지에 악성 스크립트를 삽입할 수 있는 취약점이다.

39.6.2 insertAdjacentHTML 메서드

  • Element.prototype.insertAdjacentHTML(position, DOMString) 메서드는 기존 요소를 제거하지 않고 위치를 지정해 새로운 요소를 삽입한다.
  • insertAdjacentHTML 메서드의 첫 번째 인수로는 전달할 위치를 받는다. 이때 첫 번째 인수로 ‘beforebegin’, ‘afterbegin’, ‘beforeend’, ‘afterend’ 이렇게 4가지를 받을 수 있다. 두 번째 인수로는 HTML 마크업 문자열을 파싱한다.

beforebegin : 형제요소로 앞에 afterbegin : 자식요소로 맨앞에 beforeend : 자식요소로 맨뒤에 afterend : 형제요소로 뒤에

<!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>

39.6.3 노드 생성과 추가

  • DOM은 노드를 직접 생성/삽입/삭제/치환하는 메서드도 제공한다.

  • 아래의 예제 코드로 과정을 확인해보자.

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

1. 요소 노드 생성

  • createElement 메서드로 요소 노드를 생성하고 반환한다.

  • createElement 메서드로 생성한 요소 노드는 위 그림과 같이 기존 DOM에 추가되지 않고 따로 존재하는 상태이다.
  • 따라서 생성된 요소 노드를 DOM에 추가하는 과정이 필요하다.

2. 텍스트 노드 생성

  • createTextNode 메서드를 사용하여 텍스트 노드를 생성하고 반환한다.

  • 텍스트 노드는 요소 노드의 자식 노드이다. 하지만 아직 자식 노드로 추가되지 않을 것을 확인할 수 있다.
  • 따라서 이후에 텍스트 노드를 요소 노드의 자식으로 추가하는 과정이 필요하다.

3. 텍스트 노드를 요소 노드의 자식 노드로 추가

  • appendChild 메서드는 매개변수 childNode에게 인수로 전달한 노드를 appendChild 메서드를 호출한 노드의 마지막 자식 노드로 추가한다.

  • appendChild 메서드를 통해 요소 노드와 텍스트 노드를 부자 관계로 연결해주었다.

4. 요소 노드를 DOM에 추가

  • appendChild 메서드로 요소 노드를 기존 DOM에 연결시켜준다.


39.6.4 복수의 노드 생성과 추가

  • DOM을 여러번 변경하게 되면 그 때마다 리플로우와 리페인트가 실행된다. 이는 비효율적이기때문에 컨테이너 요소를 사용해서 문제를 해결할 수 있다.
<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits"></ul>
  </body>
  <script>
    const $fruits = document.getElementById('fruits');

    // 컨테이너 요소 노드 생성
    const $container = document.createElement('div');

    ['Apple', 'Banana', 'Orange'].forEach(text => {
      // 1. 요소 노드 생성
      const $li = document.createElement('li');

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

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

      // 4. $li 요소 노드를 컨테이너 요소의 마지막 자식 노드로 추가
      $container.appendChild($li);
    });

    // 5. 컨테이너 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
    $fruits.appendChild($container);
  </script>
</html>
  • createElement로 전체의 큰 틀을 만들어서 그 안에 다 넣어서(1,2,3,4 과정) 한번만 추가(5번 과정)하면 리플로우를 한번만 실행하게되서 더욱 효율적이다!
    • 하지만 불필요한 컨테이너 요소도 추가되는 부작용이 있다.
  • 이 문제점은 DocumentFragment 노드를 통해 해결할 수 있다.

  • DocumentFragment 노드는 노드 객체의 일종으로, 부모 노드가 없어서 기존 DOM과는 별도로 존재한다는 특징이 있다.
  • DocumentFragment 노드를 DOM에 추가하면 자신은 제거되고 자신의 자식 노드만 DOM에 추가된다.

39.6.5 노드 삽입

마지막 노드로 추가

  • Node.prototype.appendChild 메서드는 인수로 전달받은 노드를 자신을 호출한 노드의 마지막 자식 노드로 DOM에 추가한다. 단, 노드를 추가할 위치를 지정할 수 없고 언제나 마지막 자식 노드로 추가한다.
<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
      <li>Banana</li>
    </ul>
  </body>
  <script>
    // 요소 노드 생성
    const $li = document.createElement('li');

    // 텍스트 노드를 $li 요소 노드의 마지막 자식 노드로 추가
    $li.appendChild(document.createTextNode('Orange'));

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


지정한 위치에 노드 삽입

  • Node.prototype.insertBefore(newNode, childNode) 메서드는 첫 번째 인수로 전달받은 노드를 두 번째 인수로 전달받은 노드 앞에 삽입한다.
  • 두 번쨰 인수로 전달받은 노드는 반드시 insertBefore 메서드를 호출한 노드의 자식 노드여야한다.
<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
      <li>Banana</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById('fruits');

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

    // 텍스트 노드를 $li 요소 노드의 마지막 자식 노드로 추가
    $li.appendChild(document.createTextNode('Orange'));

    // $li 요소 노드를 #fruits 요소 노드의 마지막 자식 요소 앞에 삽입
    $fruits.insertBefore($li, $fruits.lastElementChild);
    // Apple - Orange - Banana
  </script>
</html>

39.6.6 노드 이동

  • DOM에 이미 존재하는 노드를 appendChild 또는 insertBefore 메서드를 사용하여 DOM에 다시 추가하면 현재 위치에서 노드를 제거하고 새로운 위치에 노드를 추가한다. 즉, 노드가 이동한다.
<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
      <li>Banana</li>
      <li>Orange</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById('fruits');

    // 이미 존재하는 요소 노드를 취득
    const [$apple, $banana, ] = $fruits.children;

    // 이미 존재하는 $apple 요소 노드를 #fruits 요소 노드의 마지막 노드로 이동
    $fruits.appendChild($apple); // Banana - Orange - Apple

    // 이미 존재하는 $banana 요소 노드를 #fruits 요소의 마지막 자식 노드 앞으로 이동
    $fruits.insertBefore($banana, $fruits.lastElementChild);
    // Orange - Banana - Apple
  </script>
</html>

39.6.7 노드 복사

  • Node.prototype.cloneNode([deep: true | false]) 메서드는 노드의 사본을 생성하여 반환한다.
  • deep에 true를 인수로 전달하면 노드를 깊은 복사하여 모든 자손 노드가 포함된 사본을 생성하고, false를 전달하거나 생략하면 얕은 복사를 해서 노드 자신만의 사본을 생성한다.
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById('fruits'); const $apple =
    $fruits.firstElementChild; // $apple 요소를 얕은 복사하여 사본을 생성.
    텍스트 노드가 없는 사본이 생성된다. const $shallowClone =
    $apple.cloneNode(); // 사본 요소 노드에 텍스트 추가
    $shallowClone.textContent = 'Banana'; // 사본 요소 노드를 #fruits 요소
    노드의 마지막 노드로 추가 $fruits.appendChild($shallowClone); // #fruits
    요소를 깊은 복사하여 모든 자손 노드가 포함된 사본을 생성 const $deepClone =
    $fruits.cloneNode(true); // 사본 요소 노드를 #fruits 요소 노드의 마지막
    노드로 추가 $fruits.appendChild($deepClone);
  </script>
</html>

39.6.8 노드 교체

  • Node.prototype.replaceChild(newChild, oldChild) 메서드는 자신을 호출한 노드의 자식 노드를 다른 노드로 교체한다.
    • 첫 번째 매개변수 newChild : 교체할 새로운 노드
    • 두 번째 매개변수 oldChild : 이미 존재하는 교체될 노드
  • replaceChild 메서드는 자신을 호출한 노드의 자식인 oldChild 노드를 newChild 노드로 교체한다. oldChild 노드는 DOM에서 제거된다.
<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById('fruits');

    // 기존 노드와 교체할 요소 노드를 생성
    const $newChild = document.createElement('li');
    $newChild.textContent = 'Banana';

    // #fruits 요소 노드의 첫 번째 자식 요소 노드를 $newChild 요소 노드로 교체
    $fruits.replaceChild($newChild, $fruits.firstElementChild);
  </script>
</html>

39.6.9 노드 삭제

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

39.7 어트리뷰트

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

  • HTML 문서의 구성 요소인 HTML 요소는 여러 개의 어트리뷰트를 가질 수 있다.
  • HTML 요소의 동작을 제어하기 위한 추가적인 정보를 제공한다.
  • 대부분의 어트리뷰트는 모든 HTML 요소에서 사용될 수 있지만, type, value, checked 어트리뷰트는 input 요소에만 사용할 수 있는 경우도 있다.
  • HTML 문서가 파싱될 때 HTML 요소의 어트리뷰트는 어트리뷰트 노드로 변환되어 요소 노드와 연결된다.

39.7.2 HTML 어트리뷰트 조작

  • HTML 어트리뷰트 값을 참조하려면 Element.prototype.getAttribute(attributeName) 메서드를 사용하고, HTML 어트리뷰트 값을 변경하려면 Element.prototype.setAttribute(attributeName, attributeValue) 메서드를 사용한다.

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

  • HTML 어트리뷰트와 DOM 프로퍼티는 대부분 1:1 대응한다.

HTML 어트리뷰트

  • HTML 어트리뷰트의 역할은 HTML 요소의 초기 상태를 지정하는 것이다. -> 즉, HTML 어트리뷰트 값은 HTML 요소의 초기 상태를 의미하며 이는 변하지 않는다.

DOM 프로퍼티

  • 사용자가 입력한 최신 상태는 HTML 어트리뷰트에 대응하는 요소 노드의 DOM 프로퍼티가 관리한다.
  • DOM 프로퍼티는 사용자의 입력에 의한 상태 변화에 반응하여 언제나 최신 상태를 유지한다.
  • DOM 프로퍼티에 값을 할당하는 것은 사용자가 상태를 변경하는 행위와 같다.

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

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

  • data 어트리뷰트는 data-user-id. data-role과 같이 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>
</body>
</html>
  • dataset 프로퍼티는 HTML 요소의 모든 data 어트리뷰트의 정보를 제공하는 DOMStringMap 객체를 반환한다.
  • DOMStringMap 객체는 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>
  • 만약 존재하지 않는 이름을 키로 사용하여 dataset 프로퍼티에 값을 할당하면 HTML 요소에 data 어트리뷰트가 추가된다.
<!DOCTYPE html>
<html>
<body>
  <ul class="users">
    <li id="1" data-user-id="7621">Lee</li>
    <li id="2" data-user-id="9524">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 어트리뷰트를 추가한다.
    user.dataset.role = 'admin';
    console.log(user.dataset);
    /*
    DOMStringMap {userId: "7621", role: "admin"}
    -> <li id="1" data-user-id="7621" data-role="admin">Lee</li>
    */
  </script>
</body>
</html>

39.8 스타일

39.8.1 인라인 스타일 조작

  • HTMLElement.prototype.style 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티이며 요소 노드의 인라인 스타일을 취득하거나 추가 또는 변경한다.
<!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>

39.8.2 클래스 조작

className

  • Element.prototype.className 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티이며 요소 노드의 인라인 스타일을 취득하거나 추가 또는 변경한다.
  • className 프로퍼티는 문자열을 반환하기때문에 공백으로 구분된 여러 개의 클래스를 반환하는 경우 다루기가 불편하다.

classList

  • Element.prototype.classList 프로퍼티는 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 객체를 취득
    // 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>
  • DOMTokenList 객체가 제공하는 메서드

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

  • style 프로퍼티는 인라인 스타일만 반환하기 때문에 클래스를 적용한 스타일이나 상속을 통해 적용된 스타일은 style 프로퍼티로 참조할 수 없다.

39.9 DOM 표준


블로그에 작성한 파트 외 정리본은 아래의 깃헙에서 확인 가능합니다!

https://github.com/hyeun427/javascript-study

댓글남기기