JavaScript / querySelectorAll()로 모든 요소 선택하고 반복 처리하기

document.querySelectorAll(selector)은 CSS 선택자와 일치하는 모든 요소를 문서 순서대로 담은 NodeList를 반환합니다. 결과가 없으면 빈 목록이며, 반환된 목록은 정적이어서 이후 요소가 추가되어도 자동으로 늘어나지 않습니다. 최신 요소까지 보려면 다시 조회해야 합니다.

일치하는 요소 목록 읽기

클래스 선택자 .item이나 구역을 제한한 #list .item을 전달합니다. 목록의 첫 항목은 [0], 두 번째는 [1]이며, length로 개수를 확인합니다. 결과가 없는 경우에도 null이 아니라 길이 0인 NodeList가 반환됩니다.

const items = document.querySelectorAll('#list .item');
console.log(items.length); // 일치한 요소 수
if (items.length > 1) {
  console.log(items[1].textContent); // 두 번째 항목
}

const missing = document.querySelectorAll('.missing');
console.log(missing.length); // 0

쉼표로 선택자를 구분하면 둘 중 하나에 일치하는 요소를 모을 수 있습니다. document.querySelectorAll(".notice, .warning")은 두 클래스에 해당하는 요소들을 하나의 목록에 담습니다. 잘못된 CSS 선택자 문자열은 빈 목록 대신 SyntaxError를 발생시킵니다. MDN의 querySelectorAll() 문서에서 탐색 순서와 오류 조건을 확인할 수 있습니다.

NodeList 순회와 배열의 차이

NodeList에는 forEach()가 있어 각 요소를 처리할 수 있습니다. 배열은 아니므로 map() 같은 배열 메서드가 필요하다면 Array.from(items)로 변환합니다. 한 요소만 필요하다면 이미 발행된 document.querySelector()로 첫 요소를 찾는 방법이 간단합니다.

const items = document.querySelectorAll('#list .item');
items.forEach(item => item.classList.add('selected'));

const labels = Array.from(items, item => item.textContent);
console.log(labels); // 각 항목의 텍스트 배열

반복 중 요소를 추가·삭제해야 한다면 목록이 정적인지 살아 있는지 먼저 확인하세요. querySelectorAll() 결과는 정적이므로 처음 목록을 순회하는 동안 새로 생긴 요소가 그 목록에 끼어들지 않습니다. MDN의 NodeList 설명에서 정적 목록과 살아 있는 목록의 차이를 비교할 수 있습니다.

문서가 바뀌면 다시 조회해야 합니다

처음 얻은 목록은 DOM이 바뀌어도 길이가 고정됩니다. 다음 코드는 li를 추가한 뒤 새로 조회해야 새 요소까지 포함한다는 뜻입니다. getElementsByTagName()처럼 살아 있는 HTMLCollection과의 차이를 혼동하지 마세요.

const list = document.querySelector('#list');
const before = document.querySelectorAll('#list .item');
const item = document.createElement('li');
item.className = 'item';
list.append(item);

console.log(before.length); // 추가 전 개수 그대로
console.log(document.querySelectorAll('#list .item').length); // 1 증가

이 코드는 #list 요소가 있는 페이지에서 실행합니다. 실제 페이지에서는 list가 null이 아닌지 확인한 뒤 요소를 추가하세요.

전체 예제: 정적 목록과 새 조회 비교

아래 코드를 UTF-8로 query-selector-all-example.html에 저장해 브라우저에서 엽니다. ‘항목 추가’를 누른 뒤 처음 목록과 새로 조회한 목록의 길이를 비교하고, ‘현재 항목 강조’를 눌러 지금 있는 모든 항목에 순회 처리를 적용합니다.

<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>querySelectorAll 결과 비교</title>
  <style>.selected { background: #fff2a8; }</style>
</head>
<body>
<h1>모든 일치 요소 찾기</h1>
<ul id="list">
  <li class="item">첫 항목</li>
  <li class="item">둘째 항목</li>
</ul>
<button id="add" type="button">항목 추가</button>
<button id="mark" type="button">현재 항목 강조</button>
<button id="reset" type="button">처음으로</button>
<pre id="output" aria-live="polite"></pre>
<script>
const list = document.querySelector('#list');
const initial = document.querySelectorAll('#list .item');
const output = document.querySelector('#output');

function show() {
  const current = document.querySelectorAll('#list .item');
  const selected = document.querySelectorAll('#list .selected');
  output.textContent = `처음 조회: ${initial.length}\n새로 조회: ${current.length}\n강조된 항목: ${selected.length}`;
}

document.querySelector('#add').addEventListener('click', () => {
  const item = document.createElement('li');
  item.className = 'item added';
  item.textContent = `추가 항목 ${list.children.length + 1}`;
  list.append(item);
  show();
});
document.querySelector('#mark').addEventListener('click', () => {
  document.querySelectorAll('#list .item').forEach(item => {
    item.classList.add('selected');
  });
  show();
});
document.querySelector('#reset').addEventListener('click', () => {
  list.querySelectorAll('.added').forEach(item => item.remove());
  list.querySelectorAll('.selected').forEach(item => item.classList.remove('selected'));
  show();
});
show();
</script>
</body>
</html>

처음에는 두 길이가 모두 2입니다. 항목을 하나 추가하면 처음 조회는 2, 새 조회는 3이 됩니다. ‘현재 항목 강조’를 누르면 세 항목 모두 강조됩니다. ‘처음으로’는 추가 항목과 강조를 제거해 두 길이 2, 강조 0으로 되돌립니다. 정적 화면 캡처를 보는 대신 목록이 바뀌기 전후 값을 직접 확인할 수 있습니다.