DOM 트래버싱
하나의 요소, 즉 노드를 선택하면 해당되는 모든 자식 노드를 탐색하고 싶은 경우가 있을 것입니다.
예를 들어 목록 안에 있는 전체 목록 항목을 수정하는 등의 작업을 하고 싶은 경우가 있을 수 있습니다.
따라서 qurerySelector 등을 사용하여 선택하고 싶은 모든 요소를 수동으로 선택하는 것이 아니라 이미 선택한 요소를 이용해서
선택한 요소를 기반으로 자식 혹은 형제 요소 등으로 옮겨 갈 수 있는데 이것을 DOM 트래버싱이라고 합니다.
우선 자식, 후손, 부모, 조상 용어에 대해 정리하고 넘어가겠습니다.
<div>
<p>A <em>Example!</em></p>
</div>
자식(Child)
자식 요소는 다른 요소의 직접적인 자식 관계입니다.
자식 요소를 위의 코드로 설명하자면 <p> 태그는 <div>의 자식이며 직접적인 자식 요소이고 <div>의 바로 한 줄 아래에 위치하고 있습니다.
반면 <em> 태그는 자식 요소가 아닙니다. <em> 태그는 <div> 태그 안에 있는 것은 맞지만 직접적인 자식이 아니기 때문입니다.
후손(Descendant)
후손 요소는 다른 요소의 직접, 간접적인 자식 관계입니다.
후손 요소를 위의 코드로 설명하자면 <p> 태그와 <em> 태그는 <div>의 후손입니다.
즉 <p> 태그는 <div>의 자식이자 후손이며 <em> 태그는 자식은 아니지만 후손입니다.
부모(parent)
부모 요소는 자식과 반대라고 생각하시면 됩니다.
부모 요소는 다른 요소의 직접적인 부모 관계입니다.
위의 코드로 설명하자면 <div>는 <p> 태그의 부모이지만, <em> 태그의 경우 <div>는 직접적인 부모 요소가 아니기 때문에 부모가 아닙니다.
조상(Ancestor)
조상 요소는 후손과 반대라고 생각하시면 됩니다.
조상 요소는 다른 요소의 직접, 간접적인 부모 관계입니다.
위의 코드로 설명하자면 <div>는 <p> 태그와 <em> 태그의 조상입니다.
DOM트래버싱에 대해 이야기할 때에 각각의 차이점을 염두에 두시는 것이 중요합니다.
첫 부분에서 든 예처럼 선택된 요소 노드나 노드가 있는 경우 요소의 주변 또는 중첩된 요소를 탐색하려고 하는데 여기서 몇 가지 방향을 취할 수 있습니다.
1. 선택된 요소 노드나 노드는 parentElement 또는 parentNode를 사용하여 위로 올라가게 되면 만날 부모 요소 노드 또는 부모 노드를 선택할 수 있습니다.
즉 선택할 수 있는 모든 요소 객체는 parentElement 또는 parentNode 프로퍼티를 가지는 것입니다.
두 프로퍼티의 차이점은 parentNode의 경우 부모 노드를 선택하고 parentElement는 부모 요소 노드를 선택합니다.
즉, parentElement의 경우 부모 HTML 요소만 선택하는 것입니다.
그리고 closest() 메서드를 사용할 수 있는데 직접적인 부모 말고도 특정 CSS쿼리와 일치하는 모든 조상을 사용하는 메서드입니다.
2. 반대로 부모나 조상 요소를 찾아가는 게 아니라 자식이나 후손 요소를 선택할 수 도 있습니다.
childNodes와 children 프로퍼티를 사용할 수 있습니다.
childNodes는 텍스트 노드를 포함한 모든 자식 노드를 선택하며 children은 모든 자식 요소 노드를 선택합니다.
3. 이외에 사용 가능한 특별한 프로퍼티가 존재합니다.
주어진 요소에서 첫 번째 자식 노드를 선택하는 firstChild와 첫 번째 자식 요소 노드를 선택하는 firstElementChild가 있으며
마지막 자식 노드를 선택하는 lastChild와 마지막 자식 요소 노드를 선택할 수 있는 lastElementChild가 있고
같은 레벨에 머물고 있는 형제 요소를 확인하려면 previousSibling과 previousElementSibling을 DOM 객체에 사용하면 됩니다.
previousSibling과 previousElementSibling 메서드를 통해 요소의 직접적인 형제를 구할 수 있습니다.
그리고 현재 선택된 요소의 전과 후 모두에서 형제 요소를 구할 수 있도록 양방향으로 작용합니다.
previousSibling과 previousElementSibling은 이전 노드 또는 이전 요소 노드를 선택할 수 있으며
nextSibling과 nextElementSibling은 다음 노드 또는 다음 요소 노드를 선택할 수 있습니다.
아래에서 예시 코드를 이용하여 위의 내용을 정리해보겠습니다.
children과 childNodes
<body>
<header><h1 id="main-title">Dive into the DOM!</h1></header>
<ul>
<li class="list-item">Item1</li>
<li class="list-item">Item2</li>
<li class="list-item">Item3</li>
</ul>
<input id="input-1" class="input-default" value="Enter text..." />
</body>
만약 <ul> 태그 안에 있는 두 번째 <li> 태그에 접근하고 싶다면 querySelector 등의 같은 쿼리로는 접근이 불가능합니다.
때문에 유일하게 식별할 수 있는 CSS 선택자를 추가해주어야 접근이 가능합니다.
하지만 위에서 정리한 내용을 적용하면 접근이 가능합니다.
<li> 태그의 부모 요소인 <ul> 태그에 접근하여 자식 요소 또는 자식 노드를 살핀 뒤 두 번째 자식을 선택하면 됩니다.
children 프로퍼티를 사용하면 HTMLCollection이 보이는데 이것은 유사 배열 객체이기 때문에 ul.children[1]으로 유사 배열의 두 번째 li에 접근이 가능하게 됩니다.
(유사 배열 객체 : 실제 배열은 아니지만 반복문 등 배열에 지원되는 것을 많은 부분 지원받습니다.)
children을 사용하면 HTML 태그와 요소를 바탕으로 한 모든 자식 요소 노드에 접근을 할 수 있습니다.
텍스트 노드의 경우 DOM 트리의 일부로써, 이 트리는 HTML 문서를 반영하여 브라우저가 만듭니다. 코드 가독성을 향상하기 위해 HTML 파일에 추가한 공백조차 HTML 문서의 텍스트이기 때문에 텍스트 노드로 처리됩니다.
따라서 children이 <ul>의 직접 자식인 요소 노드의 컬렉션을 반환하고 다른 종류의 노드는 반환하지 않습니다.
<ul>
<li class="list-item">Item1</li>
<li class="list-item">Item2</li>
<li class="list-item">Item3</li>
</ul>
대신 childNodes를 사용하면 유사 배열 객체인 HTMLCollection 대신 유사 배열 객체인 NodeList를 반환합니다.
위의 설명처럼 텍스트 노드의 경우 가독성을 위해 <li> 앞에 추가한 공백조차도 텍스트 노드로 처리하기 때문에 NodeList 안에 text가 들어가 있는 것입니다.
NodeList 안에는 텍스트 노드와 요소 노드가 들어가 있는데 이것은 childNode를 사용하면 모든 자식 노드를 선택하였기 때문입니다.
정리
children과 childNodes로 자식 노드를 선택할 수 있으며, 대부분 자식 요소 노드에 접근하고 싶기 때문에 children을 자주 사용합니다.
그러나 자식 요소 노드뿐만 아니라 모든 노드에 접근하고 싶다면 childNodes를 사용합니다.
( children : 자식 요소 노드에만 접근할 경우 // childNodes : 모든 노드에 접근할 경우)
firstChild, lastChild와 firstElementChild, lastElementChild
<ul>
<li class="list-item">Item1</li>
<li class="list-item">Item2</li>
<li class="list-item">Item3</li>
</ul>
마지막 요소 노드에 접근하고 싶은 경우 last-of-type을 통해 접근을 할 수 있지만 꽤 복잡한 CSS 쿼리를 스캔해야 해당 요소 노드에 접근할 수 있습니다.
아래의 firstChild와 firstElementChild, lastChild와 lastElementChild를 사용하면 보다 쉽게 선택이 가능합니다.,
firstChild와 firstElementChild를 사용한다면 첫 번째 자식 노드나 자식 요소 노드에 보다 빠르고 쉽게 접근이 가능합니다.
firstChild의 경우 모든 자식 노드, 즉 텍스트 노드까지 포함된 노드 중에서 첫 번째 노드에 접근을 하였기 때문에 텍스트 노드를 반환합니다.
firstElementChild의 경우 자식 요소 노드 중에서 첫 번째 노드에 접근을 하였기 때문에 첫 번째 <li>를 반환합니다.
lastChild와 lastElementChild의 경우도 마찬가지로 적용되며 마지막 자식 노드나 자식 요소 노드에 접근이 가능합니다.
parentNode와 parentElement
<ul>
<li class="list-item">Item1</li>
<li class="list-item">Item2</li>
<li class="list-item">Item3</li>
</ul>
parentNode와 parentElement를 사용하면 부모 노드나 부모 요소 노드에 접근이 가능합니다.
parentNode의 경우 가장 가까운 부모 노드에 접근을 합니다.
parentElement의 경우 가장 가까운 부모 자식 노드에 접근을 합니다.
이 경우 parentNode와 parentElement의 결과가 같습니다. 왜냐하면 요소 노드만이 자식 노드를 가질 수 있기 때문입니다.
(텍스트 노드는 자식 노드를 가질 수 없습니다.)
현재 선택된 부분이 <li> 요소 노드이고 부모는 무조건 요소 노드이기 때문에 반환 값이 같은 것입니다.
반환값이 같은데 parentNode와 parentElement를 구분해서 사용하는 이유는 아래와 같은 예외 경우가 있기 때문입니다.
document.documentElement; // 예외 경우
예외 경우는 문서 전체에 접근하는 경우입니다.
document.documentElement에 parentNode를 사용할 경우만 전체 문서 객체를 부모 노드로 가지게 됩니다.
하지만 위의 경우로 전체 HTML 문서에 접근하는 것보다 document로 바로 접근하는 것이 효율적이기 때문에 사용할 일이 거의 없습니다.
따라서 원하는 대로 parentNode와 parentElement 둘 중 하나를 사용하시면 됩니다.
closest
아래 코드 예시로 조상 요소 노드에 접근하는 것을 정리해보겠습니다.
document.body를 사용하면 body에 빠르게 접근이 가능하지만 자식 요소 노드에서 접근하는 것을 예시로 들어보겠습니다.
<body>
<header><h1 id="main-title">Dive into the DOM!</h1></header>
<ul>
<li class="list-item">Item1</li>
<li class="list-item">Item2</li>
<li class="list-item">Item3</li>
</ul>
<input id="input-1" class="input-default" value="Enter text..." />
</body>
첫 번째 <li> 요소에서 <body> 요소로 접근하고 싶은 경우 직접적인 부모에 접근하는 것이 아닌 간접적 관계인 조상에 접근하는 것입니다.
따라서 <li>에 parentElement를 사용하는 것은 도움이 되지 않습니다. parentElement는 가장 가까운 부모에 접근하는 것이기 때문입니다.
이 경우 다른 조상(자기 자신을 포함한 조상)에 접근을 하려면 closest을 사용해주시면 됩니다. closest의 경우 querySelector와 같이 CSS 선택자를 사용합니다.
만약 CSS 선택자에 조상이 아닌 header을 넣게 되면 null이 나오게 됩니다.
정리
closest은 요소 트리에 있는 자기 자신을 포함한 아무 조상을 선택할 때 좋습니다.
(간접적으로 선택한 요소 노드를 감싸고 있으면 조상입니다.)
previousSibling, previousElementSibling과 nextSibling, nextElementSibling
아래 예시 코드로 형제 요소 노드에 접근하는 것을 정리해보겠습니다.
<body>
<header><h1 id="main-title">Dive into the DOM!</h1></header>
<ul>
<li class="list-item">Item1</li>
<li class="list-item">Item2</li>
<li class="list-item">Item3</li>
</ul>
<input id="input-1" class="input-default" value="Enter text..." />
</body>
이번에는 <ul> 또는 <li> 요소 노드를 선택하여 <header> 요소 노드로 접근해보겠습니다.
<header> 요소 노드는 <ul> 요소 노드와 같은 수준에 위치하기 때문에 형제 요소 노드입니다.
이럴 경우 previousSibling나 previousElementSibling를 사용하시면 형제 노드나 형제 요소 노드에 접근이 가능합니다.
previousSibling의 경우 노드, 즉 텍스트 노드까지 포함한 노드 중에 이전 형제 노드에 접근합니다.
<header> 요소 노드 끝에 줄 바꿈이 일어났는데 이것이 텍스트 노드 처리되었기 때문에 <ul> 요소 노드의 가장 이전 노드인 텍스트 노드를 선택합니다.
previousElementSibling의 경우 요소 노드 중에 이전 형제 요소 노드에 접근합니다.
때문에 원하는 요소 노드인 <header> 요소 노드에 접근이 가능합니다.
이전 방향과 반대 방향도 가능합니다. <ul> 요소 노드의 뒤에 위치한 <input> 요소 노드에 접근하고 싶다면 nextElementSibling를 사용해주시면 됩니다.
nextSibling은 previousSibling과 마찬가지로 노드 중 이후 형제 노드에 접근하는 것이기 때문에 <ul> 요소 노드 뒤에서 일어난 줄 바꿈과 공백이 텍스트 노드 처리된 것을 반환하게 됩니다.
정리
previousSibling는 이전 형제 노드에 접근합니다.
previousElementSibling는 이전 형제 요소 노드에 접근합니다.
nextSibling는 다음 형제 노드에 접근합니다.
nextElementSibling는 다음 형제 요소 노드에 접근합니다.
지금까지 정리한 탐색 기법을 사용한다면 전체 문서를 읽고 쿼리 하는 qurerySelector 등의 사용보다 효과적이고 빠르게 요소 노드나 노드에 접근이 가능합니다.
'JavaScript' 카테고리의 다른 글
요소 생성과 요소 삽입 (0) | 2022.08.28 |
---|---|
classList (0) | 2022.08.28 |
속성(Attribute)과 프로퍼티(Propertie) (0) | 2022.08.24 |
노드 쿼리 메서드 (0) | 2022.08.24 |
노드(Node)와 요소(Element) (0) | 2022.08.23 |