jQuery / Selector / :has() - 특정 요소를 포함하는 요소를 선택하는 선택자
jQuery의 :has(selector)는 지정한 조건에 맞는 자손을 하나 이상 포함한 요소를 선택합니다. $("#menu > li:has(a)")는 링크를 가진 li를 반환하며, 안쪽 a를 반환하는 것은 아닙니다. 자손은 바로 아래 자식뿐 아니라 그 아래로 중첩된 요소까지 포함하므로, 직접 자식만 원하는 경우에는 조건을 구분해야 합니다.
포함하는 요소와 포함된 요소의 차이
:has() 선택자 문서의 핵심은 조건을 만족하는 자손이 있는 후보 요소를 남긴다는 점입니다. 링크가 span 안에 들어 있어도 li의 자손이므로 li:has(a)에 일치합니다. 반면 li 자체의 class 이름이 has-link인 것만으로는 a 요소를 포함한다는 조건을 만족하지 않습니다.
| 표현 | 검사하는 조건 | 반환 대상 |
|---|---|---|
| li:has(a) | 안쪽 어느 깊이에든 a가 있음 | 조건을 만족하는 li |
| li:has(> a) | a가 바로 아래 자식임 | 조건을 만족하는 li |
| li 선택 후 .has("a") | 선택한 li에 a 자손이 있음 | 기존 li 중 조건에 맞는 것 |
| li 선택 후 .find("a") | li 안에서 a를 찾음 | 안쪽 a 요소 |
>는 직접 자식 관계를 나타냅니다. 예제 바깥의 #menu > li는 검사할 항목의 범위를, :has() 안의 > a는 각 항목 내부에서 검사할 관계를 정합니다. 두 위치의 역할을 따로 읽으면 중첩 목록에서도 조건을 명확히 만들 수 있습니다.
링크가 있는 항목을 강조하는 전체 예제
아래 전체 코드를 UTF-8의 has-demo.html로 저장하고 브라우저에서 엽니다. 별도 설치는 필요하지 않으며, 공식 jQuery 배포 파일을 CDN(Content Delivery Network, 콘텐츠 전송 네트워크)에서 읽기 위한 인터넷 연결이 필요합니다. 예제는 4.0.0을 사용하며 3.7.1과 4.0.0으로 같은 동작을 실행해 확인했습니다.
plain은 링크가 없고, direct는 바로 아래에 a가 있으며, nested는 span 안에 a가 있습니다. self는 클래스 이름만 has-link이고 실제 링크는 없습니다. 버튼은 항목 선택과 문서 변경을 비교하도록 구성합니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Items that contain links</title>
<style>
body { font: 16px/1.6 sans-serif; margin: 24px; }
#menu { list-style: none; padding: 0; max-width: 600px; }
#menu > li { border: 2px solid #ddd; margin: 8px 0; padding: 12px; }
#menu > li.marked { border-color: #c45f00; background: #fff5e7; }
button { margin: 4px; padding: 8px; }
</style>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
</head>
<body>
<h1>Items that contain links</h1>
<ul id="menu">
<li id="plain">No link</li>
<li id="direct"><a id="link-1" href="#example">Direct link</a></li>
<li id="nested"><span><a id="link-2" href="#example">Nested link</a></span></li>
<li id="self" class="has-link">Class name only</li>
</ul>
<button type="button" id="descendants">Any descendant link</button>
<button type="button" id="children">Direct child link</button>
<button type="button" id="add">Add a link to plain</button>
<button type="button" id="reset">Reset</button>
<p id="result" aria-live="polite"></p>
<script>
$(function () {
function show($chosen) {
$("#menu > li").removeClass("marked");
$chosen.addClass("marked");
const ids = $chosen.map((_, item) => item.id).get().join(", ");
$("#result").text(`${ids || "none"} | ${$chosen.length}`);
}
function refresh() {
show($("#menu > li:has(a)"));
}
$("#descendants").on("click", refresh);
$("#children").on("click", () => show($("#menu > li:has(> a)")));
$("#add").on("click", function () {
if (!$("#added-link").length) {
$("#plain").append('<a id="added-link" href="#example"> Added link</a>');
}
refresh();
});
$("#reset").on("click", function () {
$("#added-link").remove();
refresh();
});
refresh();
});
</script>
</body>
</html>
처음 열면 direct와 nested 항목의 바깥 테두리가 강조되고 결과는 다음과 같습니다.
direct, nested | 2
직접 자식·동적 추가·초기화를 확인하기
- Any descendant link는 링크가 어느 깊이에든 있으면 선택하므로
direct, nested | 2를 표시합니다. - Direct child link는 바로 아래 a만 검사하므로
direct | 1을 표시합니다. nested의 링크는 span 아래여서 제외됩니다. - Add a link to plain을 누르면 plain 안에 링크가 추가되어
plain, direct, nested | 3이 됩니다. 같은 버튼을 다시 눌러도 추가 링크의 ID를 확인하여 중복 생성하지 않습니다. - Reset은 예제에서 추가한 링크만 제거하고 기본 자손 조건을 다시 적용합니다. 기존 No link 텍스트와 원래 항목은 남으며 처음의 두 항목 선택으로 돌아갑니다.
show는 이전 강조를 먼저 지우고 새 결과에만 클래스를 붙입니다. refresh는 현재 문서를 다시 검사합니다. 이를 통해 링크를 추가한 뒤에도 저장된 옛 선택 결과만 사용하거나, 조건을 바꾼 뒤 이전 항목의 강조가 남는 실수를 피합니다.
.has() 메서드와 .find()를 직접 비교하기
이미 li 목록을 선택했다면 .has() 메서드로 필요한 후보만 남길 수 있습니다. 예제를 처음 열거나 Reset을 누른 뒤 개발자 도구의 Console에서 다음 부분 코드를 실행합니다.
const ids = $set => $set.map((_, item) => item.id).get().join(", ");
console.log(ids($("#menu > li:has(a)")));
console.log(ids($("#menu > li").has("a")));
console.log(ids($("#menu > li").find("a")));
direct, nested
direct, nested
link-1, link-2
첫 두 결과는 바깥 li의 ID이고, .find()의 결과는 안쪽 a의 ID입니다. .has()는 부모를 새로 탐색해서 찾아 올라가는 동작이 아니라, 이미 선택한 집합을 자손 조건으로 줄이는 동작입니다. 후보 자신이 조건에 일치하는지만 검사하는 .filter()와도 다릅니다.
.has()에는 선택자 문자열뿐 아니라 특정 DOM(Document Object Model, 문서 객체 모델) 요소를 전달할 수도 있습니다. 그 경우에도 해당 요소를 자손으로 포함한 후보를 남깁니다. 후보 자신만 조건에 해당하고 자손에는 없거나, 일치하는 자손이 하나도 없으면 결과는 비어 있습니다.
선택 범위·문서 변경·CSS와의 관계
$(":has(a)")처럼 검사 후보를 문서 전체로 넓히면 링크를 포함하는 여러 조상까지 선택될 수 있습니다. 목적에 맞게 컨테이너와 태그를 먼저 제한합니다. 또한 선택 결과는 문서가 바뀔 때 자동 갱신되는 목록이 아니므로 자손을 추가·제거한 뒤 현재 상태를 반영하려면 선택을 다시 실행합니다.
Selectors Level 4의 CSS :has() 정의도 있으므로 현재의 :has() 표기를 무조건 jQuery에서만 쓸 수 있다고 설명하면 정확하지 않습니다. 이 글은 jQuery API로 요소 집합을 얻는 방법을 다룹니다. CSS 규칙으로 스타일만 바꾸는 방식과 비교할 때는 프로젝트의 지원 브라우저와 허용할 선택자 조건을 함께 확인합니다.
예제가 반응하지 않으면 Network에서 jQuery가 로드되었는지, Console에 오류가 있는지 확인합니다. 선택자의 괄호·따옴표, 실제 태그 종류와 중첩 깊이도 확인합니다. 화면에 보이는 ‘링크’라는 글자나 클래스 이름 대신 실제 a 요소의 구조를 검사해야 합니다.




