[JS] DOM 탐색하기
📌 참고 사이트
https://ko.javascript.info/dom-navigation
DOM 탐색하기
ko.javascript.info
📋 DOM 접근하기
요소에 어떤 작업을 하고자 할 때에는 우선 조작하고자 하는 DOM 객체에 접근하는 것부터 선행되어야 한다.
DOM에 수행하는 모든 연산은 진입점 역할을 하는 document 객체에서 시작한다.
📍 트리 상단의 documentElement와 body
➡️ <html>
document.documentElement를 통해 접근 가능하다.
html은 document를 제외하고 DOM 트리의 가장 위에 위치한다.
➡️ <body>
document.body는 <body> 요소에 해당하는 DOM 노드이다.
➡️ <head>
document.head로 접근이 가능하다.
❓ DOM에서의 null
DOM에서의 null은 해당하는 노드가 없음을 나타낸다.
다음과 같은 코드에서 null의 결과를 볼 수 있다.
<html>
<head>
<script>
alert(document.body); // null, 아직 <body>에 해당하는 노드가 생성되지 않았음
</script>
</head>
<body>
<script>
alert(document.body); // [object HTMLBodyElement], 지금은 노드가 존재하므로 읽을 수 있음
</script>
</body>
</html>
<head> 안에서의 alert(document.body)에서는 아직 <body>가 있는 곳까지 브라우저가 읽지 않았기 때문에 접근이 불가하므로 null이 뜨게 된다.
but <body> 안에서의 alert(document.body)에서는 브라우저가 <body>를 읽고 노드가 생성된 상태이기 때문에 접근이 가능하여 null이 뜨지 않게 된다.
📋 자식 노드 탐색하기
다음과 같은 HTML 코드가 있다고 해 보자.
<html>
<body>
<div>시작</div>
<ul>
<li>
<b>항목</b>
</li>
</ul>
</body>
</html>
❓ 자식 노드 (child node)
자식 노드는 부모 노드의 바로 아래에서 중첩 관계를 만든다.
위의 코드를 보면 <head>와 <body>는 <html>의 자식 노드인 것을 볼 수 있다.
❓ 후손 노드 (descendants)
어떤 노드의 자식 노드와 자식 노드의 모든 자식 노드들이 후손 노드가 된다.
위의 코드에서 <body>의 후손 노드는 자식 노드인 <div>, <ul> 뿐만 아니라 <li>와 <b> 같은 더 안쪽에 있는 요소들도 모두 될 수 있다.
📍 childNodes
chilsNodes 컬렉션은 텍스트 노드를 포함한 모든 자식 노드를 담고 있다.
element.childNodes의 형식으로 사용하면 된다.
위의 예시에서 <body>의 모든 자식 요소들의 태그명을 출력하려면 다음과 같이 하면 된다.
document.body.childNodes.forEach((node) => console.log(node.tagName));
// undefined, DIV, undefined, UL, undefined
// undefined는 모두 텍스트 노드(공백, 줄바꿈)에 해당한다.
📍 firstChild, lastChild
firstChild와 lastChild 프로퍼티를 이용하면 각각 첫 번째, 마지막 자식 노드에 빠르게 접근할 수 있다.
즉, element.firstChild === element.childNodes[0], 그리고 element.lastChild === element.childNodes[element.childNodes.length - 1] 이라고 볼 수 있다.
💡 자식 노드의 존재 여부를 검사할 때는 element.hasChildNodes()를 사용할 수 있다.
📋 DOM 컬렉션
위에서 본 childNodes는 배열 같지만 사실은 배열이 아닌 반복 가능한(iterable) 유사 배열 객체인 컬렉션(collection)이다.
컬렉션은 다음과 같은 특징을 가진다.
➡️ for ... of를 사용할 수 있다.
반복 가능한 객체이기 때문에 Symbol.iterator 프로퍼티가 구현되어 있어 for ...of 문을 사용하는 것이 가능하다. (비슷한 forEach도 사용 가능하다.)
바로 위에서 본 html을 예시로 사용해 보겠다.
for (let node of document.body.childNodes) {
console.log(node);
}
// #text, <div>시작</div>, #text, <ul>…</ul>, #text
for ... of 문이 제대로 작동하여 <body>의 모든 자식 노드들을 출력하는 것을 볼 수 있다.
❗ 이때 for ... in 반복문을 사용하려 하지 말자.
for ... in 반복문은 객체의 모든 열거 가능한 프로퍼티를 순회하기 때문에 거의 사용되지 않는 프로퍼티까지 전부 순회할 수가 있다.
➡️ 배열이 아니기 때문에 배열에 사용되는 일부 메서드를 사용할 수 없다.
아래와 같은 코드로는 원하는 결과를 얻을 수 없다.
console.log(document.body.childNodes.filter);
// undefined (filter 메서드가 없다.)
위와 같이 filter 뿐만 아니라 map(유효한 함수가 아니라는 오류를 뱉음.) 같은 것들도 사용 불가이다.
❗ DOM 컬렉션은 읽는 것만 가능하다.
DOM 컬렉션은 읽기 전용으로, childNodes[i] = .... 등과 같은 방식으로 자식 노드를 교체하는 것이 불가하다.
따라서 배열에 사용되는 메서드 사용이 불가하다.
📋 형제와 부모 노드
형제(sibling) 노드는 같은 부모 노드를 가진 같은 레벨에 있는 노드들을 말한다.
대표적으로 <html>을 공통 부모 노드로 가지는 <head>와 <body>는 형제 노드이다.
📍 nextSibling, previousSibling
이때 <head>의 바로 다음 형제 노드, 즉 nextSibling은 <body> 노드가 되고, <body>의 바로 이전 형제 노드, 즉 previousSibling은 <head> 노드가 된다.
이 둘은 프로퍼티로 사용이 가능하다.
console.log(document.head.nextSibling); // <body></body>
console.log(document.body.previousSibling); // <head></head>
📍 parentNode
<head>와 <body>의 부모 노드, 즉 parentNode는 <html>이 된다.
이것도 프로퍼티로 사용이 가능하다.
console.log(document.head.parentNode); // <html>...</html>
console.log(document.body.parentNode); // <html>...</html>
📋 요소 간 이동
childNodes를 이용하면 텍스트 노드, 요소 노드, 주석 노드까지 참조할 수 있는데, 텍스트 노드나 주석 노드는 잘 다루지 않는다.
대부분 요소 노드를 조작하는 작업에 이용된다.
요소 간의 관계를 볼 때는 다음의 것들을 기억하자.
➡️ children 프로퍼티는 해당 요소의 자식 노드 중 요소 노드만을 가리킨다.
(childNodes는 비요소 노드들을 포함하여 값을 반환하지만 children 프로퍼티를 사용하면 비요소 노드들은 모두 제외되고 오로지 요소 노드들만을 얻을 수 있다.)
➡️ firstElementChild와 lastElementChild 프로퍼티는 각각 첫 번째 자식 요소 노드와 마지막 자식 요소 노드를 가리킨다.
➡️ previousElementSibling과 nextElementSibling은 각각 바로 이전 형제 요소 노드, 바로 다음 형제 요소 노드를 가리킨다.
➡️ parentElement는 부모 요소 노드를 가리킨다.
💡 parentElement는 부모 '요소' 노드를 반환하지만, parentNode는 종류에 상관 없이 부모면 반환한다.
두 프로퍼티를 사용하면 대부분의 경우에서 같은 결과를 볼 수 있지만 다음과 같은 경우에서는 다른 결과를 반환한다.
// document.documentElement는 html을 의미한다.
console.log(document.documentElement.parentNode); // document
console.log(document.documentElement.parentElement); // null
html의 부모는 document이다.
그런데 document는 '요소' 노드가 아니라 '문서' 노드이기 때문에 그냥 부모 노드면 반환하는 parentNode에서는 document가 출력되지만, 부모 '요소' 노드를 반환하는 parentElement에서는 null이 출력된다.
따라서 parentElement는 상위 객체인 <html>까지 올라가고 싶은데, document 까지는 도달하고 싶지 않을 때 사용하면 유용하다.
✏️ 과제
📍 자식 DOM
아래 페이지를 살펴보자.
<html>
<body>
<div>사용자:</div>
<ul>
<li>John</li>
<li>Pete</li>
</ul>
</body>
</html>
아래 DOM 노드에 접근할 방법을 최소 한 가지 이상씩 생각해 보자.
➡️ <div> DOM 노드
➡️ <ul> DOM 노드
➡️ 두 번째 <li> (Pete)
🅰️
💻 <div>에 접근하기
// 1. body의 첫 번째 자식으로 접근하기
document.body.firstElementChild; // 그냥 firstChild는 text(공백)임에 주의!
document.body.children[0];
document.body.childNodes[1]; // 첫 번째 노드인 공백 건너뛰고 두 번째 노드를 가져옴.
// 2. ul의 이전 형제로 접근하기
document.body.children[1].previousElementSibling;
💻 <ul>에 접근하기
// 1. body의 두 번째 자식으로 접근하기
document.body.children[1];
document.body.lastElementChild;
// 2. div의 바로 다음 형제로 접근하기
document.body.children[0].nextElementSibling;
💻 두 번째 <li>에 접근하기
// ul의 2번째 자식으로 접근하기
document.body.children[1].children[1];
document.body.lastElementChild.lastElementChild;
📍 형제 노드에 관한 질문
임의의 DOM 요소 노드 element가 있다고 가정해 보자. (element의 자식 노드는 항상 존재한다고 가정)
1️⃣ element.lastChild.nextSibling은 항상 null일까?
2️⃣ element.children[0].previousSibling은 항상 null일까?
🅰️
1️⃣ 맞다. element.lastChild는 항상 마지막 노드(요소 노드 뿐만 아니라 다른 노드 통틀어서)이므로 그 다음 형제 노드는 존재하지 않는다.
2️⃣ 아니다. element.children[0]은 첫 번째 자식 '요소' 노드를 나타내므로 요소 노드가 아닌 노드(텍스트 노드 같은 것)가 앞에 위치해 있다면 그것이 이전 형제 노드가 되어 반환 가능하다.