6 minute read

Achievement Goals

  • DOM을 JavaScript로 조작하여 HTML Element를 추가하거나 삭제, 혹은 내용을 변경할 수 있다.

  • createElement - CREATE
  • querySelector, querySelectorAll - READ
  • textContent, id, classList, setAttribute - UPDATE
  • remove, removeChild, innerHTML = "" , textContent = "" - DELETE
  • appendChild - APPEND
  • innerHTML과 textContent의 차이

Advanced Challenge


노드 취득

HTML 구조나 내용 또는 스타일 등을 동적으로 조작하려면 먼저 요소 노드를 취득해야 한다.

  • id를 이용한 요소 노드 취득 : Document.prototype.getElementById()

  • 태그 이름을 이용한 요소 노드 취득 : Document.prototype/Element.prototype.getElementByTagName()

  • 클래스 이름을 이용한 요소 노드 취득 : Document.prototype/Element.prototype.getElementByClassName()

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

/* 전체 선택자: 모든 요소를 선택 */
\* { ... }

/* 태그 선택자: 모든 p 태그 요소를 모두 선택 */
p { ... }

/* id 선택자: id 값이 'foo'인 요소를 모두 선택 */
\#foo { ... }

/* class 선택자: class 값이 'foo'인 요소를 모두 선택 */
.foo { ... }

/* 어트리뷰트 선택자: input 요소 중에 type 어트리뷰트 값이 'text'인 요소를 모두 선택 */
input[type=text] { ... }

/* 후손 선택자: div 요소의 후손 요소 중 p 요소를 모두 선택 */
div p { ... }

/* 자식 선택자: div 요소의 자식 요소 중 p 요소를 모두 선택 */
div > p { ... }

/* 인접 형제 선택자: p 요소의 형제 요소 중에 p 요소 바로 뒤에 위치하는 ul 요소를 선택 */
p + ul { ... }

/* 일반 형제 선택자: p 요소의 형제 요소 중에 p 요소 뒤에 위치하는 ul 요소를 모두 선택 */
p ~ ul { ... }

/* 가상 클래스 선택자: hover 상태인 a 요소를 모두 선택 */
a:hover { ... }

/* 가상 요소 선택자: p 요소의 콘텐츠의 앞에 위치하는 공간을 선택
   일반적으로 content 프로퍼티와 함께 사용된다. */
p::before { ... }

Document.prototype/Element.prototype.querySelector() 인수로 전달한 CSS 선택자를 만족시키는 하나의 요소 노드를 탐색하여 반환

Document.prototype/Element.prototype.querySelectorAll() 인수로 전달한 CSS 선택자를 만족시키는 모든 요소 노드를 탐색하여 반환.


노드 탐색

  • 자식 노드 탐색

    Node.prototype.childNodes							// 자식 노드를 모두 탐색해 DOM 컬렉션 객체인 NodeList에 담아 반환
    Element.prototype.childNodes					// 자식 노드중 요소 노드만 탐색해 DOM 컬렉션 객체인 HEMLCollection에 담아 반환
    Node.prototype.firstChild							// 첫 번째 자식 노드를 반환. 반환한 노드는 텍스트 노드나 요소 노드
    Node.prototype.lastChild							// 마지막 자식 노드를 반환. 반환한 노드는 텍스트 노드나 요소 노드
    Element.prototype.firstElementChild		// 첫 번째 자식 노드를 반환. 요소 노드만 반환
    Element.prototype.lastElementChild		// 마지막 자식 노드를 반환. 요소 노드만 반환
    
  • 부모 노드 탐색

    Node.prototype.parenNode // 텍스트 노드는 리프 노드이므로 부모 노드가 텍스트 노드인 경우는 없다.

  • 형제 노드 탐색

    Node.prototype.previousSibling							// 부모 노드가 같은 형제 요소 노드 중 자신의 이전 형제 요소 노드를 탐색하여 반환
    Node.prototype.nextSibling									// 부모 노드가 같은 형제 요소 노드 중 자신의 다음 형제 요소 노드를 탐색하여 반환
    Element.prototype.previousElementSibling		// 부모 노드가 같은 형제 요소 노드 중 자신의 이전 형제 요소 노드를 탐색하여 반환
    Element.prototype.nextElementSibling				// 부모 노드가 같은 형제 요소 노드 중 자신의 다음 형제 요소 노드를 탐색하여 반환
    

DOM 조작

  • nodeValue : 노드의 값을 반환. 텍스트 노드의 값만을 반환, 텍스트 노드가 아닌 노드 객체의 nudeValue 프로퍼티를 참조하면 null 반환

    // 해당 텍스트 노드의 부모 요소 노드를 선택한다.
    const one = document.getElementById('one');
    console.dir(one); // HTMLLIElement: li#one.red
      
    // nodeName, nodeType을 통해 노드의 정보를 취득할 수 있다.
    console.log(one.nodeName); // LI
    console.log(one.nodeType); // 1: Element node
      
    // firstChild 프로퍼티를 사용하여 텍스트 노드를 탐색한다.
    const textNode = one.firstChild;
      
    // nodeName, nodeType을 통해 노드의 정보를 취득할 수 있다.
    console.log(textNode.nodeName); // #text
    console.log(textNode.nodeType); // 3: Text node
      
    // nodeValue 프로퍼티를 사용하여 노드의 값을 취득한다.
    console.log(textNode.nodeValue); // Seoul
      
    // nodeValue 프로퍼티를 이용하여 텍스트를 수정한다.
    textNode.nodeValue = 'Pusan';
    

4.3 HTML 콘텐츠 조작(Manipulation)

  • textContent

    요소의 텍스트 콘텐츠를 취득 또는 변경한다. 이때 마크업은 무시된다. textContent를 통해 요소에 새로운 텍스트를 할당하면 텍스트를 변경할 수 있다. 이때 순수한 텍스트만 지정해야 하며 마크업을 포함시키면 문자열로 인식되어 그대로 출력된다.

<!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>
    <script>
    const ul = document.querySelector('ul');

    // 요소의 텍스트 취득
    console.log(ul.textContent);
    /*
            Seoul
            London
            Newyork
            Tokyo
    */

    const one = document.getElementById('one');

    // 요소의 텍스트 취득
    console.log(one.textContent); // Seoul

    // 요소의 텍스트 변경
    one.textContent += ', Korea';

    console.log(one.textContent); // Seoul, Korea

    // 요소의 마크업이 포함된 콘텐츠 변경.
    one.textContent = '<h1>Heading</h1>';

    // 마크업이 문자열로 표시된다.
    console.log(one.textContent); // <h1>Heading</h1>
    </script>
  </body>
</html>
  • innerText

    innerText 프로퍼티를 사용하여도 요소의 텍스트 콘텐츠에만 접근할 수 있다. 하지만 아래의 이유로 사용하지 않는 것이 좋다.비표준이다.CSS에 순종적이다. 예를 들어 CSS에 의해 비표시(visibility: hidden;)로 지정되어 있다면 텍스트가 반환되지 않는다.CSS를 고려해야 하므로 textContent 프로퍼티보다 느리다

  • innerHTML

    해당 요소의 모든 자식 요소를 포함하는 모든 콘텐츠를 하나의 문자열로 취득할 수 있다. 이 문자열은 마크업을 포함한다.

const ul = document.querySelector('ul');

// innerHTML 프로퍼티는 모든 자식 요소를 포함하는 모든 콘텐츠를 하나의 문자열로 취득할 수 있다. 이 문자열은 마크업을 포함한다.
console.log(ul.innerHTML);
// IE를 제외한 대부분의 브라우저들은 요소 사이의 공백 또는 줄바꿈 문자를 텍스트 노드로 취급한다
/*
        <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>
*/

innerHTML 프로퍼티를 사용하여 마크업이 포함된 새로운 콘텐츠를 지정하면 새로운 요소를 DOM에 추가할 수 있다.

const one = document.getElementById('one');

// 마크업이 포함된 콘텐츠 취득
console.log(one.innerHTML); // Seoul

// 마크업이 포함된 콘텐츠 변경
one.innerHTML += '<em class="blue">, Korea</em>';

// 마크업이 포함된 콘텐츠 취득
console.log(one.innerHTML); // Seoul <em class="blue">, Korea</em>

위와 같이 마크업이 포함된 콘텐츠를 추가하는 것은 크로스 스크립팅 공격(XSS: Cross-Site Scripting Attacks)에 취약하다.

// 크로스 스크립팅 공격 사례

// 스크립트 태그를 추가하여 자바스크립트가 실행되도록 한다.
// HTML5에서 innerHTML로 삽입된 <script> 코드는 실행되지 않는다.
// 크롬, 파이어폭스 등의 브라우저나 최신 브라우저 환경에서는 작동하지 않을 수도 있다.
elem.innerHTML = '<script>alert("XSS!")</script>';

// 에러 이벤트를 발생시켜 스크립트가 실행되도록 한다.
// 크롬에서도 실행된다!
elem.innerHTML = '<img src="#" onerror="alert(\'XSS\')">';

DOM 조작 방식

innerHTML 프로퍼티를 사용하지 않고 새로운 콘텐츠를 추가할 수 있는 방법은 DOM을 직접 조작하는 것이다. 한 개의 요소를 추가하는 경우 사용한다. 이 방법은 다음의 수순에 따라 진행한다.

  1. 요소 노드 생성 createElement() 메소드를 사용하여 새로운 요소 노드를 생성한다. createElement() 메소드의 인자로 태그 이름을 전달한다.
  2. 텍스트 노드 생성 createTextNode() 메소드를 사용하여 새로운 텍스트 노드를 생성한다. 경우에 따라 생략될 수 있지만 생략하는 경우, 콘텐츠가 비어 있는 요소가 된다.
  3. 생성된 요소를 DOM에 추가 appendChild() 메소드를 사용하여 생성된 노드를 DOM tree에 추가한다. 또는 removeChild() 메소드를 사용하여 DOM tree에서 노드를 삭제할 수도 있다.
  • createElement(tagName)

    태그이름을 인자로 전달하여 요소를 생성한다.Return: HTMLElement를 상속받은 객체모든 브라우저에서 동작한다.

  • createTextNode(text)

    텍스트를 인자로 전달하여 텍스트 노드를 생성한다.Return: Text 객체모든 브라우저에서 동작한다.

  • appendChild(Node)

    인자로 전달한 노드를 마지막 자식 요소로 DOM 트리에 추가한다.Return: 추가한 노드모든 브라우저에서 동작한다.

  • removeChild(Node)

    인자로 전달한 노드를 DOM 트리에 제거한다.Return: 추가한 노드모든 브라우저에서 동작한다.


innerHTML vs. DOM 조작 방식 vs. insertAdjacentHTML()

innerHTML

장점 단점
DOM 조작 방식에 비해 빠르고 간편하다. XSS공격에 취약점이 있기 때문에 사용자로 부터 입력받은 콘텐츠(untrusted data: 댓글, 사용자 이름 등)를 추가할 때 주의하여야 한다.
간편하게 문자열로 정의한 여러 요소를 DOM에 추가할 수 있다. 해당 요소의 내용을 덮어 쓴다. 즉, HTML을 다시 파싱한다. 이것은 비효율적이다.
콘텐츠를 취득할 수 있다.  

DOM 조작 방식

장점 단점
특정 노드 한 개(노드, 텍스트, 데이터 등)를 DOM에 추가할 때 적합하다. innerHTML보다 느리고 더 많은 코드가 필요하다.

insertAdjacentHTML()

장점 단점
간편하게 문자열로 정의된 여러 요소를 DOM에 추가할 수 있다. XSS공격에 취약점이 있기 때문에 사용자로 부터 입력받은 콘텐츠(untrusted data: 댓글, 사용자 이름 등)를 추가할 때 주의하여야 한다.
삽입되는 위치를 선정할 수 있다.  

결론

innerHTML과 insertAdjacentHTML()은 크로스 스크립팅 공격(XSS: Cross-Site Scripting Attacks)에 취약하다. 따라서 untrusted data의 경우, 주의하여야 한다. 텍스트를 추가 또는 변경시에는 textContent, 새로운 요소의 추가 또는 삭제시에는 DOM 조작 방식을 사용하도록 한다.

참고 문서

모던 자바스크립트 Deep Dive