HTML을 지탱하는 것은 태그이다.
문서 객체 모델에 따르면, 모든 HTML 태그는 객체이다. 태그 하나가 감싸고 있는 '자식' 태그는 중첩 태그라고 부른다. 태그 내의 문자 역시 객체이다.
이런 모든 객체는 자바스크립트를 통해 접근할 수 있고, 페이지를 조작할 때 이 객체를 사용한다.
간단한 문서를 이용해 DOM 구조
<script>
<!DOCTYPE HTML>
<html>
<head>
<title>사슴에 관하여</title>
</head>
<body>
사슴에 관한 진실.
</body>
</html>
</script>
DOM은 HTML을 아래와 같이 태그 트리 구조로 표현한다.
트리에 있는 노드는 모두 객체이다. 태그는 요소 노드이고, 트리 구조를 구성한다. html은 루트 노드가 되고, head와 body는 루트 노드의 자식이 된다.
요소 내의 문자는 텍스트 노드가 된다. 위 그림에서 #text 텍스트 노드는 문자열만 담는다. 자식 노드를 가질 수 없고, 트리의 끝에서 잎 노드가 된다.
기형적인 HTML을 만나면 브라우저는 DOM 생성과정에서 HTML을 자동으로 교정한다.
예를 들어 가장 최상위 태그는 항상 html이어야 하는데 문서에 html 태그가 없는 경우, 문서 최상위에 자동으로 넣어준다. 따라서 DOM에는 html에 대응하는 노드가 항상 있다. body도 같은 방식이 적용된다.
<script>
<p>안녕하세요
<li>엄마
<li>그리고
<li>아빠
</script>
위와 같이 닫는 태그가 없이 짝이 안맞아도 브라우저는 태그를 읽고, 자동으로 빠진 부분을 넣어 채워 준다. 따라서 최종 결과물은 정상적인 DOM이 된다.
요소와 텍스트 노드 외에도 다양한 노드 타입이 있다.
주석도 노드가 된다.
<script>
<!DOCTYPE HTML>
<html>
<body>
사슴에 관한 진실.
<ol>
<li>사슴은 똑똑합니다.</li>
<!-- comment -->
<li>그리고 잔꾀를 잘 부리죠!</li>
</ol>
</body>
</html>
</script>
DOM을 이용하면 요소와 요소의 컨텐츠에 무엇이든 할 수 있다. 하지만 무언가를 하기 전엔, 당연히 조작하고자 하는 DOM 객체에 접근하는 것이 선행되어야 한다.
DOM에 수행하는 모든 연산은 document 객체에서 시작한다. document 객체는 DOM에 접근하기 위한 '진입점'이다. 진입점을 통과하면 어떤 노드에도 접근할 수 있다.
DOM 트리 상단의 노드들은 document가 제공하는 프로퍼티를 사용해 접근할 수 있다.
html = document.documentElement
document를 제외하고 DOM 트리 꼭대기에 있는 문서 노드는 html 태그에 해당하는 document.documentElement 이다.
body = document.body
document.body는 body 요소에 해당하는 DOM 노드로, 자주 쓰이는 노드 중 하나이다.
head = document.head
head 태그는 document.head로 접근할 수 있다.
요소들이 가까이 붙어있다면 DOM 탐색 프로퍼티를 사용해 목표 요소에 쉽게 접근할 수 있다. 하지만, 요소들이 가까이 붙어있지 않은 경우도 있기 마련이다.
document.getElementById 혹은 id를 사용해 요소 검색하기
요소에 id 속성이 있으면 위치에 상관없이 메서드 document.getElementById를 이용해 접근할 수 있다.
<script>
<div id="elem">
<div id="elem-content">Element</div>
</div>
<script>
// 요소 얻기
let elem = document.getElementById('elem');
// 배경색 변경하기
elem.style.background = 'red';
</script>
</script>
그런데 이렇게 요소 id를 따서 자동으로 선언된 전역변수는 동일한 이름을 가진 변수가 선언되면 무용지물이 된다.
<script>
<div id="elem"></div>
<script>
let elem = 5; // elem은 더이상 <div id="elem">를 참조하지 않고 5가 됩니다.
alert(elem); // 5
</script>
</script>
id를 따서 만들어진 전역변수를 요소 접근 시 사용하지 말 것
id에 대응하는 전역변수는 명세서의 내용을 구현해 만들어진 것으로 표준이긴 하지만 하위 호환성을 위해 남겨둔 동작이다.
브라우저는 스크립트의 네임스페이스와 DOM의 네임스페이스를 함께 사용할 수 있도록 해서 개발자의 편의를 도모한다. 그런데 이런 방식은 스크립트가 간단할 땐 괜찮지만, 이름이 충돌할 가능성이 있기 때문에 추천하는 방식은 아니다.
elem.querySelectorAll은 다재다능한 요소 검색 메서드이다. 이 메서드는 elem의 자식 요소 중 주어진 CSS 선택자에 대응하는 요소 모두를 반환한다.
<script>
<ul>
<li>1-1</li>
<li>1-2</li>
</ul>
<ul>
<li>2-1</li>
<li>2-2</li>
</ul>
<script>
let elements = document.querySelectorAll('ul > li:last-child');
for (let elem of elements) {
alert(elem.innerHTML); // 마지막 <li>요소 모두 반환 "1-2", "2-2"
}
</script>
</script>
가상 클래스도 사용할 수 있다.
querySelectorAll에는 :hover나 :active 같은 CSS 선택자의 가상 클래스도 사용할 수 있다. document.querySelectorAll(':hover')을 사용하면 마우스 포인터가 위에 있는 요소 모두를 담은 컬렉션이 반환된다.
elem.querySelector는 주어진 CSS 선택자에 대응하는 요소 중 첫 번째 요소를 반환한다.
부모 요소, 부모 요소의 부모 요소등 DOM 트리에서 특정 요소의 상위에 있는 요소들은 조상 요소라고 한다.
메서드 elem.closest는 elem 자기 자신을 포함하여 CSS 선택자와 일치하는 가장 가까운 조상 요소를 찾을 수 있게 도와준다.
closest 메서드는 해당 요소부터 시작해 DOM 트리를 한 단계씩 거슬러 올라가면서 원하는 요소를 찾는다. CSS 선택자와 일치하는 요소를 찾으면, 검색을 중단하고 해당 요소를 반환한다.
<script>
<h1>목차</h1>
<div class="contents">
<ul class="book">
<li class="chapter">1장</li>
<li class="chapter">2장</li>
</ul>
</div>
<script>
let chapter = document.querySelector('.chapter'); // LI
alert(chapter.closest('.book')); // UL
alert(chapter.closest('.contents')); // DIV
alert(chapter.closest('h1')); // null(h1은 li의 조상 요소가 아님)
</script>
</script>
태그나 클래스 등을 이용해 원하는 노드를 찾아주는 메서드도 있다.
querySelector를 이용하는게 더 편리하고 문법도 짧아서, 요즘은 이런 메서드들을 잘 쓰진 않는다.
elem.getElementsByTagName(tag) – 주어진 태그에 해당하는 요소를 찾고, 대응하는 요소를 담은 컬렉션을 반환한다. 매개변수 tag에 "*"이 들어가면, '모든 태그’가 검색된다.
elem.getElementsByClassName(className) – class 속성값을 기준으로 요소를 찾고, 대응하는 요소를 담은 컬렉션을 반환한다.
document.getElementsByName(name) – 아주 드물게 쓰이는 메서드로, 문서 전체를 대상으로 검색을 수행한다. 검색 기준은 name 속성값이고, 이 메서드 역시 검색 결과를 담은 컬렉션을 반환한다.