본문 바로가기
Programming/JavaScript

DOM - [Javascript 입문 _10]

by Muko 2020. 4. 19.

이번 포스팅에서는 DOM에 대해서 다뤄보도록 하겠습니다.

1. DOM이란

우리가 보는 웹 페이지는 하나의 문서(document)입니다. 인터넷이 국내에서 지금처럼 누구나 사용하던 것이 아니던 1990년대에 쓰던 웹페이지를 보면 더욱 이 문장이 실감납니다.

네이버 초창기 화면(맨 처음에는 노란색이었음)

위에 첨부한 네이버의 화면도 이미지가 조금 있지만, 더 초창기 인터넷 세계로 들어가면 텍스트로만 이루어진 화면을 볼 수 있습니다. 이처럼 웹 페이지 문서는 보통 HTML로 작성되어 있고, 크롬이나 인터넷 익스플로러와같은 웹 브라우저를 통해서 그 내용이 해석되어 웹 브라우저 화면에 나타나거나, HTML 소스 자체로 나타나기도 합니다. 여기서 동일한 문서를 사용해서 이렇게 다른 형태로 나타날 수 있는 것은 DOM이라는 존재 덕분입니다. DOM은 문서를 표현, 저장, 조작하는 방법을 제공하는 모델로 정확한 명칭은 '문서 객체 모델(The Document Object Model)' 입니다. DOM은 문서를 구조화된 표현으로 제공해서 프로그래밍 언어가 DOM 구조에 접근할 수 있는 방법을 제공합니다. 우리는 자바스크립트를 이용해서 이 DOM 객체에 접근하고, 문서의 구조나 내용, 스타일을 바꾸는 것이 가능한 것이죠.

이 DOM을 살펴보면 결국 구성 요소는 HTML 입니다. 그리고 이 HTML을 구성하는 것은 태그(tag)입니다. DOM에서 이 HTML 태그는 모두 객체입니다. 그리고, 이 각각의 HTML는 또 다른 태그를 감싸고 있을 수 있는데요, 이러한 자식 태그는 중첩 태그(nested tag)라고도 부르며, 태그 내의 문자 역시 객체입니다. 이런 모든 객체는 위의 문단에서 설명한 것과 같이 자바스크립트로 접근이 가능합니다. 예를 들어보겠습니다.

document.body.style.background = 'blue'; // 웹 문서 배경을 파란색으로 변경

// 3초 후에 원래 배경색으로 설정
setTimeout(() => {
  document.body.style.background = '';
}, 3000);

 

여기서 document가 전체 문서에 접근할 수 있게 해주는 객체입니다. 사실 정확하게는 window.document인데, window는 생략 가능(전역 객체)해서 보통 document로 사용합니다. DOM에 수행하는 모든 연산은 document 객체에서 시작하고, 이 document를 사용하면 문서 내에 어떤 노드라도 접근이 가능합니다. 예시를 보면, 첫 번째 줄에 document.body<body>태그를 객체로 표현한 코드입니다. 그래서 해석하면 body 태그의 스타일 중에서 background의 속성 값을 blue로 설정하겠다는 의미입니다. 자바스크립트로 문서의 스타일을 바꾼 예시코드 입니다.

그 아래에 setTimeout으로 시작하는 코드는 '호출 스케쥴링(scheduling a call)'이라고도 부르는데, 함수를 당장 실행하지 않고 정확히 입력된 숫자값(ms 단위)의 시간 딜레이 후에 실행할 때 사용됩니다. 여기에는 setIntervalsetTimeout이 있는데, 반복 실행되는지 한 번만 실행되는지에 대한 차이가 있습니다. setTimeout이 한 번만 실행되는 메서드입니다. 그래서 document.body.style.background = '';라는 코드를 3000ms 뒤, 그러니까 3초 뒤에 실행하겠다는 의미입니다.

Q. document.body가 null일 수도 있다?
A. script를 읽는 도중에 존재하지 않는 요소는 접근이 불가능합니다. 코드를 실행시켰을 때, HTML을 모두 로딩하기 전에 script 파일을 로딩해야할 경우 항상 script 파일이 우선시 됩니다. 그래서 브라우저가 아직 document.body를 읽기 전에 <head> 부분에 있는 스크립트에서 document.body에 접근하려고 할 경우에는 null에 접근하는 것과 동일한 결과를 받게 됩니다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>DOM 예제</title>
    <script>
      alert("HEAD: " + document.body); // null
    </script>
  </head>
  <body>
    <script>
      alert("BODY: " + document.body); // HTMLBodyElement
    </script>
  </body>
</html>

DOM은 HTML을 태그들로 구성된 트리 구조로 표현합니다. 트리 구조는 회사의 조직도를 생각하면 쉽게 이해할 수 있습니다. 어떤 부서가 있으면 그 아래에 여러 팀들이 있고, 그 아래에 팀장과 팀원들이 존재하는 것과 같이 DOM에서도 HTML을 계층적인 구조로 보관하기 위해 트리 구조를 사용합니다. 이렇게 트리를 구성하는 요소들을 element node라고 표현하며, <html>은 루트 노드가 되고 <head><body>는 루트노드의 자식노드가 된다고 이해하시면 됩니다.

또한 요소 내에 있는 문자 또한 텍스트 노드로 존재하게 됩니다. 여기서 주의하셔야 할 점은 개행(newline)이나 공백(space) 또한 유효한 문자로 여겨져서 텍스트 노드로 만들어진다는 것입니다.

<!DOCTYPE HTML>
<html>
  <head>
    <title>DOM에 대해서</title>
  </head>
  <body>
    DOM은 어려워...
  </body>
</html>



위와 같은 문서가 있을 때, 이 HTML을 DOM은 아래와 같은 트리 구조로 표현하게 됩니다.

- HTML
  - HEAD
    - #text ↵␣␣
    - TITLE
      - #text DOM에 대해서
    - #text ↵␣␣
  - #text ↵␣␣
  - BODY
    - #text DOM은 어려워...

 

여기서 #text는 텍스트 노드를 의미하며, 문자열만 담고 있는 노드입니다. 자식 노드를 가질 수 없고, 각 트리의 끝을 이루는 잎 노드(leaf node)가 됩니다. 또한 개행이나 공백 또한 유효한 문자로 여겨진다고 설명드린 것을 확인할 수 있습니다. HTML코드에서 엔터나 공백에 대해서도 텍스트 노드가 트리에 포함되어 있습니다. 여기서 엔터 기호가 개행, 받침대 같이 생긴 것이 공백 기호를 의미합니다. 여기서 예외적인 부분은 <head> 이전의 공백과 개행은 무시되며, HTML 명세서에 따르면 모든 콘텐츠는 body 안쪽에 위치해야되서 </body> 뒤에 어떤 것을 작성하더라도 자동으로 body 안쪽으로 옮겨진다는 것입니다.



2. DOM에서 특정 노드 접근, 조작하기

<!DOCTYPE html>
<html>
  <head>
    <title>DOM에 대해서</title>
  </head>
  <body>
    <div id="container" name="myNodes">
      <div id="node001" class="innerTag" name="myNodes">
        <p id="examText" name="examText">test text</p>
      </div>
      <div id="node002" class="innerTag" name="myNodes"></div>
      <div id="node003" class="innerTag" name="myNodes"></div>
      <div id="node004" class="innerTag" name="myNodes"></div>
    </div>
  </body>
</html>



1.document.getElementById

  document.getElementById('container');

 

위의 코드를 실행하면 <div id="container"> 노드에 접근이 가능합니다. 여기서 id는 보통 고유한, 하나뿐인 이름을 사용해야 해서 이 메서드도 단 하나의 노드가 반환됩니다.

2.document.getElementsByClassName

  document.getElmentsByClassName('innerTag');

 

id와 달리 이번에는 class 이름을 이용해서 노드들에 접근이 가능한 메서드입니다. 클래스는 여러개에 적용이 가능하기 때문에 getElementById와 달리 복수형인 getElement's'ByClassName이라고 메서드 이름이 정의되어 있습니다. 위와 같이 작성하면 innerTag라는 클래스를 사용하는 div 노드 4개가 반환되게 됩니다.

3.document.getElementsByName

  document.getElementsByName('myNodes');

 

class와 비슷하게 이번에는 name 속성을 이용해서 접근하는 메서드입니다. 마찬가지로 고유한 값이 아니므로 복수형을 사용하고 있습니다. 위의 코드를 실행하면 5개의 div 노드가 반환됨을 확인할 수 있습니다.

4.document.getElementsByTagName

  document.getElementsByTagName('p');

 

이번에는 HTML tag를 이용해서 접근하는 메서드입니다. 위의 코드에서는 p 태그를 찾겠다고 적혀있고, p 태그가 하나밖에 없으므로 결과적으로 p 태그 노드 하나가 반환됩니다.

5.document.querySelector, document.querySelectorAll

  document.querySelector('#node004'); // id가 node004인 태그 노드 반환
  document.querySelector('.innerTag'); // 클래스가 innerTag인 첫 번째 노드 반환
  document.querySelector('div.innerTag p[name=examText]'); // 클래스가 inner인 div 태그 안에 있는 p 태그 중에서 name이 "examText"인 노드 반환

  document.querySelectorAll('.innerTag'); // 클래스가 innerTag인 모든 노드 반환

 

querySelector는 CSS 선택자를 이용해서 노드를 탐색하는 메서드입니다. HTML과 CSS를 작성할 때 id와 class, name등 다양한 속성을 작성할 수 있는데, 이러한 값들을 이용해서 찾으려고 할 때 사용합니다. 이 때, id는 '#'으로, class는 '.'으로, name등의 속성은 '태그[속성=찾고자 하는 값]'의 식으로 사용합니다.

6.document.createElement

  const myDiv = document.createElement('div');

 

위와 같이 코드를 작성하면, div 태그가 문서안에 추가되는 것이 아니라 myDiv라는 변수 안에 div 태그 객체가 저장됩니다. 이 객체를 생성해서 문서에 추가할지, 다른 div 태그를 대체할지 등은 나중에 원하는 동작을 코드로 구현하는 방식으로 사용합니다.

7.children, childNodes, firstChild, lastChild

  document.children; // [head, body]
  document.childNodes; // [head, text, body]
  document.body.childNodes; // [text, div#container, text]
  document.body.firstChild; // #text
  document.body.lastChild; // #text
  document.body.childNodes[1].children; // [div#node001.innerTag, div#node002.innerTag, div#node003.innerTag, div#node004.innerTag, ...];

 

여기 사용된 4가지 종류의 노드 타입은 모두 자식 노드를 탐색할 때 사용하는 속성입니다. 여기서 children은 텍스트 노드가 제외되어 반환되는 반면, childNodes는 텍스트 노드까지 포함되어 반환되는 차이가 존재합니다. 물론 document.getElementById나 querySelector와 같은 메서드로 노드를 탐색하는 방법도 있지만, 자식 노드가 어떤 것인지 명확히 모르는 상태에서 자식 노드에 접근해야할 경우 유용하게 쓰이는 속성 값입니다. 그리고 firstChild는 첫 번째 자식 노드를, lastChild는 마지막 자식 노드를 반환합니다.

8.nextSibling, previousSibling, nextElementSibling, previousElementSibling

  document.getElementById('#node003').nextSibling; // div#node004.innerTag
  document.getElementById('#node003').previousSibiling; // div#node002.innerTag
  document.body.chidlNodes[0].nextSibling; // div#container

 

이 속성은 위아래의 관계가 아니라 같은 레벨에 속해있는 태그, 즉 형제자매 태그를 찾을 때 사용합니다. sibling이 형재자매라는 뜻이라는 것을 알아 둔 다면 쉽게 사용 가능한 속성입니다. nextElementSibling, nextElementSibling은 텍스트 노드를 건너뛰고 찾는다는 차이점이 있습니다.

9.parentElement

  document.body.parentElement; // html

 

부모를 찾을 때는 parentNode 속성을 사용하면 됩니다. 자식은 여러 개일 수 있어서 복수형을 사용했지만, 부모 노드는 항상 하나이기 때문에 단수형을 사용한 것을 확인할 수 있습니다.

10.tag.innerHTML, tag.outerHTML

  const examText = document.getElementById('#examText');
  examText.innerHTML; // test text
  examText.innerHTML = 'bye bye';

  document.getElementById('#node003').innerHTML = '<b>bold text</b>';
  document.getElementById('#node004').outerHTML = '<div id="node004" class="innerTag" name="myNodes"><span>node004 text</span></div>';

 

innerHTML과 outerHTML을 사용하면 탐색한 태그 노드의 내용물을 가져오거나 변경하는 것이 가능해집니다. 위의 예시에서 id가 examText인 노드에 접근해서 innerHTML을 통해 정보를 가져와보니 'test text'라는 문자열이 저장되어 있는 것을 확인가능했습니다. 그 다음 줄에 innerHTML에 값을 넣음으로써 안에 있는 내용물을 바꿨고, 실제로 웹에서 화면에 글자가 바뀌게 됩니다.

또한 내용물은 단순히 문자열일 수도 있지만, HTML 태그가 들어갈 수도 있습니다. innerHTML은 해당 노드의 안에 있는 내용물이라면, outerHTML은 현재 탐색으로 접근한 노드까지 포함한 문자열을 반환해야 하는 차이를 가지고 있습니다.

10.tag.속성
위에서 각 노드에 접근하기 위한 여러가지 속성과 메서드를 나열했는데요, 이렇게 접근한 노드에서 .속성이름으로 해당 태그의 속성까지 조회가 가능합니다. 여기서 접근 가능한 속성으로는 id, class, name, value, placeholder, checked, disabled, readonly 등의 값이 있습니다. 만약 해당 태그가 가진 모든 속성에 대해서 보고 싶을 경우에는 tag.attributes를 치면 되고, 해당 태그의 크기와 관련된 속성을 보기 위해서는 tag.clientHeight, tag.clientWidth, tag.offsetHeight, tag.offsetWidth를 사용하면 됩니다. 여기서 client로 시작하는 것은 태그의 margin, border, scrollbar를 제외한 높이와 너비를 반환하고, offset으로 시작하는 것은 margin만을 제외한 높이와 너비를 반환한다는 차이가 있습니다.

11.tag.메서드

  • tag.appendChild: createElement를 이용해서 만든 새로운 태그를 페이지에 넣을 때 사용하는 메서드입니다. document.body.appendChild(myElement);를 실행하게되면 body에 myElement라는 태그 노드가 자식 노드로 추가됩니다.
  • tag.removeChild: 선택한 자식 태그를 삭제하는 메서드입니다. 해당 태그에서 어떤 자식 태그를 삭제할 것인지 파라미터로 삭제하고자 하는 태그 노드를 넣으면 삭제할 수 있습니다.
  • tag.insertBefore: appendChild가 어떤 태그를 자식 노드로 추가하는 메서드라면, insertBefore은 sibling 노드로 추가하는 것입니다. 파라미터로는 '넣고자 하는 태그 노드', 그리고 '기준이 되는 태그'로 구성됩니다. 새로 추가한 노드의 nextSibling이 기준이 되는 태그가 되는 것이죠.



여기까지 따라오시느라 수고하셨습니다!
다음 시간에는 이벤트 핸들링에 대해서 알아보도록 하겠습니다.

댓글8