자식 요소의 체크 상태나 에러 유무에 따라 부모 요소의 배경색이나 레이아웃을 변경해야 하는 상황을 경험해봤을 것이다.
다만 이전에는 CSS만으로 부모 선택자를 지정할 방법이 없어 어쩔 수 없이 JavaScript 이벤트 리스너로 클래스를 붙였다 떼었다 하는 복잡한 방식을 사용하는 경우가 많았다.
이번에는 CSS :has() 의사 클래스가 정확히 무엇인지, 왜 필요한지, 그리고 JavaScript 없이 UI 상태를 관리하는 실무 활용법을 완벽하게 정리해서 소개하겠다.
이전의 CSS 선택자는 언제나 부모에서 자식 방향으로만 스타일을 내려주는 단방향 구조였다. 예를 들어 .card input:checked 구문은 .card 내부의 체크된 input 요소 자체를 선택할 뿐, input이 체크되었다고 해서 그 상위의 .card 상자 스타일을 바꿀 수는 없었다.
:has() 의사 클래스는 인자로 전달된 선택자가 내부(자식 또는 하위)에 존재하는지 판단하여 부모 요소를 선택하는 관계형 선택자다. 즉, 부모 선택자의 부재로 인해 스크립트에 의존했던 수많은 스타일 제어 로직을 CSS만으로 단순화할 수 있게 되었다.
실무에서는 부모-자식 관계뿐만 아니라 인접 형제 선택자(+)나 일반 형제 선택자(~)와 조합하여 다양한 조건부 스타일을 적용한다.
| 구분 | 작성 패턴 | 설명 |
|---|---|---|
| 기존 방식 (JS) | input.addEventListener('change', ...) | DOM 이벤트를 감지하여 부모 노드에 class를 수동으로 추가/제거 |
| 자식 탐색 | .card:has(img) | img 요소를 포함하고 있는 .card 부모 요소 선택 |
| 상태 감지 | .form-group:has(input:invalid) | 유효하지 않은 input을 품고 있는 폼 그룹 감지 |
| 인접 결합 | h2:has(+ p) | 바로 다음에 p 태그가 이어지는 h2 제목 요소만 선택 |
사용자가 특정 옵션을 체크했을 때 카드 전체의 배경색과 테두리를 변경하는 실무 UI 구현 예제다.
✗ 잘못된 방식 (JavaScript 스크립트로 부모 DOM 조작)
과거에는 자식 input의 상태 변화를 JS로 이벤트 감지하여 parentNode 또는 closest() 메서드로 부모 요소에 클래스를 토글했다. 불필요한 이벤트 리스너가 늘어나고 DOM 조작 코드가 가중되는 원인이 된다.
// ✗ 스크립트로 상태를 매번 동기화하는 방식
const checkboxes = document.querySelectorAll('.card-item input[type="checkbox"]');
checkboxes.forEach(input => {
input.addEventListener('change', (e) => {
const card = e.target.closest('.card-item');
if (e.target.checked) {
card.classList.add('selected');
} else {
card.classList.remove('selected');
}
});
});✓ 올바른 방식 (CSS :has() 선택자만 활용)
JavaScript 제어 코드를 완전히 제거하고 CSS만으로 부모 카드 요소의 스타일을 즉각 변환한다.
/* ✓ 내부 체크박스가 checked 상태인 .card-item 부모 요소를 직접 선택 */
.card-item {
border: 1px solid #e2e8f0;
padding: 16px;
border-radius: 8px;
transition: all 0.2s ease;
}
.card-item:has(input[type="checkbox"]:checked) {
border-color: #3b82f6;
background-color: #eff6ff;
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.1);
}출력 결과: 자식 체크박스를 클릭하는 즉시 스크립트 실행 없이 부모 .card-item 상자의 테두리가 파란색으로 바뀌고 배경색이 부드럽게 강조된다.
✗ 잘못된 사용 (범위가 지정되지 않은 루트 탐색)
body:has(.modal-open) 처럼 최상위 문서 노드에서 전체 DOM 트리를 순회하는 선택자는 브라우저 렌더링 리플로우(Reflow) 비용을 크게 높일 수 있다.
/* ✗ 전체 DOM 성능을 떨어뜨리는 광범위한 루트 검색 */
body:has(.danger-alert) {
background-color: #fee2e2;
}✓ 올바른 사용 (구체적인 부모 컴포넌트 범위 지정)
스타일 변경이 필요한 가깝고 명확한 부모 컨테이너 범위 내로 구체화하여 사용한다. 또한 미지원 구형 브라우저를 대비해 @supports 쿼리를 병행하는 것이 안전하다.
/* ✓ 특정 컴포넌트 영역으로 범위를 제한 */
.alert-container:has(.danger-alert) {
background-color: #fee2e2;
}
/* 브라우저 호환성 분기 */
@supports selector(:has(*)) {
.form-field:has(input:focus) {
outline: 2px solid #3b82f6;
}
}
CSS :has()는 프론트엔드 UI 개발에서 불필요한 JavaScript 상태 관리 코드를 획기적으로 줄여주는 강력한 도구다. 작은 선택자 최적화가 모여서 코드 유지보수성과 렌더링 성능에 큰 차이를 만든다는 점을 잊지 말자. 이 글의 실전 패턴을 참고해 기존의 DOM 클래스 토글 코드를 CSS로 전환하면, 훨씬 깔끔하고 직관적인 프론트엔드 코드베이스를 얻을 수 있을 것이다.