ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Group Study, 모던 자바스크립트 Deep Dive] - 39 DOM
    Front-end/Javascript 2023. 2. 20. 02:25
    반응형

    Group study background

    나만 그런건지는 모르겠지만, 실무를 하다보면 잊어버리는 개념들이 있다.

    가끔 FE 뉴비인분들에게 질문을 받는데, 아리송 할때만큼 쪽팔릴때가 없었다.

    인간은 망각의 동물이라고 교수님께서 말씀하셨지만 반복 학습의 힘을 믿는다. React 오픈카톡방에서 모집한 스터디원분들과 함께 "모던 자바스크립트 Deep Dive" 1권 톺아보기를 시작한다!

     

    정보 전달용이 아닌 개인 스터디 레코딩용 포스트입니다.


    39.0 Preface

    DOM, Browser Rendering, ... 등 어쩌면 FE 개발자의 정석이라고 해도 과언이 아니다.

    DOM(Document Object Model)은 HTML 문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API, 즉 프로퍼티와 메서드를 제공하는 트리 자료구조다.


    39.1 노드

    39.1.1 HTML 요소와 노드 객체

    HTML element는 HTML 문서를 구성하는 개별적인 요소이며 렌더링 엔진에 의해 파싱되어 DOM을 구성하는 element Node 객체로 변환된다. 

    [그림 39-01]

    HTML 문서는 HTML element들의 집합으로 이뤄지며, element와 중첩 관계에 의해 계층적인 parent-child 관계가 형성된다. HTML element 간의 관계를 바녕ㅇ하여 HTML 요소를 개체화한 모든 노드 객체들을 트리 자료 구조로 구성한다. 노드 객체들로 구성된 트리 자료구조DOM 또는 DOM Tree라 한다. 

     

    39.1.2 노드 객체의 타입

     

    [그림 39-02]

     

    [그림 39-03]

    그림 39-02의 HTML 문서를 렌더링 엔진이 파싱하면 그림 39-03처럼 파싱하여 DOM을 생성한다.

     

    DOM의 중요한 노드 타입 4가지

    - Document Node

     DOM 트리의 최상위에 존재하는 루트 노드로 document 객체 자체, 브라우저가 렌더링한 HTML 문서 자체를 가리키는 객체이며 전역 객체 window의 document 프로퍼티에 바인딩되어 있다. (window.document or document)

     브라우저 환경의 모든 JS 코드는 script 태그에 분리되어 있어도 하나의 전역 객체 window를 공유한다. 따라서 모든 JS 코드는 전역 객체 window의 document 프로퍼티에 바인딩 되어 있는 하나의 document 객체를 바라본다. 즉, document 객체는 HTML에 하나만 존재하며 DOM 트리의 노드들에 접근하기 위한 entry point 역할을 담당한다.

    - Element Node

    HTML element를 가리키는 객체, 이 element 간의 중첩에 의해 부자 관계를 가지며, 이 부자 관계를 통해 정보를 구조화한다. 따라서 문서의 구조를 표현한다고 할 수 있다.

    - Attribute Node

    말 그대로, HTML element의 attribute를 가리키는 객체. 당연한 이야기지만, 부모 element node에 연결이 되지 않고, 관련된 element node에 직접 연결되어 있음.

    - Text Node

    HTML element의 텍스트를 가리키는 객체. child node를 가질 수 없는 leaf node이므로 dom 트리의 최종단. 

     

    39.1.3 노드 객체의 상속 구조

    DOM을 구성하는 노드 객체는 자신의 구조와 정보를 제어할 수 있는 DOM API를 사용할 수 있다. 이를 통해 자신의 부모, 형제, 자식을 탐색할 수 있으며 attribute와 텍스트를 조작할 수도 있다.

    [그림 39-04] 우측 방향으로의 상하 관계

     

    [그림 39-05] 


    예를 들어, Input 요소 노드 객체는 프로토타입 체인에 있는 모든 프로토타입의 프로퍼티나 메서드를 상속받아 사용할 수 있다. 노드 객체의 상속 구조는 아래처럼 개발자 도구의 Elements 패널 오축의 Properties 패널에서 확인할 수 있음.

    [그림 39-06] 

     

    노드 객체에는 노드 객체의 종류(타입)에 상관없이 공통으로 갖는 기능도 있고, 노드 타입에 따라 고유한 기능도 있다. EventTarget interface(EventTarget.addEventListener, EventTarget.removeEventListener ... 등), Node interface(Node.nodeType, node.nodeName, Node.parentNode, Node.childNodes, ...등)

     정리하자면 DOM은 HTML 문서의 계층적 구조와 정보를 표현하는 것은 물론 노드 객체의 종류, 즉 노드 타입에 따라 필요한 기능을 프로퍼티와 메서드의 집합인 DOM API로 제공한다. 이 DOM API를 통해 HTML의 구조나 내용 또는 스타일 등을 동적으로 조작할 수 있다


    39.2 Element Node 취득

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

    39.2.1 id를 이용한 Element Node 취득

    Document.prototype.getElementByID 메서드는 인수로 전달한 id attribute 값을 갖는 하나의 element node를 탐색해 반환한다. document.getEelementById('${id}')를 통해서 얻을 수 있다. HTML 요소에 id attribute를 부여하면 id 값과 동일한 이름의 전역 변수가 암묵적으로 선언되고 해당 노드 객체가 할당되는 부수 효과가 있다.

     

    39.2.2 Tag 이름을 이용한 Element Node 취득

    Document.prototype/Element.prototype.getElementsByTagName 메서드는 인수로 전달한 태그 이름을 갖는 모든 요소 노드들을 탐색하여 유사 배열 객체이면서 이터러블인 HTML Collecion 객체로 반환한다. 

     

     

    39.2.3 Class를 이용한 Element Node 취득

    Document.prototype/Element.prototype.getElementsByClassName 메서드는 인수로 전달한 class attribute 값을 갖는 모든 요소 노드들을 탐색하여 반환한다. 인수로 전달할 class 값은 공백으로 구분하여 여러 개의 class를 지정할 수 있다. 

     

    39.2.4 CSS selector을 이용한 Element Node 취득

    CSS Selector는 스타일을 적용하고자 하는 HTML element를 특정할 때 사용하는 문법이다. 

    [그림 39-07]

    Document.prototype/Element.prototype.querySelectorAll 메서드는 인수로 전달한 CSS 선택자를 만족시키는 모든 요소 노드를 탐색하여 반환한다. e.g(document.querySelectorAll('ul > li')

     

    39.2.5 특정 element 노드를 취득할 수 있는지 확인

    Element.prototype.matches 메서드는 인수로 전달한 CSS 선택자를 통해 특정 요소 노드를 취득 할 수 있는지 확인하여 Boolean 값으로 반환한다.

     

    39.2.6 HTMLCollection과 NodeList

    HTML Collection은 언제나 live 객체로 동작하여 노드 객체의 상태 변화를 실시간으로 반영하지만, NodeList는 대부분 과거의 정적 상태를 유지하는 None-live 객체로 동작한다.

     

    HTMLCollection

    HTMLCollection 객체는 실시간으로 노드 객체의 상태 변경을 반영하여 요소를 제거할 수 있기 때문에 HTML Collection 객체를 for 문으로 순회하면서 노드 객체의 상태를 변경해야 할 때 주의해야 한다.

     

    [예제 39-18]

    ...(생략)
    <body>
      <ul>
        <li class="red">A</li>
        <li class="red">B</li>
        <li class="red">C</li>
      </ul>
      <script>
       const $elems = document.getElementByClassName('red')
       console.log($elems) //HTMLCollection(3) [li.red, li.red, li.red]
       
       // not intended
       for (let i = 0; i < $elems.lenght; i++) $elems[i].className = 'blue'
       
       console.log($elems) //HTMLCollection(1) [li.red]
       
       
       // solution1 역방향 순회
       for (let i = $elems.length - 1; i >= 0; i--) $elems[i].className = 'blue'
       
       // solution2 while을 이용해 노드 객체 length
       let i = 0;
       while($elems.length > i)  $elems[i].className = 'blue'
      </script>
    </body>
    ...(생략)

     

    NodeList

    HTMLCollection 객체의 부작용을 해결하기 위해 NodeList를 반환하는 querySelectorAll 메서드를 사용할 수 있다. 하지만 childNodes 프로퍼티가 반환하는 NodeList 객체는 HTMLCollection 객체와 같이 실시간으로 노드 객체의 상태 변경을 반영하는 Live 객체로 동작하므로 주의가 필요하다.

     

    이처럼 상태 변경과 상관없이 안전하게 DOM 컬렉션을 사용하려면 Spread 문법이나 Array.from 메서드를 사용하여 배열로 변환하여 사용하는 것이 낫다. 

     


    39.3 Element 탐색

    DOM은 트리 상의 노드를 탐색 할 수 있도록 Node, Element 인터페이스는 트리 탐색 프로퍼티를 제공한다.

    node 탐색 프로퍼티는 모두 접근자 프로퍼티로 getter만 존재하는 읽기 전용 접근자 프로퍼티다.

    [그림 39-08]

    39.3.1 공백 텍스트 노드

    HTML element 사이의 스페이스, 탭, 줄바꿈(개행) 등의 공백 문자는 공백 텍스트 노드를 생성한다.

     

    39.3.2 자식 노드 탐색

     

    [그림 39-09]

    39.3.3 자식 노드 확인

    자식 노드가 존해하는지 확인하려면 Node.prototype.hasChildNodes 메서드를 사용한다. 

     


    39.4 노드 정보 취득

    노드 객체에 대한 정보를 취득하기 위해선 다음과 같은 노드 정보 프로퍼티를 사용한다.

    [그림 39-10]


    39.5 Element 노드의 텍스트 조작

    39.5.1 nodeValue

    Node.prototype.nodeValue 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티다 따라서 nodeValue 프로퍼티는 참조와 할당 모두 가능하다.

    [예제 39-20]

    ...(생략)
    <body>
     <div id="foo">Hello</div>
    </body>
     const $foo = docuemnt.getEelementById('foo')
     console.log($foo.nodeValue)//null
     
     const $textNode = $foo.firstChild
     console.log($textNode.nodeValue)
     
     
     $textNode.nodeValue = 'World'
     
    ...(생략)

     

    39.5.2 textContent

    textContent를 참조하면 Element Node의 HTML 마크업은 무시하고 콘텐츠 영역의 텍스트를 모두 반환한다.

     


    39.6 DOM 조작

    DOM 조작은 새로운 노드를 생성하여 DOM에 추가하거나 기존 노드를 삭제 || 교체하는 것이기 떄문에 reflow && repaint가 발생한다. 따라서 성능에 영향을 주게 되어 성능 최적화를 위해 주의해 다루어야 한다.

    39.6.1 innerHTML

    element node의 innerHTML 프로퍼티를 참조하면 컨텐츠 영역 내에 포함한 모든 HTML 마크업을 문자열로 반환한다.(getter/setter)

    사용자로부터 입력받은 데이터를 그대로 innerHTML에 할당하는것은 XSS에 취약하므로 위험하지만 HTML5는 innerHTML 프로퍼티로 삽인된 script 요소 내의 자바스크립트 코드를 실행하지 않는다.

    innerHTML 프로퍼티를 조작하는 경우 모든 자식 노드를 제거하고 할당한 HTML 마크업 문자열을 파싱하여 DOM을 변경하는 것이다.

    39.6.2 insertAdjacentHTML 

    innerHTML과 달리 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입한다.

    하지만 XSS에 취약한 것은 동일하다.

    [그림 39-11]

    [예제 39-21]

    (생략)
    <!-- beforebegin -->
    <div id="foo">
      <!-- afterbegin -->
      text
      <!-- beforeend -->
    </div>
    <!-- afterend -->
    
    
    <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>
    (생략)

    39.6.3 노드 생성과 추가

    [예제 39-22]

    (생략)
    <ul id="fruits">
     <li>Apple</li>
    </ul>
    
    
    <script>
     const $fruits = document.getElementById('fruits')
    
     // 1. 요소 노드 생성
     const $li = document.createEelement('li');
     
     // 2. 텍스트 노드 생성
     const textNode = document.createTextNode('Banana');
     
     // 3.텍스트 노드를 $li 요소 노드의 자식 노드로 추가
     $li.appendChild(textNode);
     
     // 4. $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
     $fruits.appendChild($li)
    </script>
    (생략)

     

    39.6.4 복수의 노드 생성과 추가

    forEach로 복수의 노드를 생성해 붙이는 경우는 dom의 N번 변경으로 비효율적이며 div를 만들어 붙이는 것은 불필요한 노드를 생성한다. 

    따라서 DocumentFragment 노드를 사용할 수 있는데, 기존 DOM과는 별도로 존재한다. DocumentFragment 노드에 자식 노드를  추가해도 DOM에는 어떤 변경도 발생하지 않고 자신만 제거되고 자식의 노드만 DOM에 추가된다. 아래 예제와 같이 하면 reflow, repaint 모두 한 번만 실행된다.

    [예제 39-23] 

    (생략)
    <body>
     <ul id="fruits"></ul>
    </body>
    <scripts>
     const $fruits = document.getElementById('fruits');
     
     //DocumentFragment
     const $fragment = document.createDocumentFragment()
     ['A, 'B', 'C'].foreEach(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)
    (생략)

     

    [그림 39-12]

    39.6.6 노드 이동

    DOM에 이미 존재하는 노드를 appendChild 또는 insertBefore 메서드를 사용하여 DOM에 다시 추가하면 현재 위치에서 노드를 제거하고 새로운 위치에 노드를 추가하므로 노드가 이동한다.

    39.6.7 노드 복사

    Node.prototype.clondNode([deep: true | false]) 메서드는 노드의 사본을 생성하여 반환한다. 얀튼 복사로 생성된 요소 노드는 자손 노드를 복하자이 않으므로 텍스트 노드도 없다.

    39.6.7 노드 교체

    Node.prototype.replaceChild(newChild, oldChild) 메서드는 자신을 호출한 노드의 자식 노드를 다른 노드로 교체한다 

    39.6.7 노드 삭제

     

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


    39.7 Attribute

    39.7.3 HTML attribute vs DOM property

    element 노드 객체에는 HTML attribute에 대응하는 프로퍼티가 존재한다. 이 DOM 프로퍼티들은 HTML attribute 값을 초기값으로 갖고 있다.

    HTML attribute는 다음과 같이 DOM에서 중복 관리되고 있는 것처럼 보인다.

    1. element 노드의 attribute property에서 관리하는 attribute 노드
    2. html attriute에 대응하는 element node의 property

     

    HTML attribute의 역할은 HTML 요소의 초기 상태를 지정하는 것이다. 즉, HTML attribute 값은 HTML element의 초기 상태를 의미하며 이는 변하지 않는다.

     

    첫 렌더링 이후 사용자가 input element에 무언가를 입력하기 시작하면 달라진다.

    element node는 사용자가 입력 필드에 입력한 값을 state로 가지고 있다. HTML attriute로 지정한 초기 상태 "ilgwon"도 관리해야하는데, 웹페이지를 처음 표시하거나 새로고침할 경우 필요하기 때문이다.

    즉 element node는 2개의 상태, 즉 초기 상태와 최신 상태를 관리해야 한다. element node의 초기 상태는 attribute node가 관리하고 최신 상태는 DOM property가 관리한다.

     

     

    39.7.4 data attribute와 dataset property

    data attribute와 dataset property를 사용하면 HTML element에 정의한 사용자 정의 attribute와 js간에 데이터를 교환할 수 있다.

    data attribute는 data-user-id, data-role과 같이 data-접두사 다음에 임의의 이름을 붙인다.

    data attribute값은 HTML.dataset property로 취득 가능하다. 

     

     


    39.8 Style

    39.8.1 인라인 스타일 조작

    HTMLElement.property.style property는 setter, getter 모두 존재하는 접근자 property로 element node의 inline style을 취득하거나 추가, 변경한다.

    [예제 39-24]

    const $style = 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'

     

    39.8.2 클래스 조작

    [예제 39-25]

    const $box = document.querySelector('.box')
    console.log($box.className)
    
    $box.className = $box.className.replace('red' , 'blue')

     

    [예제 39-26]

    Element.prototype.classList property는 class attribute 정보를 담은 DOMTokenList 객체를 반환한다. 

    const $box = document.querySelector('.box')
    console.log($box.classList)
    // DOMTokenList(2) [length: 2, value: "box blue", 0: "box", 1: "blue"]
    
    $box.classList.replace('red' , 'blue')

    DOMTokenList 객체는 class attribute 정보를 나타내는 collection 객체로 유사 배열 객체이면서 이터러블이다.

    • add(...className)
    • replace(...clasName)
    • item(index)
    • contains(className)
    • replace(oldClassName, newClassName)
    • toggle(className[, force])
      • class attribute에 인수로 전달한 문자열과 일치하는 클래스가 존재하면 제거, 존재하지 않으면 추가
      • 두 번째 인수로 boolean 값으로 평가되는 조건식 전달 가능. true이면 class attribute에 강제로 첫 번째 인수로 전달받은 문자열을 추가하고 false면 강제로 제거.

     

     

     

     

     

     

    반응형

    댓글

Designed by Tistory.