2021년 12월 15일

DOM 탐색하기

DOM을 이용하면 요소와 요소의 콘텐츠에 무엇이든 할 수 있습니다. 하지만 무언가를 하기 전엔, 당연히 조작하고자 하는 DOM 객체에 접근하는 것이 선행되어야 합니다.

DOM에 수행하는 모든 연산은 document 객체에서 시작합니다. document 객체는 DOM에 접근하기 위한 '진입점’이죠. 진입점을 통과하면 어떤 노드에도 접근할 수 있습니다.

아래 그림은 DOM 노드 탐색이 어떤 관계를 통해 이루어지는지를 보여줍니다.

화살표로 나타낸 관계에 대하여 좀 더 자세히 알아봅시다.

트리 상단의 documentElement와 body

DOM 트리 상단의 노드들은 document가 제공하는 프로퍼티를 사용해 접근할 수 있습니다.

<html> = document.documentElement
document를 제외하고 DOM 트리 꼭대기에 있는 문서 노드는 <html> 태그에 해당하는 document.documentElement입니다.
<body> = document.body
document.body<body> 요소에 해당하는 DOM 노드로, 자주 쓰이는 노드 중 하나입니다.
<head> = document.head
<head> 태그는 document.head로 접근할 수 있습니다.
document.bodynull일 수도 있으니 주의하세요.

스크립트를 읽는 도중에 존재하지 않는 요소는 스크립트에서 접근할 수 없습니다.

브라우저가 아직 document.body를 읽지 않았기 때문에 <head> 안에 있는 스크립트에선 document.body에 접근하지 못하죠.

따라서 아래 예시에서 첫 번째 alert 창엔 null이 출력됩니다.

<html>

<head>
  <script>
    alert( "HEAD: " + document.body ); // null, 아직 <body>에 해당하는 노드가 생성되지 않았음
  </script>
</head>

<body>

  <script>
    alert( "BODY: " + document.body ); // HTMLBodyElement, 지금은 노드가 존재하므로 읽을 수 있음
  </script>

</body>
</html>
DOM의 나라에서 null은 '존재하지 않음'을 의미합니다.

DOM에서 null 값은 '존재하지 않음’이나 '해당하는 노드가 없음’을 의미합니다.

childNodes, firstChild, lastChild로 자식 노드 탐색하기

앞으로 사용할 두 가지 용어를 먼저 정의하고 설명을 이어나가도록 하겠습니다.

  • 자식 노드(child node, children) 는 바로 아래의 자식 요소를 나타냅니다. 자식 노드는 부모 노드의 바로 아래에서 중첩 관계를 만듭니다. <head><body><html>요소의 자식 노드입니다.
  • 후손 노드(descendants) 는 중첩 관계에 있는 모든 요소를 의미합니다. 자식 노드, 자식 노드의 모든 자식 노드 등이 후손 노드가 됩니다.

아래 예시에서 <body><div><ul>, 몇 개의 빈 텍스트 노드를 자식 노드로 갖습니다.

<html>
<body>
  <div>시작</div>

  <ul>
    <li>
      <b>항목</b>
    </li>
  </ul>
</body>
</html>

<div><ul>같은 <body>의 자식 요소뿐만 아니라 <ul>의 자식 노드인 <li><b>같이 더 깊은 곳에 있는 중첩 요소도 <body>의 후손 노드가 됩니다.

childNodes 컬렉션은 텍스트 노드를 포함한 모든 자식 노드를 담고 있습니다.

아래 예시를 실행하면 document.body의 자식 노드가 출력됩니다.

<html>
<body>
  <div>시작</div>

  <ul>
    <li>항목</li>
  </ul>

  <div>끝</div>

  <script>
    for (let i = 0; i < document.body.childNodes.length; i++) {
      alert( document.body.childNodes[i] ); // Text, DIV, Text, UL, ... , SCRIPT
    }
  </script>
  ...추가 내용...
</body>
</html>

예시를 실행하면 흥미로운 점이 하나 발견됩니다. 마지막에 <script>가 출력되죠. <script> 아래 더 많은 내용(…추가 내용…)이 있지만, 스크립트 실행 시점엔 브라우저가 추가 내용은 읽지 못한 상태이기 때문에 스크립트 역시 추가 내용을 보지 못해서 이런 결과가 나타났습니다.

firstChildlastChild 프로퍼티를 이용하면 첫 번째, 마지막 자식 노드에 빠르게 접근할 수 있습니다.

이 프로퍼티들은 단축키 같은 역할을 합니다. 자식 노드가 존재하면 아래 비교문은 항상 참이 됩니다.

elem.childNodes[0] === elem.firstChild
elem.childNodes[elem.childNodes.length - 1] === elem.lastChild

참고로 자식 노드의 존재 여부를 검사할 땐 함수 elem.hasChildNodes()를 사용할 수도 있습니다.

DOM 컬렉션

위에서 살펴본 childNodes는 마치 배열 같아 보입니다. 하지만 childNodes는 배열이 아닌 반복 가능한(iterable, 이터러블) 유사 배열 객체인 컬렉션(collection) 입니다.

childNodes는 컬렉션이기 때문에 아래와 같은 특징을 가집니다.

  1. for..of를 사용할 수 있습니다.
for (let node of document.body.childNodes) {
  alert(node); // 컬렉션 내의 모든 노드를 보여줍니다.
}

이터러블이기 때문에 Symbol.iterator 프로퍼티가 구현되어 있어서 for..of를 사용하는 것이 가능하죠.

  1. 배열이 아니기 때문에 배열 메서드를 쓸 수 없습니다.
alert(document.body.childNodes.filter); // undefined (filter 메서드가 없습니다.)

첫 번째 특징은 장점으로 작용합니다. 두 번째 특징은 썩 좋지는 않지만 Array.from을 사용하면 ‘진짜’ 배열을 만들 수 있기 때문에 참을 만합니다. 컬렉션에 배열 메서드를 사용하고 싶다면 Array.from을 적용합시다.

alert( Array.from(document.body.childNodes).filter ); // function
DOM 컬렉션은 읽는 것만 가능합니다.

DOM 컬렉션을 비롯해 이번 챕터에서 설명하는 모든 탐색용 프로퍼티는 읽기 전용입니다.

childNodes[i] = ...를 이용해 자식 노드를 교체하는 게 불가능하죠.

DOM을 변경하려면 다른 메서드가 필요합니다. 다음 챕터에서 이 메서드에 대해 살펴보겠습니다.

DOM 컬렉션은 살아있습니다.

몇몇 예외사항을 제외하고 거의 모든 DOM 컬렉션은 살아있습니다. DOM의 현재 상태를 반영한다는 말이죠.

elem.childNodes를 참조하고 있는 도중에 DOM에 새로운 노드가 추가되거나 삭제되면, 변경사항이 컬렉션에도 자동으로 반영됩니다.

컬렉션에 for..in 반복문을 사용하지 마세요,

컬렉션은 for..of를 이용해 순회할 수 있습니다. 그런데 가끔 for..in을 사용하려는 사람들이 있죠.

for..in은 절대 사용하지 마세요. for..in 반복문은 객체의 모든 열거 가능한 프로퍼티를 순회합니다. 컬렉션엔 거의 사용되지 않는 ‘추가’ 프로퍼티가 있는데, 이 프로퍼티까지 순회 대상에 포함하길 원하지 않으실 거니까요.

<body>
<script>
  // 0, 1, length, item, values 등 불필요한 프로퍼티까지도 출력됩니다.
  for (let prop in document.body.childNodes) alert(prop);
</script>
</body>

형제와 부모 노드

같은 부모를 가진 노드는 형제(sibling) 노드 라고 부릅니다.

<head><body>는 대표적인 형제 노드입니다.

<html>
  <head>...</head><body>...</body>
</html>
  • <body><head>의 ‘다음(next)’ 혹은 '우측(right)'에 있는 형제 노드입니다.
  • <head><body>의 ‘이전(previous)’ 혹은 '좌측(left)'에 있는 형제 노드입니다.

다음 형제 노드에 대한 정보는 nextSibling, 이전 형제 노드에 대한 정보는 previousSibling 프로퍼티에서 찾을 수 있습니다.

부모 노드에 대한 정보는 parentNode 프로퍼티를 이용해 참조할 수 있습니다.

예시:

// <body>의 부모 노드는 <html>입니다
alert( document.body.parentNode === document.documentElement ); // true

// <head>의 다음 형제 노드는 <body>입니다.
alert( document.head.nextSibling ); // HTMLBodyElement

// <body>의 이전 형제 노드는 <head>입니다.
alert( document.body.previousSibling ); // HTMLHeadElement

요소 간 이동

지금까지 언급한 탐색 관련 프로퍼티는 모든 종류의 노드를 참조합니다. childNodes를 이용하면 텍스트 노드, 요소 노드, 심지어 주석 노드까지 참조할 수 있죠.

하지만 실무에서 텍스트 노드나 주석 노드는 잘 다루지 않습니다. 웹 페이지를 구성하는 태그의 분신인 요소 노드를 조작하는 작업이 대다수이죠.

이런 실제 상황을 토대로 DOM 요소 노드 탐색이 어떻게 이루어지는지 알아봅시다.

위 그림 속 관계는 챕터 앞쪽에서 다뤘던 관계와 유사해 보입니다. Element라는 단어가 추가된 점만 다르네요.

  • children 프로퍼티는 해당 요소의 자식 노드 중 요소 노드만을 가리킵니다.
  • firstElementChildlastElementChild 프로퍼티는 각각 첫 번째 자식 요소 노드와 마지막 자식 요소 노드를 가리킵니다.
  • previousElementSiblingnextElementSibling은 형제 요소 노드를 가리킵니다.
  • parentElement 는 부모 요소 노드를 가리킵니다.
부모가 요소가 아니라면 parentElement는 어떻게 되나요?

parentElement 프로퍼티는 부모 '요소 노드’를 반환하는 반면 parentNode 프로퍼티는 '종류에 상관없이 부모 노드’를 반환합니다. 대개 두 프로퍼티는 같은 노드를 반환합니다.

그런데 document.documentElement아래와 같은 상황에서는 다른 노드를 반환합니다.

alert( document.documentElement.parentNode ); // document
alert( document.documentElement.parentElement ); // null

반환 값이 다른 이유는 <html>에 해당하는 document.documentElement의 부모는 document인데, document 노드는 요소 노드가 아니기 때문입니다. 따라서 위 예시에서 parentNode는 의도한 대로 document 노드를 반환하지만, parentElementnull을 반환합니다.

이런 사소한 차이는 임의의 요소 노드 elem에서 시작해 <html>까지 거슬러 올라가고 싶은데, document까지는 가고 싶지 않을 때 유용하게 활용할 수 있습니다.

while(elem = elem.parentElement) { // <html>까지 거슬러 올라갑니다.
  alert( elem );
}

앞서 보았던 예시에서 childNodeschildren으로 대체해봅시다. 요소 노드만 출력되는 것을 확인할 수 있습니다.

<html>
<body>
  <div>시작</div>

  <ul>
    <li>항목</li>
  </ul>

  <div>끝</div>

  <script>
    for (let elem of document.body.children) {
      alert(elem); // DIV, UL, DIV, SCRIPT
    }
  </script>
  ...
</body>
</html>

테이블 탐색하기

지금까지 DOM 탐색 기본 프로퍼티를 알아보았습니다.

그런데 일부 DOM 요소 노드는 편의를 위해 기본 프로퍼티 외에 추가적인 프로퍼티를 지원합니다.

테이블이 가장 대표적입니다. 좀 더 자세히 알아봅시다.

<table> 요소는 기본 프로퍼티 이외에 다음과 같은 프로퍼티를 지원합니다.

  • table.rows<tr>요소를 담은 컬렉션을 참조합니다.
  • table.caption/tHead/tFoot은 각각 <caption>, <thead>, <tfoot> 요소를 참조합니다.
  • table.tBodies<tbody> 요소를 담은 컬렉션을 참조합니다. 표준에 따르면, 테이블 내에 여러 개의 <tbody>가 존재하는 게 가능한데, 최소한 하나는 무조건 있어야 합니다. HTML 문서에는 <tbody>가 없더라도 브라우저는 <tbody> 노드를 DOM에 자동으로 추가합니다.

<thead>, <tfoot>, <tbody> 요소는 rows 프로퍼티를 지원합니다.

  • tbody.rows는 tbody 내 <tr> 요소 컬렉션을 참조합니다.

<tr> 요소는 다음 프로퍼티를 지원합니다.

  • tr.cells는 주어진 <tr> 안의 모든 <td>, <th>을 담은 컬렉션을 반환합니다.
  • tr.sectionRowIndex는 주어진 <tr><thead>/<tbody>/<tfoot>안쪽에서 몇 번째 줄에 위치하는지를 나타내는 인덱스를 반환합니다.
  • tr.rowIndex<table>내에서 해당 <tr>이 몇 번째 줄인 지를 나타내는 숫자를 반환합니다.

<td><th> 요소는 다음 프로퍼티를 지원합니다.

  • td.cellIndex<td><th>가 속한 <tr>에서 해당 셀이 몇 번째인지를 나타내는 숫자를 반환합니다.

용례:

<table id="table">
  <tr>
    <td>일</td><td>이</td>
  </tr>
  <tr>
    <td>삼</td><td>사</td>
  </tr>
</table>

<script>
  // '이'가 적힌 td를 가져옴(첫 번째 줄, 두 번째 칸)
  let td = table.rows[0].cells[1];
  td.style.backgroundColor = "red"; // 강조
</script>

표에 관련한 공식 명세서는 tabular data에서 찾아볼 수 있습니다.

테이블과 마찬가지로, HTML 폼(form)에만 쓸 수 있는 탐색 프로퍼티도 있습니다. 폼을 배우면서 이 프로퍼티에 대해서도 살펴보도록 하겠습니다.

요약

탐색 프로퍼티를 사용하면 이웃 노드로 바로 이동할 수 있습니다.

탐색 프로퍼티는 크게 두 개의 집합으로 나뉩니다.

  • 모든 노드에 적용 가능한 parentNode, childNodes, firstChild, lastChild, previousSibling, nextSibling
  • 요소 노드에만 적용 가능한 parentElement, children, firstElementChild, lastElementChild, previousElementSibling, nextElementSibling

테이블과 같은 몇몇 DOM 요소는 추가 프로퍼티와 콘텐츠에 접속할 수 있게 해주는 컬렉션을 제공합니다.

과제

중요도: 5

아래 페이지를 살펴봅시다.

<html>
<body>
  <div>사용자:</div>
  <ul>
    <li>John</li>
    <li>Pete</li>
  </ul>
</body>
</html>

아래 DOM 노드에 접근할 방법을 최소 한 가지 이상씩 생각해보세요.

  • <div> DOM 노드
  • <ul> DOM 노드
  • 두 번째 <li> (Pete)

방법은 다양합니다.

<div> DOM 노드:

document.body.firstElementChild
// 또는
document.body.children[0]
// 또는 (첫 번째 노드는 공백이므로 두 번째 노드를 가져옴)
document.body.childNodes[1]

<ul> DOM 노드:

document.body.lastElementChild
// 또는
document.body.children[1]

두 번째 <li> (Pete):

// <ul>을 가져오고, <ul>의 마지막 자식 요소를 가져옴
document.body.lastElementChild.lastElementChild
중요도: 5

임의의 DOM 요소 노드 elem이 있다고 가정해봅시다.

  • elem.lastChild.nextSibling은 항상 null일까요?
  • elem.children[0].previousSibling은 항상 null일까요?
  1. 네 맞습니다. elem.lastChild는 항상 마지막 노드이기 때문에 nextSibling이 없습니다.
  2. 아닙니다. elem.children[0]요소 노드 중 첫 번째 자식 노드를 나타내기 때문입니다. 이 앞에 요소 노드가 아닌 다른 노드가 올 수도 있습니다. previousSibling은 텍스트 노드가 될 수도 있습니다.

주의 사항: 두 경우 모두 자식 노드가 없는 경우 에러가 발생합니다.

자식 노드가 없으면 elem.lastChildnull이 되기 때문에 elem.lastChild.nextSibling에 접근할 수 없습니다. 그리고 컬렉션 elem.children은 빈 배열 []같이 빈 상태가 됩니다.

중요도: 5

테이블의 모든 대각선 셀을 빨간색으로 칠하는 코드를 작성해보세요.

<table>에서 모든 대각선 <td>를 가져와 아래 코드를 이용해 칠해야 합니다.

// td는 테이블 셀에 대한 참조가 되어야 합니다.
td.style.backgroundColor = 'red';

결과는 아래와 같습니다.

샌드박스를 열어 정답을 작성해보세요.

rowscells 프로퍼티를 이용해 테이블의 대각선 셀에 접근합니다.

샌드박스를 열어 정답을 확인해보세요.

튜토리얼 지도