jQuery / 자식 요소의 개수를 기준으로 부모 요소 선택하기

자식 요소의 개수로 부모를 선택하려면 부모 후보에 .filter(function () { ... })를 적용하고, 각 부모 안에서 $(this).children(".item").length를 비교합니다. 정확히 2개면 === 2, 2개 이상이면 >= 2를 사용합니다. 중첩된 모든 자손을 세려는 경우에만 .children() 대신 .find()를 사용합니다.

부모마다 무엇을 셀지 정하기

.filter()는 기존 선택 집합 중 조건을 통과한 요소를 남깁니다. 콜백의 this는 검사 중인 부모 DOM 요소이며, true를 반환하면 그 부모가 결과에 포함됩니다. 따라서 결과는 자식이 아니라 조건에 맞는 부모 집합입니다. 원래 문서에서 다른 부모를 삭제하는 기능은 아닙니다.

.children(".item")은 바로 아래 한 단계에 있는 item 클래스 요소만 셉니다. .children()에 선택자를 생략하면 제목·컨테이너 등 모든 직접 자식 요소가 대상이 됩니다. 텍스트와 주석은 요소가 아니므로 어느 쪽에서도 세지 않습니다.

.find(".item")는 더 깊이 중첩된 자손까지 찾습니다. 예제 Beta는 직접 item이 1개지만 그 안에도 item이 2개 있어 자손 기준으로는 3개입니다. 숨긴 요소도 DOM에 존재하면 개수에 포함됩니다. 보이는 항목만 필요하다면 그 조건을 별도로 설계해야 합니다.

정확한 수와 최솟값을 비교하는 전체 예제

전체 코드를 UTF-8의 child-count-demo.html로 저장하고 브라우저로 엽니다. 인터넷으로 공식 jQuery 파일 4.0.0을 읽습니다. 별도 서버나 설치는 필요하지 않습니다. Windows Edge에서 jQuery 3.7.1·4.0.0으로 각 조건과 초기화, 동적 추가를 실행했습니다.

직접 item 수는 Alpha 2, Beta 1, Gamma 0, Delta 3입니다. Delta의 D3는 화면에서 숨겼어도 셉니다. 제목과 일반 텍스트는 .item이 아니므로 제외합니다. 후보도 #board 바로 아래 .group으로 한정해 안쪽 요소를 부모 후보에 섞지 않습니다.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Filter parents by child count</title>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <style>
    body { font: 16px/1.6 sans-serif; margin: 20px; max-width: 760px; }
    label { display: block; margin: 8px 0; }
    select, input, button { font: inherit; max-width: 100%; }
    .group { border: 2px solid #aaa; padding: 12px; margin: 12px 0; }
    .group.chosen { border-color: teal; background: #e5f5f1; }
    .item { display: inline-block; padding: 4px; border: 1px solid #777; margin: 3px; }
    .item[hidden] { display: none; }
    .group h2 { font-size: 20px; margin: 0; }
    #result { white-space: pre-wrap; overflow-wrap: anywhere; }
  </style>
</head>
<body>
  <label>Count scope <select id="scope"><option value="direct">Direct children</option><option value="descendant">All descendants</option></select></label>
  <label>Rule <select id="rule"><option value="exact">Exactly</option><option value="minimum">At least</option></select></label>
  <label>Required count <input id="count" type="number" min="0" step="1" value="2"></label>
  <button type="button" id="add">Add direct item to Alpha</button>
  <button type="button" id="reset">Reset</button>
  <div id="board">
    <section class="group" id="alpha"><h2>Alpha</h2><span class="item">A1</span><span class="item">A2</span> plain text <!-- comment --></section>
    <section class="group" id="beta"><h2>Beta</h2><div class="item">B1 <span class="item">B2</span><span class="item">B3</span></div></section>
    <section class="group" id="gamma"><h2>Gamma</h2>Text without an item</section>
    <section class="group" id="delta"><h2>Delta</h2><span class="item">D1</span><span class="item">D2</span><span class="item" hidden>D3 hidden</span></section>
  </div>
  <pre id="result" aria-live="polite"></pre>
  <script>
    $(function () {
      const initial = $("#board").html();
      let added = 0;
      function apply() {
        const $groups = $("#board > .group");
        const scope = $("#scope").val(), rule = $("#rule").val();
        const field = document.getElementById("count"), target = Number(field.value);
        $groups.removeClass("chosen");
        if (field.value === "" || !field.checkValidity() || !Number.isInteger(target) || target < 0) {
          $("#result").text("Enter a nonnegative integer."); return;
        }
        function count(group) {
          return scope === "direct" ? $(group).children(".item").length : $(group).find(".item").length;
        }
        const $selected = $groups.filter(function () {
          const n = count(this);
          return rule === "exact" ? n === target : n >= target;
        });
        $selected.addClass("chosen");
        const ids = $selected.map(function () { return this.id; }).get();
        const counts = $groups.map(function () { return this.id + "=" + count(this); }).get();
        $("#result").text(`scope=${scope}; rule=${rule}; count=${target}\nselected=${ids.join(",") || "(none)"}\n${counts.join("; ")}`);
      }
      $("#scope, #rule, #count").on("change input", apply);
      $("#add").on("click", function () {
        added++; $("<span>").addClass("item").text("Added " + added).appendTo("#alpha"); apply();
      });
      $("#reset").on("click", function () {
        $("#board").html(initial); added = 0;
        $("#scope").val("direct"); $("#rule").val("exact"); $("#count").val(2); apply();
      });
      apply();
    });
  </script>
</body>
</html>

처음에는 직접 자식이 정확히 2개인 Alpha만 강조합니다.

scope=direct; rule=exact; count=2
selected=alpha
alpha=2; beta=1; gamma=0; delta=3
  1. Rule을 At least로 바꾸면 Alpha와 Delta를 선택합니다. 같은 2라는 수라도 정확히 2개와 2개 이상의 결과가 다릅니다.
  2. Count scope을 All descendants로 바꾸면 Beta도 포함됩니다. Beta의 수가 1에서 3으로 달라집니다.
  3. Exactly와 Required count 3을 고르면 자손 기준 Beta·Delta입니다. Direct children으로 되돌리면 Delta만 남습니다.
  4. Exactly 0은 Gamma입니다. At least 0은 0개도 허용하므로 후보 네 개 전부입니다.
  5. Reset 후 Add direct item to Alpha를 누르면 Alpha가 3개가 되어 정확히 2개 결과는 (none)입니다. 부모가 사라진 것이 아니라 강조 대상이 없어진 것입니다.
  6. 빈 값·음수·소수에는 오류 문구를 표시하고 강조를 지웁니다. Reset은 새 항목을 지우고 원래 구조와 조건을 복원합니다.

전체 합계와 부모별 조건을 구분하기

아래 코드를 실행하기 전 예제의 Reset을 누릅니다. 개발자 도구 콘솔에서 실행하면 부모 전체의 자식 집합과 부모별 필터의 차이를 볼 수 있습니다.

const $groups = $("#board > .group");
console.log($groups.children(".item").length);
console.log($groups.filter(function () {
  return $(this).children(".item").length === 0;
}).map(function () { return this.id; }).get().join(","));

첫 줄의 6은 Alpha 2+Beta 1+Gamma 0+Delta 3을 한 집합으로 모은 결과입니다. 어느 부모가 2개인지 알려 주는 값은 아닙니다. 두 번째 줄은 각 부모를 검사해 직접 item이 없는 Gamma를 반환합니다.

6
gamma

일반 function을 쓴 이유는 this로 현재 부모를 받기 위해서입니다. 같은 코드에서 화살표 함수로만 바꾸면 this가 부모를 가리킨다는 전제가 깨집니다. 화살표 함수를 쓰려면 콜백의 두 번째 인자인 요소를 받아 그 요소를 명시적으로 검사합니다.

선택한 결과를 변수에 보관했다고 조건이 계속 재평가되지는 않습니다. Alpha에 항목을 추가한 뒤에도 과거 결과 집합에는 Alpha가 남을 수 있습니다. 예제는 추가 작업 직후 apply()를 다시 호출하고 새 부모 후보·개수를 조회합니다. .filter()가 DOM 변경을 자동으로 감시하지 않는다는 점에 주의합니다.

적용 기준과 실수하기 쉬운 조건

‘자식’이 모든 요소인지 특정 카드·li·입력 같은 대상인지 먼저 정합니다. 여기서는 .item만 계산하므로 Gamma는 h2와 텍스트가 있어도 0개입니다. 이것은 문서가 완전히 비었다는 의미가 아닙니다. 화면 감춤·비활성·데이터 상태까지 조건에 필요하면 그 기준을 명시해 검사합니다.

특정 자손이 하나라도 존재하는지만 필요하다면 .has()처럼 존재 여부를 나타내는 방법이 더 간단할 수 있습니다. 정확한 개수나 최솟값 비교는 예제처럼 숫자 조건으로 표현하면 의미가 분명합니다. 형제 위치를 나타내는 nth-child를 부모의 전체 개수와 혼동하지 않습니다.

부모가 수천 개이고 깊은 탐색을 반복한다면 후보 범위를 좁히고 같은 개수를 불필요하게 재계산하지 않도록 조정합니다. 반대로 너무 일찍 계산한 결과를 계속 재사용해 최신 구조를 놓치지 않게 합니다. 초기 HTML과 동적 변경 뒤 상태를 모두 확인해야 합니다.