#1 DOM part.2

정지훈·2021년 1월 7일
0

자식 노드 탐색

이전에 NodeList와 HTMLCollection에 대해서 공부했었다.

이제 DOM 트리의 노드를 옮겨 다니며 부모, 형제, 자식 노드 등을 탐색 해야 할 때가 있다. 다음 예제를 살펴보자.

<ul id="fruits">
   <li class="apple">Apple</li>  
   <li class="banana">Banana</li>  
   <li class="orange">Orange</li>  
</ul>

ul요소는 3개의 자식 요소를 갖는다. 이때 부모의 id를 취득한 후에 자식을 찾을 수 있다.

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li class="apple">Apple</li>
      <li class="banana">Banana</li>
      <li class="orange">Orange</li>
    </ul>
  </body>
  <script>

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

    // #fruits 요소의 모든 자식 노드를 탐색한다.
    // 텍스트 노드도 포함되어 있다.
    console.log($fruits.childNodes);
    // NodeList(7) [text, li.apple, text, li.banana, text, li.orange, text]

    // #fruits 요소의 모든 자식 노드를 탐색한다.
    //  요소 노드만 포함되어 있다.
    console.log($fruits.children);
    // HTMLCollection(3) [li.apple, li.banana, li.orange]

    // #fruits 요소의 첫 번째 자식 노드를 탐색한다.
    // 텍스트 노드를 반환할 수도 있다.
    console.log($fruits.firstChild); // #text

    // #fruits 요소의 마지막 자식 노드를 탐색한다.
    // lastChild 프로퍼티는 텍스트 노드를 반환할 수도 있다.
    console.log($fruits.lastChild); // #text

    // #fruits 요소의 첫 번째 자식 노드를 탐색한다.
    //  요소 노드만 반환한다.
    console.log($fruits.firstElementChild); // li.apple

    // #fruits 요소의 마지막 자식 노드를 탐색한다.
    // 요소 노드만 반환한다.
    console.log($fruits.lastElementChild); // li.orange
  </script>
</html>

이렇게 텍스트 노드가 포함되어 잇는 노드 탐색 프로퍼티도 있고 포함하지 않는 프로퍼티도 있다.

내가 추천하는 요소노드찾는거는 텍스트 노드가 포함하지 않고 요소노드만 반환하는 프로퍼티가 좋은 거 같다.

즉 텍스트 노드를 포함하지 않는 프로퍼티는

  • Element.prototype.children
  • Element.proptotype.firstElementChild
  • Element.proptotype.lastElementChild

이 3가지가 텍스트 노드를 포함하지 않고 요소노드만 반환하는 아이들이다.

텍스트노드를 포함하고 싶으면

  • Node.prototype.childNodes
  • Node.protptype.firstChild
  • Node.prototype.lastChild

이 3가지 프로퍼티를 사용하면 된다.

그리고 만약 자식노드가 존재하는지 확인하고 싶으면

Node.prototype.hasChildNodes

이 메서드를 이용하면 된다. 이것은 자식노드가 존재하면 true이고 존재하지 않으면 false를 반환한다.

형제 요소 탐색

형제 요소도 자식요소와 마찬가지로 텍스트 노드를 포함하는 프로퍼티도 있다.

형제요소를 찾을때는

  • Node.prototype.previousSibling
  • Node.prototype.nextSibling
  • Element.prototype.previousElementSibling
  • Element.prototype.nextElementSibling

이 있다. 이 중 Node.prototype인 경우 텍스트 요소도 포함할 할 수 있기 때문에 텍스트 노드가 필요 없는 상황이면 Element.prototype을 이용하는게 좋다.

textContent

Node.prototype.textContent프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티로서 요소 노드의 텍스트와 모든 자손 노드의 텍스트를 모두 취득하거나 변경할 수 있다.

요소노드의 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!
  </script>
</html>

근데 이때 가져올때 마크업은 모두 무시가 된다.

만약 위에 foo의 text를 바꾸고 싶으면

document.getElementById('foo').textContent = 'Hi! <span>there!</span>;

이렇게 하고 싶지만 HTML 마크업이 파싱되지 않는다.

이런 상태가 된다.

DOM 조작

DOM을 조작하면 새로운 노드를 생성해서 DOM에 추가하거나 기존 노드를 삭제 또는 교체하는 것을 말한다. 하지만 DOM조작에 의해 DOM에 새로운 노드가 추가 되거나 삭제되면 리플로우와 리페인트가 발생하는 원인이 되므로 성능에 영향을 준다.

innerHTML

Element.prototype.innerHTML 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티다. 요소노드의 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>

앞서 textContent와 다르게 문자열을 할당하면 HTML마크업도 파싱되어서 요소노드의 자식 노드로 DOM에 반영한다. innerHTML 프로퍼티를 사용하면 HTML마크업 문자열로 간단히 DOM 조작이 가능하다.

주의해야하는게 HTML마크업 문자열은 렌더링 엔진에 의해 파싱되어 요소노드의 자식으로 DOM에 반영된다. 이때 사용자로부터 입력 받은 데이터를 그대로 innerHTML 프로퍼티에 할당하는 것은 크로스 사이트 스크립팅 공격에 취약하므로 위험하다.

크로스 사이트 스크립팅 공격(XSS: Cross-Site-Scripting)
게시판이나 웹 메일 등에 자바 스크립트와 같은 스크립트 코드를 삽입 해 개발자가 고려하지 않은 기능이 작동하게 하는 치명적일 수 있는 공격이다. 또한 대부분의 웹 해킹 공격 기법과는 다르게 클라이언트 즉, 사용자를 대상으로 한 공격이다

이처럼 innerHTML프로퍼티를 사용한 DOM 조작은 구현이 간단하고 직관적이라는 장점이 있지만 크로스 사이트 스크립팅 공격에 취약한 단점도 있다. 그래서 HTML 새니티제이션을 이용해 사용자로부터 입력 받은 데이터에 의해 발생할 수 잇는 크로스 사이트 스크립팅 공격을 예방하기 위해 잠재적 위험을 제거하는 기능을 추가한다. 이 새니티제이션 함수를 직접 구현할 수도 있겠지만 DOMPurify라이브러리를 사용하는 것을 권장한다.

그리고 innerHTML 프로퍼티의 또 다른 단점은 요소 노드의 innerHTML프로퍼티에 HTML 마크업 문자열을 할당하는 경우 요소 노드의 모든 자식 노드를 제거하고 할당한 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>';
  </script>
</html>

이처럼 innerHTML 프로퍼티에 HTML 마크업 문자열을 할당하면 유지되어도 좋은 기존의 자식 노드까지 모두 제거하고 다시 처음부터 새롭게 자식노드를 생성 하여 DOM에 반영해서 이는 효율적이지 않다.

innerHTML프로퍼티의 단점은 이뿐만이 아니다 새로운 요소를 삽입할때 삽입될 위치를 지정할 수 없다는 단점도 있다. innderHTML프로퍼티는 복잡하지 않은 요소를 새롭게 추가할 때 유용하지만 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입해야 할 때는 사용하지 않는 것이 좋다.

insertAdjacentHTML 메서드

이 메서드는 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입한다.

첫번째 인수로 전달할 수 있는 문자열은 ‘beforebegin’, ‘afterbegin’, ‘beforeend’, ‘afterend’의 4가지다.

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

DOM 노드 생성

요소 노드 생성

Document.prototype.createElement(tagName)메서드는 요소 노드를 생성하여 반환합니다.

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

요소 노드를 DOM에 추가

Node.prototype.appendChild메서드를 사용하여 부자 관걔로 연결한 요소노드를 #fruits요소 노드의 마지막 자식 요소로 추가한다.

$fruits.appendChild($li);

복수의 노드 생성과 추가

이번에는 여러개의 요소 노드를 생성하여 DOM에 추가해보자.

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

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

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

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

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

이렇게 하면 3개의 요소노드를 생성하여 DOM에 3번 추가하므로 DOM이 3번 변경된다. 이때 리플로우와 리페인트가 3번 실행되는 것은 가급적 횟수를 줄이는 편이 성능에 유리하다. 그래서 컨테이너 요소를 미리 생성하고 3개의 요소노드를 컨테이너 요소에 자식 노드로 추가하고 컨테이너 요소를 #fruits요소에 자식 으로 추가한다면 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>

그런데 불필요한 div가 DOM에 추가되는 부작용이 있다. 이는 바람직하지 않아서 사용할 수 있는게 DocumentFragment노드를 통해 해결할 수있다. 이 노드는 부모 노드가 없어서 기존 DOM과는 별도로 존재한다는 특징이 있다.

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

지정한 위치에 노드 삽입

Node.prototype.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>

두 번째 인수로 전달받은 노드는 반드시 insertBefore 메서드를 호출한 노드의 자식 노드이어야 한다. 그렇지 않으면 DOMException 에러가 발생한다.

어트리뷰트

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

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

글로벌 어트리뷰트(id,class,style,title...)와 이벤트 핸들러 어트리뷰트는 모든 HTML요소에서 공통적으로 사용할 수 있지만 특정 HTML요소에만 한정적으로 사용 가능한 어트리뷰트도 잇다. type,value,checked 어트리뷰트는 input 요소에만 사용할 수 있다.

<!DOCTYPE html>
<html>
<body>
  <input id="user" type="text" value="ungmo2">
  <script>
    // 요소 노드의 attribute 프로퍼티는 요소 노드의 모든 어트리뷰트 노드의 참조가 담긴 NamedNodeMap 객체를 반환한다.
    const { attributes } = document.getElementById('user');
    console.log(attributes);
    // NamedNodeMap {0: id, 1: type, 2: value, id: id, type: type, value: value, length: 3}

    // 어트리뷰트 값 취득
    console.log(attributes.id.value); // user
    console.log(attributes.type.value); // text
    console.log(attributes.value.value); // ungmo2
  </script>
</body>
</html>

HTML 어트리뷰트 조작

어트리뷰트를 조작할 수 있다.
HTML 어트리뷰트 값을 참조하려면 getAttribute 메서드를 사용하고 어트리뷰트 값을 변경하려면 setAttribute(attributeName, attributeValue) 메서드를 사용하면 된다.

<!DOCTYPE html>
<html>
<body>
  <input id="user" type="text" value="ungmo2">
  <script>
    const $input = document.getElementById('user');

    // value 어트리뷰트 값을 취득
    const inputValue = $input.getAttribute('value');
    console.log(inputValue); // ungmo2

    // value 어트리뷰트 값을 변경
    $input.setAttribute('value', 'foo');
    console.log($input.getAttribute('value')); // foo
  </script>
</body>
</html>

특정 어트리뷰트를 삭제하고 싶으면 removeAttribute를 이용한다.

스타일

인라인 스타일 조작

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>

클래스 조작

.으로 시작하는 클래스 선택자를 사용하여 class를 미리 정의한 다음에 class 어트리뷰트 값을 변경하여 HTML 요소의 스타일을 변경할 수도 있다.
이때 class 어트리뷰트를 조작하려면 DOM프로퍼티를 사용한다.

단 class 어트리뷰트에 대응하는 DOM 프로퍼티는 class가 아니라 className과 classList이다. class는 예약어 이기 때문이다.

className

className을 이용하면 class 어트리뷰트 값을 취득할 수 있다.
단 문자열 공백도 포함하니 비추천한다.

classList

class 어트리뷰트의 정보를 담은 DOMTokenList 객체를 반환한다.
이것도 살아있는 객체인데 이걸 쓰는게 className보다 좋다.

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

여기서 다양한 메서드를 제공하는데

  • add: 클래스 추가
  • remove: 삭제
  • item: index에 해당하는 클래스 반환
  • contains: class 어트리뷰트 포함 확인
  • replace: class어트리뷰트에서 첫번째 인수로 전달한 문자열을 두번째 인수로 전달한 문자열로 변경
  • toggle: class어트리뷰트에 인수로 전달한 문자열과 일치하는 클래스가 존재하면 제거 아니면 추가

출저: https://poiemaweb.com/fastcampus/dom

0개의 댓글