2022년 8월 24일

getElement*, querySelector*로 요소 검색하기

요소들이 가까이 붙어있다면 앞서 학습한 DOM 탐색 프로퍼티를 사용해 목표 요소에 쉽게 접근할 수 있습니다. 그런데, 요소들이 가까이 붙어있지 않은 경우도 있기 마련입니다. 상대 위치를 이용하지 않으면서 웹 페이지 내에서 원하는 요소 노드에 접근하는 방법은 없는 걸까요?

이번 챕터에선 이를 가능하게 해주는 메서드에 대해 알아보겠습니다.

document.getElementById 혹은 id를 사용해 요소 검색하기

요소에 id 속성이 있으면 위치에 상관없이 메서드 document.getElementById(id)를 이용해 접근할 수 있습니다.

예시:

<div id="elem">
  <div id="elem-content">Element</div>
</div>

<script>
  // 요소 얻기
  let elem = document.getElementById('elem');

  // 배경색 변경하기
  elem.style.background = 'red';
</script>

이에 더하여 id 속성값을 그대로 딴 전역 변수를 이용해 접근할 수도 있습니다.

<div id="elem">
  <div id="elem-content">Element</div>
</div>

<script>
  // 변수 elem은 id가 'elem'인 요소를 참조합니다.
  elem.style.background = 'red';

  // id가 elem-content인 요소는 중간에 하이픈(-)이 있기 때문에 변수 이름으로 쓸 수 없습니다.
  // 이럴 땐 대괄호(`[...]`)를 사용해서 window['elem-content']로 접근하면 됩니다.
</script>

그런데 이렇게 요소 id를 따서 자동으로 선언된 전역변수는 동일한 이름을 가진 변수가 선언되면 무용지물이 됩니다.

<div id="elem"></div>

<script>
  let elem = 5; // elem은 더이상 <div id="elem">를 참조하지 않고 5가 됩니다.

  alert(elem); // 5
</script>
id를 따서 만들어진 전역변수를 요소 접근 시 사용하지 마세요.

id에 대응하는 전역변수는 명세서의 내용을 구현해 만들어진 것으로 표준이긴 하지만 하위 호환성을 위해 남겨둔 동작입니다.

브라우저는 스크립트의 네임스페이스와 DOM의 네임스페이스를 함께 사용할 수 있도록 해서 개발자의 편의를 도모합니다. 그런데 이런 방식은 스크립트가 간단할 땐 괜찮지만, 이름이 충돌할 가능성이 있기 때문에 추천하는 방식은 아닙니다. HTML을 보지 않은 상황에서 코드만 보고 변수의 출처를 알기 힘들다는 단점도 있습니다.

본 튜토리얼에선 간결성을 위해 요소의 출처가 명확한 경우, id를 사용해 요소에 직접 접근하는 방법을 사용할 예정입니다.

실무에선 document.getElementById를 사용하시길 바랍니다.

id는 중복되면 안 됩니다.

id는 유일무이해야 합니다. 문서 내 요소의 id 속성값은 중복되어선 안 됩니다.

같은 id를 가진 요소가 여러 개 있으면 document.getElementById같이 id를 이용해 요소를 검색하는 메서드의 동작이 예측 불가능해집니다. 검색된 여러 요소 중 어떤 요소를 반환할지 판단하지 못해 임의의 요소가 반환되죠. 문서 내 동일 id가 없도록 해 이런 일을 방지하도록 합시다.

anyNode.getElementById가 아닌 document.getElementById

getElementByIddocument 객체를 대상으로 해당 id를 가진 요소 노드를 찾아 줍니다. 문서 노드가 아닌 다른 노드엔 호출할 수 없습니다.

querySelectorAll

elem.querySelectorAll(css)은 다재다능한 요소 검색 메서드입니다. 이 메서드는 elem의 자식 요소 중 주어진 CSS 선택자에 대응하는 요소 모두를 반환합니다.

아래 예시는 마지막 <li>요소 모두를 반환합니다.

<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); // "1-2", "2-2"
  }
</script>

querySelectorAll은 CSS 선택자를 활용할 수 있다는 점에서 아주 유용합니다.

가상 클래스도 사용할 수 있습니다.

querySelectorAll에는 :hover:active 같은 CSS 선택자의 가상 클래스(pseudo-class)도 사용할 수 있습니다. document.querySelectorAll(':hover')을 사용하면 마우스 포인터가 위에 있는(hover 상태인) 요소 모두를 담은 컬렉션이 반환됩니다. 이때 컬렉션은 DOM 트리 최상단에 위치한 <html>부터 가장 하단의 요소 순으로 채워집니다.

querySelector

elem.querySelector(css)는 주어진 CSS 선택자에 대응하는 요소 중 첫 번째 요소를 반환합니다.

elem.querySelectorAll(css)[0]과 동일하죠. elem.querySelectorAll(css)[0]은 선택자에 해당하는 모든 요소를 검색해 첫 번째 요소만을 반환하고, elem.querySelector는 해당하는 요소를 찾으면 검색을 멈춘다는 점에서 차이가 있습니다. elem.querySelector가 더 빠른 이유이죠. querySelectorquerySelectorAll에 비해 코드의 길이가 짧다는 장점도 있습니다.

matches

지금까지 소개한 모든 메서드는 DOM 검색에 쓰입니다.

elem.matches(css)는 DOM을 검색하는 일이 아닌 조금 다른 일을 합니다. 이 메서드는 요소 elem이 주어진 CSS 선택자와 일치하는지 여부를 판단해줍니다. 일치한다면 true, 아니라면 false를 반환하죠.

요소가 담겨있는 배열 등을 순회해 원하는 요소만 걸러내고자 할 때 유용합니다.

예시:

<a href="http://example.com/file.zip">...</a>
<a href="http://ya.ru">...</a>

<script>
  // document.body.children가 아니더라도 컬렉션이라면 이 메서드를 적용할 수 있습니다.
  for (let elem of document.body.children) {
    if (elem.matches('a[href$="zip"]')) {
      alert("주어진 CSS 선택자와 일치하는 요소: " + elem.href );
    }
  }
</script>

closest

부모 요소, 부모 요소의 부모 요소 등 DOM 트리에서 특정 요소의 상위에 있는 요소들은 조상(ancestor) 요소라고 합니다.

메서드 elem.closest(css)elem 자기 자신을 포함하여 CSS 선택자와 일치하는 가장 가까운 조상 요소를 찾을 수 있게 도와줍니다.

closest메서드는 해당 요소부터 시작해 DOM 트리를 한 단계씩 거슬러 올라가면서 원하는 요소를 찾습니다. CSS 선택자와 일치하는 요소를 찾으면, 검색을 중단하고 해당 요소를 반환합니다.

예시:

<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>

getElementsBy*

태그나 클래스 등을 이용해 원하는 노드를 찾아주는 메서드도 있습니다.

querySelector를 이용하는 게 더 편리하고 문법도 짧아서, 요즘은 이런 메서드들을 잘 쓰진 않습니다.

튜토리얼의 완성도를 높이고 오래된 스크립트에서 해당 메서드들을 만날 때 당황하지 않으시길 바라면서 이 메서드들을 잠시 언급하도록 하겠습니다.

  • elem.getElementsByTagName(tag) – 주어진 태그에 해당하는 요소를 찾고, 대응하는 요소를 담은 컬렉션을 반환합니다. 매개변수 tag"*"이 들어가면, '모든 태그’가 검색됩니다.
  • elem.getElementsByClassName(className) – class 속성값을 기준으로 요소를 찾고, 대응하는 요소를 담은 컬렉션을 반환합니다.
  • document.getElementsByName(name) – 아주 드물게 쓰이는 메서드로, 문서 전체를 대상으로 검색을 수행합니다. 검색 기준은 name 속성값이고, 이 메서드 역시 검색 결과를 담은 컬렉션을 반환합니다.

예시:

// 문서 내 모든 div를 얻습니다.
let divs = document.getElementsByTagName('div');

다음 예시는 표 안의 모든 input 태그를 찾습니다.

<table id="table">
  <tr>
    <td>나이:</td>

    <td>
      <label>
        <input type="radio" name="age" value="young" checked> 18세 미만
      </label>
      <label>
        <input type="radio" name="age" value="mature"> 18세 이상, 60세 미만
      </label>
      <label>
        <input type="radio" name="age" value="senior"> 60세 이상
      </label>
    </td>
  </tr>
</table>

<script>
  let inputs = table.getElementsByTagName('input');

  for (let input of inputs) {
    alert( input.value + ': ' + input.checked );
  }
</script>
's'를 절대 빠트리지 마세요!

초보 개발자들은 가끔 's'를 빼먹는 실수를 하곤 합니다. getElementsByTagName를 써야 하는데 getElementByTagName을 입력하곤 하죠.

getElementById는 요소 하나만을 반환하기 때문에 s가 없습니다. getElementsByTagName 등의 메서드는 대응하는 요소를 담은 컬렉션을 반환하기 때문에 메서드 중간에 "s"가 들어갑니다.

요소 하나가 아닌, 컬렉션을 반환합니다!

초보자들이 자주 저지르는 실수가 하나 더 있습니다.

// 동작하지 않는 코드
document.getElementsByTagName('input').value = 5;

input 요소 전체를 담은 컬렉션에 5를 할당하는 위 코드는 동작하지 않습니다. 아마도 본래 의도는 컬렉션 내 요소에 값을 할당하는 것이었을 겁니다.

컬렉션을 순회하거나 인덱스를 사용해 요소를 얻고 그 요소에 값을 할당하면 기존 의도대로 동작합니다. 아래와 같이 말이죠.

// (문서에 input 요소가 있다면) 아래 코드는 잘 동작합니다.
document.getElementsByTagName('input')[0].value = 5;

아래는 클래스 속성의 값이 article인 요소를 검색해주는 예시입니다.

<form name="my-form">
  <div class="article">글</div>
  <div class="long article">내용이 긴 글</div>
</form>

<script>
  // name 속성을 이용해 검색
  let form = document.getElementsByName('my-form')[0];

  // form 내에서 클래스 이름을 이용해 검색
  let articles = form.getElementsByClassName('article');
  alert(articles.length); // 2. 클래스 속성값이 'article'인 요소는 2개입니다.
</script>

살아있는 컬렉션

'getElementsBy'로 시작하는 모든 메서드는 살아있는 컬렉션을 반환합니다. 문서에 변경이 있을 때마다 컬렉션이 '자동 갱신’되어 최신 상태를 유지합니다.

예시 내엔 스크립트 두 개가 있습니다.

  1. 첫 번째 스크립트는 <div>에 상응하는 요소를 담은 컬렉션에 대한 참조를 만듭니다. 스크립트가 실행되는 시점에 이 컬렉션의 길이는 1입니다.
  2. 두 번째 스크립트는 문서에 <div>가 하나 더 추가된 이후에 실행됩니다. 따라서 컬렉션의 길이는 2가 됩니다.
<div>첫 번째 div</div>

<script>
  let divs = document.getElementsByTagName('div');
  alert(divs.length); // 1
</script>

<div>두 번째 div</div>

<script>
  alert(divs.length); // 2
</script>

반면, querySelectorAll정적인 컬렉션을 반환합니다. 컬렉션이 한 번 확정되면 더는 늘어나지 않습니다.

querySelectorAll을 사용하면 두 스크립트가 동일하게 1을 출력합니다.

<div>첫 번째 div</div>

<script>
  let divs = document.querySelectorAll('div');
  alert(divs.length); // 1
</script>

<div>두 번째 div</div>

<script>
  alert(divs.length); // 1
</script>

예시를 통해 두 방식의 차이를 살펴보았습니다. 문서에 새로운 div가 추가되어도 querySelectorAll이 반환한 컬렉션은 이를 반영하지 못합니다.

요약

DOM에서 원하는 노드를 검색하게 해주는 주요 메서드 6가지는 다음과 같습니다.

메서드 검색 기준 호출 대상이 요소가 될 수 있는지에 대한 여부 컬렉션 갱신 여부
querySelector CSS 선택자 -
querySelectorAll CSS 선택자 -
getElementById id - -
getElementsByName name -
getElementsByTagName 태그나 '*'
getElementsByClassName class

아마 실무에선 querySelectorquerySelectorAll을 가장 많이 사용하실 겁니다. getElementBy로 시작하는 메서드는 대개 오래된 스크립트에서 만날 수 있는데, 일부 이 메서드가 꼭 필요한 상황에서 쓰이는 경우도 있습니다.

이 외에 알아두면 좋을 만한 메서드는 아래와 같습니다.

  • elem.matches(css)elem이 해당 CSS 선택자와 일치하는지 여부를 검사합니다.
  • elem.closest(css)는 해당 CSS 선택자와 일치하는 가장 가까운 조상 요소를 탐색합니다. 이때, elem 자기 자신도 검색 대상에 포함됩니다.

위에선 언급하지 않았지만, 노드의 부모-자식 관계를 확인할 수 있도록 도와주는 유용한 메서드 하나를 더 소개해 드리고 마무리하겠습니다.

  • elemA.contains(elemB)elemBelemA에 속하거나(elemBelemA의 후손인 경우) elemA==elemB일 때, 참을 반환합니다.

과제

중요도: 4

테이블과 폼이 있는 문서가 있다고 가정합시다.

아래 조건에 맞는 요소는 어떻게 찾을 수 있을까요?

  1. id="age-table"인 테이블
  2. 테이블 내의 label 요소 모두(총 3개)
  3. 테이블 내의 첫 번째 td(Age가 적힌 곳)
  4. name="search"form
  5. 폼의 첫 번째 input
  6. 폼의 마지막 input

별도의 창에서 table.html을 열어 브라우저 내 도구를 사용해 문제를 풀어보세요.

다양한 방법으로 문제를 풀 수 있습니다.

그 중 일부를 여기서 소개해드리겠습니다.

// 1. `id="age-table"`인 테이블
let table = document.getElementById('age-table')

// 2. 테이블 내의 `label` 요소 모두
table.getElementsByTagName('label')
// 또는
document.querySelectorAll('#age-table label')

// 3. 테이블 내의 첫 번째 `td`(Age가 적힌 곳)
table.rows[0].cells[0]
// 또는
table.getElementsByTagName('td')[0]
// 또는
table.querySelector('td')

// 4. `name="search"`인 `form`
// name이 "search"인 요소는 문서에 단 하나뿐이라고 가정합시다.
let form = document.getElementsByName('search')[0]
// from을 정확히 지정해 줄 수도 있습니다.
document.querySelector('form[name="search"]')

// 5. 폼의 첫 번째 `input`
form.getElementsByTagName('input')[0]
// 또는
form.querySelector('input')

// 6. 폼의 마지막 `input`
let inputs = form.querySelectorAll('input') // 모든 input 요소를 찾교
inputs[inputs.length-1] // 마지막 요소를 얻어냅니다.
튜토리얼 지도