[Javascript] 쿼리선택자의 종류와 차이점 - getElementById, getElementsByClassName, getElementsByTagName, querySelector, querySelectorAll

DOM에서 HTML 요소를 선택하는 방법은 5가지가 있습니다.

getElement로 시작하는 getElementById(), getElementsByClassName(), getElementsByTagName() 3개의 메서드는 HTML 요소(Element), 또는 요소들을 반환합니다.

getElementByClassName()과 getElementsByTagName()의 Elements에 주의해야 합니다.

요소를 선택하지만 "s"가 붙어 있습니다. 그러니까 선택 가능한 모든 요소들을 선택하는 것이고, 복수 개의 요소가 반환됩니다. 그래서 선택되어 반환되는 데이터 타입은 HTML Element가 아니라 HTMLCollection이 됩니다.

이 차이점은 선택한 요소가 있는지 조건 체크를 할 때 중요한 차이점으로 작용하므로 주의해야 합니다.

querySelector로 시작하는 querySelector(), querySelectorAll() 2개의 메서드는 노드(Node), 또는 노드들을 반환합니다.

노드에는 HTML 태그 요소(Element)를 포함해 텍스트, 코멘트 등 DOM을 구성하는 다양한 객체들이 포함됩니다.

쿼리 선택자의 쿼리 문을 사용해 요소만을 가져올 수 있기 때문에 여기서는 HTML 요소를 가져오는 것을 기준으로 다룹니다.

주요 노드들은 다음과 같습니다.

  • Element Node
  • Text Node
  • CData Section Node
  • Processing Instruction Node
  • Comment Node
  • Document Node
  • Document Type Node
  • Document Fragment Node

HTML 요소를 선택하는 방법 중에는 getElementByName(), getElementByTagNameNS() 도 있지만, 거의 사용되고 있지 않습니다. 이런게 있다 정도만 알고 넘어가면 됩니다.

1. getElementById와 getElementsByClassName, getElementsByTagName

앞서 설명했지만 메서드의 Element(s) 가 복수형인지 단수형인지를 주의해야 합니다.

getElementById는 ID로 검색을 하며, 언제나 1개의 요소만을 반환합니다. 웹 페이지 안에서 ID는 유일해야 하기 때문에 한 개의 요소만을 반환할 수 있습니다.

웹 페이지에서 중복된 ID는 가장 앞에 나오는 ID 한 개만 인식됩니다. 다르게 말하면, ID가 중복된 뒤쪽의 요소는 getElementById()로 선택할 수 없습니다.

웹 페이지를 파싱해서 DOM을 생성할 때, ID는 별도의 리스트로 등록해서 관리하며, ID 등록 전 먼저 등록된 ID가 있는지를 항상 확인합니다.

모든 쿼리 선택자 메서드를 통틀어서 요소를 선택하는 속도가 가장 빠릅니다. ID로 단일 HTML 요소를 선택할 수 있으면 getElementById()를 사용하는 것이 좋습니다.

getElement* 메서드들은 메서드 체인을 지원합니다.

따라서 다음과 같은 방식으로 선택하는 HTML 요소의 범위를 제한할 수 있습니다.

document.getElementById('sidebar').getElementsByClassName('box_aside')

메서드 체인을 사용하면 원하는 요소를 선택할 때 더 빠른 결과를 얻을 수 있습니다.

앞선 ID 선택자에서 뒤쪽의 클래스를 검색할 범위를 한정했기 때문에 전체 DOM 트리를 검색할 필요 없이 "#sidebar" 하위의 ".box_aside"만 검색하면 됩니다.

getElementsByClassName()은 여러 클래스 조건을 모두 만족하는 요소를 선택할 수 있도록 여러 개의 이름을 한꺼번에 지정할 수 있습니다.

여러 개의 클래스를 공백으로 띄워서 나열하면 모든 클래스를 가진 요소, 즉 "클래스1 and 클래스2"의 조건으로 요소를 검색합니다. 따옴표(큰따옴표) 안에 공백으로 띄워서 클래스 이름을 나열해야 합니다.

다음 클래스 이름 선택자는 "box_aside"와 "list" 클래스 2개를 모두 가진 요소를 선택합니다.

document.getElementsByClassName('box_aside list')

태그 이름으로 선택하는 getElementsByTagName()에는 적용되지 않으므로 주의해야 합니다.

getElements* 로 선택한 복수 개의 요소들은 HTMLCollection으로 반환됩니다. 이 컬렉션에서 HTML 요소들을 담고 있으며, 인덱스로 개별 요소에 다음과 같이 접근할 수 있습니다.

document.getElementsByClassName('box_aside list')[0]
document.getElementById('sidebar').getElementsByClassName('box_aside')[0]

여러 개의 클래스 중 1개 이상의 클래스를 가지고 있는 요소들을 모두 선택하려면 클래스를 쉼표로 구분해서 개별 클래스들을 나열해야 합니다. 여러 종류의 태그를 선택(getElementsByTagName())할 때도 동일한 방법으로 여러 태그를 인자 형태로 나열해서 모두 선택할 수 있습니다.

document.getElementsByClassName('box_aside','list')
document.getElementsByTagName('div','li')

2. querySelector와 querySelectorAll

CSS 쿼리 형태의 선택자를 통해 원하는 노드를 선택하는 메서드입니다.

querySelector()는 1개의 노드를 querySelectorAll()은 해당되는 모든 노드를 선택해서 노드리스트를 반환합니다.

ID, 클래스, 태그 이름에 따라 따로 선택자 메서드를 사용해야 했던 앞서와 달리, 3가지를 모두 섞어서 CSS 쿼리 자체를 사용할 수 있는 장점이 있습니다.

앞서 대상에 따라 종류별로 따로따로 메서드를 사용해서 메서드 체인으로 연결한 선택자를 1개의 querySelector()로 줄여보겠습니다.

document.getElementById('sidebar').getElementsByClassName('box_aside')[0]
document.querySelector('#sidebar .box_aside:first-child')

예제에서는 짧은 쿼리문을 작성해서 별로 차이가 나지 않지만, 복잡한 CSS 쿼리를 작성하게 되면 효율 차이가 많이 나게 됩니다.

querySelector() 선택자로 선택을 할 때 선택되는 대상이 여러 개인 경우 첫 번째 노드가 반환됩니다.

CSS 쿼리를 감싸는 따옴표(또는 큰따옴표)는 선택사항입니다. 그리고 CSS 쿼리 안에 문자열이 있어서 따옴표, 또는 큰따옴표로 감싸서 구분해야 할 경우 다음처럼 중복되지 않도록 변경할 수 있습니다.

document.querySelector("#loginform .div.fields input[name='userid']");
document.querySelector('#loginform .div.fields input[name="userid"]');

!주의사항

CSS 쿼리문 안에 가상 요소(pseudo-element)가 포함되어 있으면 항상 빈 노드리스트를 반환합니다. 쿼리 선택자에 CSS 쿼리를 사용할 때는 가상 요소는 포함하면 안 됩니다.

document.querySelectorAll('.box_aside::before') // length 0인 노드리스트가 반환됨

3. 쿼리 선택자 반환 값 체크

쿼리 선택자를 사용해서 요소(들)를 선택할 때, 선택한 요소(들)가 없는데 요소의 속성에 접근을 하면 에러가 납니다.

따라서 에러가 발생하지 않도록 요소가 선택되었는지 먼저 확인한 후 요소의 속성에 접근해야 합니다.

쿼리 선택자는 반환하는 요소의 개수를 기준으로 2가지로 구분합니다.

getElementById()와 querySelector()는 1개의 요소, 또는 노드를 반환합니다.

getElementByClassName(), getElementByTagName(), querySelectorAll() 은 컬렉션, 또는 노드리스트 즉 목록 형태의 결괏값을 반환합니다.

1개의 요소를 반환하는 쿼리 선택자는 다음과 같이 값의 반환 여부를 확인합니다. 쿼리 선택자로 선택된 요소가 없는지를 확인하려면 null과 비교해야 합니다.

const el = document.querySelector('.box_aside');
if(el){
    console.log(el.tagName)
}

if(el == null){ // 선택된 요소가 없으면 null과 비교
    console.log('Element not selected!')
}

목록 형태의 결과를 반환하는 쿼리 선택자는 다음과 같이 비교합니다. HTML컬렉션, 또는 노드리스트를 반환하는 쿼리선택자는 선택된 요소가 없어도 length 속성을 가지고 있습니다.

따라서 length 속성 값이 0인지 여부로 선택된 노드, 또는 요소가 있는지를 판단할 수 있습니다.

const nodelist = document.querySelectorAll('.box_aside')
if(nodelist){ // nodelist.length > 0 도 가능
    console.log(nodelist.length)
}

if(nodelist.length == 0){ // 선택된 요소가 없으면 길이 속성이 0인지 확인
    console.log('Elements not selected!')
}

단순하게 반환된 요소의 속성에만 접근하는 게 목적이면 조건절 체크를 다음과 같이 단순하게 단축할 수 있습니다.

요소의 존재를 확인하는 ? 연산자를 요소 바로 뒤에 붙여서 조건 체크를 대신할 수 있습니다.

const nodelist = document.querySelectorAll('.box_aside')

if(nodelist){// nodelist.length > 0 도 가능
    console.log(nodelist[0]?.tagName)
}

console.log(document.querySelector('.box_aside')?.tagName)

단, 목록 형태인 경우 nodelist?[0].tagName 처럼 인덱스 앞에 객체 확인을 하는 방식은 사용할 수 없습니다.

4. 라이브 HTML컬렉션과 정적 노드리스트

!중요합니다.

HTML 규격에서 라이브(live)라는 용어로 정의를 하기 때문에 "라이브"로 사용합니다. 

개념적으로는 동적(dynamic)<->정적(static)으로 구분하는 것이 조금 더 적당합니다.

앞서 선택할 요소의 속성 종류별로 구분을 해서 대상 요소를 선택하는 선택자들(getElementById(), getElementsByClassName(), getElementsByTagName())은 HTML 요소, 또는 HTML컬렉션을 반환한다고 배웠습니다.

그리고, CSS 쿼리문으로 노드(들)를 선택하는 쿼리 선택자들은 노드, 또는 노드리스트를 반환한다고 배웠습니다.

HTML DOM에서 선택하려는 대상이 HTML 요소라면 둘은 별다른 차이가 없어 보입니다.

하지만 이 두 가지 방식은 아주 큰 차이점이 있습니다.

반환하는 객체가 HTML 요소, 또는 HTML 컬렉션이 되는 getElement* 메서드는 라이브(live) 객체를 반환합니다.

라이브 객체는 DOM의 원본 데이터에 변경이 생기면 getElement* 반환받은 요소(들)에도 동시에 변경이 반영됩니다.

getElement*로 선택한 요소(들)는 DOM의 원본 요소에 변화가 생기는지를 모니터링한다고 생각하면 됩니다.

이에 반해 노드, 또는 노드리스트(NodeList)를 반환하는 querySelector* 메서드가 반환하는 객체는 정적(static) 객체입니다.

DOM의 원본에 변경이 생겨도 정적 노드리스트는 DOM 트리의 변경을 모니터링하지 않기 때문에 DOM의 변경이 반영되지 않습니다.

다음 HTML 목록에서 라이브 HTML컬렉션과 정적 노드리스트가 어떻게 달라지는지 확인해 보겠습니다.

<ul id="recent" class="recent">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
</ul>

자바스크립트로 정적 HTML 컬렉션(elements)과 정적 노드리스트(nodes)를 가져와서 변수에 저장합니다.

그리고 새 목록 요소를 HTML DOM에 추가한 후 앞서 결과를 저장한 변수의 길이 변화를 다시 확인합니다.

HTML 컬렉션과 다시 결과를 얻는 정적 노드리스트는 길이가 5로 변경되었지만, nodes 변수에 저장했었던 정적 노드리스트의 길이는 4로 그대로 유지가 됩니다.

let elements = document.getElementsByTagName('li')
console.log(elements.length) // 4
nodes = document.querySelectorAll('li')
console.log(nodes.length) // 4

//새 HTML 요소를 DOM에 추가
let newli = document.createElement('li');
newli.textContent = '5';
document.querySelector('.recent').append(newli);

console.log(nodes.length) // 4
console.log(document.querySelectorAll('li')) // 5
console.log(elements.length) // 5

!주의할 점이 있습니다.

DOM 트리의 변화가 반영이 안 된다고 개별 요소의 내용이 반영이 안 되는 것은 아닙니다. DOM 트리의 구조가 변경되는 것에 대한 차이가 있지 노드리스트의 개별 요소 내용에 변경이 생기는 것은 정적 노드리스트도 동일하게 반영됩니다.

앞서 처음의 HTML 내용에서 다음처럼 정적 노드리스트로 처음 요소의 내용을 변경해 보겠습니다.

노드리스트의 첫 번째 요소의 innerText를 변경하면, 원본 DOM과 앞서 선택한 HTML 컬렉션에 모두 반영된 내용이 표시됩니다.

let elements = document.getElementsByTagName('li')
console.log(elements[0].innerText) // 1
nodes = document.querySelectorAll('li')
console.log(nodes[0].innerText) // 1

nodes[0].innerText = "5"
console.log(document.querySelectorAll('li')[0].innerText) // 5
console.log(elements[0].innerText) // 5

5. 쿼리선택자 속도 비교.

속도는 getElementById > getElementsByClassName > querySelector > querySelectorAll 순으로 빠릅니다.

ID로 선택할 수 있는 요소는 getElementById로 선택하면 가장 빠른 결과를 얻을 수 있습니다.

ID는 처음 HTML 문서를 파싱 할 때 별도의 리스트를 생성해 따로 관리하기 때문에 DOM 트리를 검색하는 과정보다 속도가 월등하게 빠릅니다. 그래서 HTML 문서를 작성할 때는 주요 요소들에 ID로 선택할 수 있도록 ID를 부여하는 것이 중요합니다.

단일 클래스 이름으로 요소를 선택하고 싶으면 querySelectorAll보다는 getElementByClassName을 사용하는 것이 좋습니다.

querySelector()와 querySelectorAll()은 CSS 쿼리문을 파싱 하는 전처리 과정이 필요합니다. 전처리 과정을 통해 검색할 대상을 선택하고, 효율적인 검색 방법을 결정하게 됩니다.

따라서 동일한 클래스로 선택한다고 해도 getElementsByClassName()이 약간은 더 빠른 결과를 반환합니다.

querySelectorAll()은 가장 강력하고, 모든 선택을 가능하게 해주는 만능 툴이지만 가장 느립니다.