jQuery / 특정 요소를 자식 요소로 갖지 않는 요소 선택하는 방법
특정 요소를 자손으로 갖지 않는 부모를 선택하려면 부모 후보에서 .has(".badge")에 해당하는 집합을 .not()으로 제외합니다. 바로 아래의 직접 자식만 검사하려면 각 부모의 .children(".badge").length === 0을 비교합니다. ‘특정 자식이 없음’은 ‘내용이 완전히 비어 있음’과 다른 조건입니다.
없는 대상과 탐색 깊이 정하기
.has(".badge")는 일치하는 자손이 하나라도 있는 부모 후보를 남깁니다. 깊이가 한 단계로 제한되지 않으므로 안쪽 컨테이너의 badge도 찾습니다. 후보에서 이 집합을 .not()으로 빼면 깊이에 관계없이 badge가 없는 부모가 남습니다.
.children(".badge")는 직접 자식만 찾습니다. badge가 손자 위치에 있다면 ‘직접 badge 없음’ 조건은 통과하지만 ‘badge 자손 없음’은 통과하지 않습니다. 화면에서 숨겨도 요소가 DOM(Document Object Model)에 남아 있으면 존재하는 것으로 검사합니다.
이 글에서 부모는 검사 대상인 컨테이너를 뜻합니다. .parent()로 위로 이동하는 작업이 아닙니다. 후보를 먼저 제한해야 문서의 모든 조상이나 안쪽 요소까지 잘못 결과에 넣는 일을 피할 수 있습니다.
중첩·텍스트·숨김을 비교하는 전체 예제
전체 코드를 UTF-8의 missing-child-demo.html로 저장하고 브라우저로 엽니다. 인터넷으로 공식 jQuery 파일 4.0.0을 읽습니다. 서버나 설치는 필요하지 않습니다. Windows Edge에서 jQuery 3.7.1·4.0.0으로 초기 결과와 추가·이동·복원을 실행했습니다.
a는 직접 badge, b는 중첩된 badge, c는 다른 종류의 자식, d는 텍스트만 갖습니다. e는 빈 태그, f는 주석만, g는 공백 하나, h는 숨긴 badge입니다. 화면의 a: 같은 이름은 CSS 가상 요소로 표시해 빈 DOM 내용과 구분했습니다. 실습 HTML의 e·f·g 안쪽 공백을 임의로 바꾸면 :empty 결과도 달라질 수 있습니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Select containers without a badge</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; }
button { font: inherit; margin: 4px; }
.card { border: 2px solid #aaa; margin: 10px 0; padding: 10px; min-height: 28px; }
.card::before { content: attr(id) ": "; font-weight: bold; }
.card.chosen { border-color: teal; background: #e4f6f0; }
.badge { border: 1px solid #777; padding: 2px; }
.badge[hidden] { display: none; }
#result { white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<p>Green cards have no .badge descendant. Names are CSS-generated labels.</p>
<button type="button" id="add">Add badge to c</button>
<button type="button" id="move">Move b badge to direct child</button>
<button type="button" id="reset">Reset</button>
<div id="cards">
<div class="card" id="a">Direct <span class="badge">A</span></div>
<div class="card" id="b"><div class="wrapper">Nested <span class="badge">B</span></div></div>
<div class="card" id="c"><span class="note">Other element</span></div>
<div class="card" id="d">Text only</div>
<div class="card" id="e"></div>
<div class="card" id="f"><!-- comment only --></div>
<div class="card" id="g"> </div>
<div class="card" id="h"><span class="badge" hidden>Hidden</span></div>
</div>
<pre id="result" aria-live="polite"></pre>
<script>
$(function () {
const initial = $("#cards").html();
function ids($set) { return $set.map(function () { return this.id; }).get().join(",") || "(none)"; }
function report() {
const $cards = $("#cards > .card");
const $withoutDescendant = $cards.not($cards.has(".badge"));
const $withoutDirect = $cards.filter(function () { return $(this).children(".badge").length === 0; });
const $empty = $cards.filter(":empty");
const $withoutElements = $cards.filter(function () { return $(this).children().length === 0; });
$cards.removeClass("chosen"); $withoutDescendant.addClass("chosen");
$("#result").text([
"no descendant badge=" + ids($withoutDescendant),
"no direct badge=" + ids($withoutDirect),
"empty=" + ids($empty),
"no element children=" + ids($withoutElements)
].join("\n"));
}
$("#add").on("click", function () {
$("<span>").addClass("badge").text("New").appendTo("#c");
$(this).prop("disabled", true); report();
});
$("#move").on("click", function () {
$("#b .badge").appendTo("#b"); $(this).prop("disabled", true); report();
});
$("#reset").on("click", function () {
$("#cards").html(initial); $("#add, #move").prop("disabled", false); report();
});
report();
});
</script>
</body>
</html>
초기 결과입니다. 초록색 강조는 첫 줄인 ‘badge 자손 없음’ 집합에만 적용합니다. 다른 세 줄은 서로 다른 조건의 비교 결과입니다.
no descendant badge=c,d,e,f,g
no direct badge=b,c,d,e,f,g
empty=e,f
no element children=d,e,f,g
- 첫 줄 c,d,e,f,g는 badge가 어느 깊이에도 없습니다. c에는 note 요소가 있어도 badge는 없으므로 포함됩니다.
- 두 번째 줄은 b도 포함합니다. b의 badge는 wrapper 안쪽에 있어 직접 자식이 아니기 때문입니다.
- Add badge to c를 누르면 첫 줄은 d,e,f,g, 두 번째 줄은 b,d,e,f,g가 됩니다. c는 더 이상 ‘대상 없음’ 조건을 만족하지 않습니다.
- Move b badge to direct child를 누르면 두 번째 줄도 d,e,f,g가 됩니다. 원래 같은 badge를 이동했으며, 자손 존재 여부는 그대로이고 직접 관계만 달라집니다.
- Reset은 원래 여덟 컨테이너와 구조를 복원합니다. h의 숨긴 badge가 존재하는 것으로 검사되는지도 다시 확인합니다.
.not(".badge")와 :empty를 혼동하지 않기
Reset을 누른 뒤 개발자 도구 콘솔에서 아래 코드를 실행합니다. 첫 줄은 후보 자신이 badge인지 검사하고, 두 번째는 후보 안에 badge가 없는지 검사합니다.
const $cards = $("#cards > .card");
console.log($cards.not(".badge").map(function () { return this.id; }).get().join(","));
console.log($cards.filter(function () {
return $(this).find(".badge").length === 0;
}).map(function () { return this.id; }).get().join(","));
여덟 card 자신은 어느 것도 badge 클래스가 아니므로 첫 결과는 전부입니다. 두 번째는 각 컨테이너 내부를 검사하므로 실제 원하는 c,d,e,f,g입니다.
a,b,c,d,e,f,g,h
c,d,e,f,g
:empty는 특정 종류가 없는지를 뜻하지 않습니다. 자식 요소나 텍스트 노드가 없는 요소를 선택합니다. 예제에서는 e와 주석만 있는 f가 해당합니다. 주석은 이 판단을 막지 않지만 g의 공백 텍스트는 막습니다. d의 일반 텍스트도 있으므로 :empty가 아닙니다.
.children().length === 0은 직접 자식 ‘요소’가 없는 조건입니다. 텍스트와 공백은 세지 않으므로 d,e,f,g입니다. CSS ::before로 그린 이름도 DOM 자식이 아니어서 e·f의 :empty 여부를 바꾸지 않습니다. 화면에 무엇이 보이는지와 DOM에 어떤 노드가 있는지는 별도로 확인합니다.
동적 변경과 실제 적용 시 확인하기
조건에 맞는 집합을 변수에 저장해도 나중에 badge가 추가될 때 자동으로 갱신되지는 않습니다. 예제는 추가·이동 코드 끝에서 report()를 다시 실행하고 현재 후보를 조회합니다. 다른 코드에서 DOM을 바꾸는 화면이라면 그 변경이 끝난 지점에서도 재평가하도록 연결합니다.
.not()과 .filter()는 선택 집합을 만드는 메서드이며 일치하지 않는 card를 문서에서 지우지 않습니다. 빈 상태 안내를 표시하거나 클래스를 적용하려면 선택 결과에 별도 조작을 합니다. 이 예제는 이전 강조를 지운 뒤 현재 결과만 강조해, 과거 상태가 남지 않게 합니다.
실제 대상이 span인지 특정 클래스인지, 직접 자식인지 전체 자손인지, 비활성·숨김도 존재로 볼지 먼저 정합니다. 비슷해 보이는 선택자 하나로 바꾸기보다 초기 구조와 변경 후 구조를 모두 비교합니다. 깊은 문서를 반복 검색한다면 후보 범위를 좁혀 불필요한 탐색도 줄입니다.




