5.23 Javascript DOM

문서 객체 모델(Document Object Model)

1. Introduction

브라우저는 HTML 문서를 로드할 때 DOM(문서 객체 모델: Document Object Model)을 생성한다. DOM은 플랫폼/프로그래밍 언어 중립적으로 구조화된 문서를 표현하는 W3C의 공식 표준이다.

Web document는 텍스트로 만들어져 있는데 이것을 브라우저가 로드하여 구조화된 객체 모델(DOM)로 구성하여 메모리에 올리게 된다. 또한 로드된 DOM에 접근하고 수정할 수 있는 API도 제공한다. API는 일반적으로 메서드와 속성을 갖는 JavaScript 객체로 제공된다.

DOM은 다음 두 가지 기능을 담당한다.

HTML 문서에 대한 모델 구성
브라우저는 HTML 문서를 로드한 후 해당 문서에 대한 모델을 메모리에 생성한다. 이때 모델은 객체의 트리로 구성되는데 이것을 DOM tree라 한다.
HTML 문서 내의 각 요소에 접근 / 수정
DOM은 모델 내의 각 객체에 접근하고 수정할 수 있는 메서드와 속성들을 제공한다. DOM이 수정되면 브라우저를 통해 사용자가 보게 될 Contents 또한 변경된다.

2. DOM tree

DOM tree는 브라우저가 HTML 문서를 로드한 후 생성하는 모델을 의미하는데 객체의 트리로 구조화되어 있기 때문에 DOM tree라 부른다.

<!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>
  </body>
</html>

DOM tree

DOM tree

DOM tree는 네 종류의 노드로 구성된다.

문서 노드
트리의 최상위에 존재하며 각각 요소, 속성, 텍스트 노드에 접근하려면 문서 노드를 통해야 한다.
요소 노드
요소 노드는 HTML 요소를 표현한다. 속성, 텍스트 노드에 접근하려면 먼저 요소 노드를 찾아 접근해야 한다.
속성 노드
속성 노드는 HTML 요소의 속성을 표현한다. 먼저 요소 노드를 찾고 해당 요소의 속성을 참조 또는 수정할 수 있는 자바스크립트 메서드, 속성을 사용하여 참조, 수정할 수 있다.
텍스트 노드
텍스트 노드는 HTML 요소의 텍스트을 표현한다. 텍스트 노드는 자식 노드를 가질 수 없다.

DOM tree는 브라우저가 생성한다. 생성되어 있는 DOM을 가지고 웹페이지를 조작(manipulate)하기 위해서는

  • DOM에 접근하여 조작하고자하는 요소를 선택 또는 탐색한 후
  • 선택된 요소의 내용(contents) 또는 속성을 조작하는 것이 필요하다.

JavaScript는 이것에 필요한 수단(API)을 제공한다.

3. DOM Query / Traversing (요소에의 접근)

3.1 하나의 요소 노드 선택(DOM Query)

select an individual element node

  • 요소의 id 속성값으로 선택할 수 있다.
var elem = document.getElementById('one');
elem.className = 'blue';
  • CSS 선택자를 이용해 요소를 선택할 수 있다. 복수개가 선택된 경우, 첫번째 요소만 반환된다.
// querySelector() only returns the first match
var elem = document.querySelector('li.red');
elem.className = 'blue';

3.2 여러개의 요소 노드 선택(DOM Query)

select multiful elements

지정된 class 속성값을 가지는 요소를 모두 선택한다. 공백으로 구분하여 여러개의 class 속성값을 지정할 수 있다.

// getElementsByClassName returns a HTMLCollection
var elems = document.getElementsByClassName('red'), i;
for (i=0; i<elems.length; i++){
  elems[i].className = 'blue';
}

위 예제를 실행해 보면 예상대로 동작하지 않는다. (두번째 요소만 클래스 변경이 되지 않는다.)

getElementsByClassName 메서드의 반환값은 HTMLCollection이다. 이것은 반환값이 복수인 경우, node의 리스트를 담아 반환하기 위한 객체로 배열과 비슷한 사용법을 가지고 있지만 배열은 아닌 유사배열(array-like object)이다. 따라서 배열의 메서드인 forEach 등은 사용할 수 없다. 또한 HTMLCollection은 실시간으로 Node의 상태 변경을 반영한다. (live HTMLCollection)

위 예제가 예상대로 동작하지 않은 이유를 알아보자.

elems.length는 3으로 3번의 loop가 실행된다.

  1. i가 0일때 elems의 첫 요소(li#one.red)의 className이 red에서 blue로 변경된다. 이때 더이상 class명이 red가 아니므로 elems에서 첫째 요소는 제거된다.(실시간으로 Node의 상태 변경을 반영한다.)

  2. i가 1일때 elems에서 첫째 요소는 제거되었으므로 elems[1]은 3번째 요소(li#three.red)가 된다. li#three.red의 className이 blue로 변경되고 마찬가지로 HTMLCollection에서 제외된다.

  3. i가 2일때 HTMLCollection의 1,3번째 요소가 실시간으로 제거되었으므로 2번째 요소(li#two.red)만 남았다. elems[2]는 undefined이다.

이처럼 HTMLCollection는 실시간으로 Node의 상태 변경을 반영하기 때문에 loop가 필요한 경우 주의가 필요하다. 아래와 같은 방법으로 회피할 수 있다.

  • 반복문을 역방향으로 돌린다.
var elems = document.getElementsByClassName('red'), i;
for (i=elems.length-1; i>=0; i--){
  elems[i].className = 'blue';
}
  • while 반복문을 사용한다. 이때 elems에 요소가 남아 있지 않을 때까지 무한반복하기 위해 index는 0으로 고정시킨다.
var elems = document.getElementsByClassName('red'),
    i = 0;
while (elems.length > i) { // elems에 요소가 남아 있지 않을 때까지 무한반복
  elems[i].className = 'blue';
  // i++;
}
  • HTMLCollection을 배열로 변경한다.
var elems = document.getElementsByClassName('red'), i;
// 5.11 Javascript this의 4.apply 호출 패턴(Apply Invocation Pattern) 참조
var arr = [].slice.call(elems);
for (i=0; arr.length>0; i++){
  arr[i].className = 'blue';
}
  • querySelectorAll 메서드를 사용하여 non-live NodeList를 반환하게 한다.
// querySelectorAll returns a Nodelist
var elems = document.querySelectorAll('.red'), i;
for (i=0; i<elems.length; i++){
  elems[i].className = 'blue';
}

지정된 태그명을 가지는 요소를 모두 선택한다.

// getElementsByTagName returns a HTMLCollection
var elems = document.getElementsByTagName('li'), i;
for (i=0; i<elems.length; i++){
  elems[i].className = 'blue';
}

지정된 CSS 선택자와 일치하는 요소를 모두 선택한다.

// querySelectorAll returns a Nodelist
var elems = document.querySelectorAll('li.red'), i;
for (i=0; i<elems.length; i++){
  elems[i].className = 'blue';
}
구분 getElementsBy* querySelector*
id document.getElementById(‘foo’) document.querySelector(‘#foo’)
tag document.getElementsByTagName(‘li’) document.querySelectorAll(‘li’)
class document.getElementsByClassName(‘bar’) document.querySelectorAll(‘.bar’)
속성(name=foo) document.getElementsByName(‘foo’) document.querySelectorAll(‘[name=foo])

getElementsBy* 메서드는 HTMLCollection, querySelector* 메서드는 Nodelist를 반환한다.

3.3 DOM Traversing (탐색)

traversing

현재 요소의 부모 요소를 선택한다.

var elem = document.getElementById('two');
var parentNode = elem.parentNode;
parentNode.className = 'blue';

현재 요소의 형제 요소를 선택한다.

var elem = document.getElementById('two');
var previousSibling = elem.previousSibling;
var nextSibling = elem.nextSibling;

previousSibling.className = 'blue';
nextSibling.className = 'blue';

위 예제를 실행해 보면 예상대로 동작하지 않는다. 그 이유는 IE를 제외한 대부분의 브라우저들은 요소 사이의 공백 또는 줄바꿈 문자를 텍스트 노드로 취급하기 때문이다. 이것을 회피하기 위해서는 HTML에 공백을 제거하거나 jQuery를 사용한다.

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

현재 요소의 자식 요소를 선택한다.

var elem = document.getElementsByTagName('ul')[0];
var firstChild = elem.firstChild;
var lastChild = elem.lastChild;

firstChild.className = 'blue';
lastChild.className = 'blue';

4. DOM Manipulation (조작)

4.1 텍스트 노드에의 접근/수정

nodeValue

요소의 텍스트는 요소의 텍스트 노드에 저장되어 있다. 텍스트 노드에 접근하려면 아래와 같은 수순이 필요하다.

  1. 해당 텍스트를 가지는 요소 노드를 선택한다.
  2. firstChild 프로퍼티를 사용하여 텍스트 노드를 선택한다.
  3. 텍스트 노드의 유일한 속성(nodeValue)을 이용하여 텍스트를 취득한다.
  4. nodeValue를 이용하여 텍스트를 수정한다.
var one  = document.getElementById('one');
//Get text
var text = one.firstChild.nodeValue;
//Replace text
text = text.replace('Seoul', 'Pusan');
//Set text
one.firstChild.nodeValue = text;

4.2 속성 노드에의 접근/수정

nodeValue

속성 노드을 조작할 때 다음 속성 또는 메서드를 사용할 수 있다.

className

class 속성값을 취득 또는 변경한다. class 속성이 존재하지 않으면 지정된 속성값으로 class 속성을 생성한다. class 속성값이 여러개일 경우, 공백으로 구분된 문자열이 반환되므로 String 메서드 split(' ')를 사용하여 배열로 변경한다.

var elems = document.getElementsByTagName('li'), i;
for (i = elems.length-1; i >= 0; i--) {
  if(elems[i].className === 'red'){
    elems[i].className = 'blue';
  } else {
    elems[i].className = 'red';
  }
}

id

id 속성값을 취득 또는 변경한다. id 속성이 존재하지 않으면 지정된 속성값으로 id 속성을 생성한다.

var heading = document.getElementsByTagName('h1')[0];
heading.id = 'heading';
console.log("h1's id is " + heading.id);

hasAttribute()

지정한 속성을 가지고 있는지 검사한다.

getAttribute()

속성값을 취득한다.

setAttribute()

속성값을 지정한다.

removeAttribute()

지정한 속성을 제거한다.

var one = document.getElementById('one');
if(one.hasAttribute('class')) {
  console.log(one.getAttribute('class'));
  one.removeAttribute('class');
} else {
  one.setAttribute('class', 'blue');
}

4.3 HTML Contents 조작(Manipulation)

innerHTML

HTML Contents를 조작(Manipulation)하기 위해 아래의 속성 또는 메서드를 사용할 수 있다. 마크업이 포함된 Contents를 추가하는 행위는 크로스 스크립팅 공격(XSS: Cross-Site Scripting Attacks)에 취약하므로 주의가 필요하다.

textContent

textContent 속성을 사용하면 요소의 text contents에만 접근할 수 있다. 이때 마크업은 무시된다.

var one = document.getElementsByTagName('ul')[0];
console.log(one.textContent); // SeoulLondonNewyorkTokyo

textContent 속성을 사용하여 새로운 text contents를 지정하면 text contents를 변경할 수 있다. 이때 순수한 텍스트만 지정해야 하며 마크업을 포함시키면 문자열로 인식되어 그대로 출력된다.

var one = document.getElementById('one');
console.log(one.textContent); // Seoul
one.textContent +=  ', Korea'
console.log(one.textContent); // Seoul, Korea

innerText

innerText 속성을 사용하여도 text contents에만 접근할 수 있다. 하지만 아래의 이유로 사용하지 않는 것이 좋다.

  • 비표준이다.
  • CSS에 순종적이다. 예를 들어 CSS에 의해 비표시(visibility: hidden;)로 지정되어 있다면 텍스트가 반환되지 않는다.
  • CSS를 고려해야 하므로 textContent 속성보다 느리다

innerHTML

innerHTML 속성을 사용하면 해당 요소의 모든 자식 요소를 포함하는 모든 Contents를 하나의 문자열로 취득할 수 있다. 이 문자열은 마크업을 포함한다.

var elem = document.getElementsByTagName('ul')[0];
console.log(elem.innerHTML);
// <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 속성을 사용하여 마크업이 포함된 새로운 Contents를 지정하면 새로운 요소를 DOM에 추가할 수 있다.

var 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>'

DOM 조작 방식

innerHTML 속성을 사용하지 않고 새로운 contents를 추가할 수 있는 방법은 DOM을 직접 조작하는 것이다. 이 방법은 다음의 수순에 따라 진행한다.

  1. 요소 노드 생성
    createElement() 메서드를 사용하여 새로운 요소 노드를 생성한다.

  2. 텍스트 노드 생성
    createTextNode() 메서드를 사용하여 새로운 텍스트 노드를 생성한다. 경우에 따라 생략될 수 있다. 하지만 contents가 비어 있는 요소가 된다.

  3. 생성된 요소를 DOM에 추가
    appendChild() 메서드를 사용하여 생성된 노드를 DOM tree에 추가한다. 또는 removeChild() 네서드를 사용하여 DOM tree에서 노드를 삭제할 수도 있다.

// create a new element node
var newElem = document.createElement('li');

// create a new text node
var newText = document.createTextNode('Beijing');

// Append a new element
newElem.appendChild(newText);
var container = document.getElementsByTagName('ul')[0];
container.appendChild(newElem);

// Remove an existing element
var removeElem = document.getElementById('one');
container.removeChild(removeElem);

innerHTML vs. DOM 조작 방식

  • innerHTML
장점 단점
DOM 조작 방식에 비해 빠르고 간편하다. XSS공격에 취약점이 있기 때문에 사용자로 부터 입력받은 Contents(untrusted data: 댓글, 사용자 이름 등)를 추가할 때 사용해서는 않된다.
간편하게 요소의 contents를 삭제할 수 있다 하나의 요소를 구분하기 여려우며 이벤트 핸들러가 의도한 대로 동작하지 않는다.
  • DOM 조작 방식
장점 단점
이벤트 핸들러에 영향을 주지 않으며 특정 요소를 변경할 때 적합하다. innerHTML보다 느리고 더 많은 코드가 필요하다.
  • 결론
    innerHTML은 사용하지 않는다. 텍스트 추가,변경시에는 textContent, 새로운 요소의 추가/삭제시에는 DOM 조작 방식을 사용하도록 한다.

Reference

Back to top
Close