정적 마케팅 페이지와 문서 사이트는 여전히 손으로 작성한 HTML을 대량으로 배포합니다. 예전에는 자식 상태에 따라 부모 스타일을 바꾸려면 추가 JavaScript나 중복 래퍼 클래스가 필요했습니다. 관계 의사 클래스 :has()는 방향을 뒤집습니다. 안쪽 체크박스가 켜지면 섹션 테두리가 바뀌거나, 입력이 무효면 행이 강조되거나—번들러 없이, 종종 JS 한 줄 없이 구현됩니다. 2026년에는 대상 고객이 Safari 15.4 이상으로 충분히 기울어진 많은 팀이 시각적 단서에 :has()를 채택할 수 있습니다. 전제는 실제 WebKit에서 검증하고 선택자 복잡도를 존중하는 것입니다. 이 글은 실무 패턴, JS 및 컨테이너 쿼리와의 의사결정 표, Safari QA 워크플로에 검사를 넣는 방법, 임대 Mac mini에서의 실행을 다룹니다.
정적 페이지에서 :has()가 고치는 것
고전 CSS는 조상에서 자손으로는 스타일할 수 있었지만(.theme-dark .card) 반대 방향은 안 됐습니다. 제품 팀은 React로 클래스를 동기화하거나 data-*로 자식 상태를 거울처럼 복제했습니다. JS 없는 정적 사이트—법적 고지, API 마이크로사이트, 컨퍼런스 LP—에서는 그 마찰이 섬세한 UX를 막았습니다. :has()는 의도를 직접 씁니다.「이 카드는 .error-text를 포함하므로 오류 상태」입니다. 접근성 팀은 여전히 보이는 레이블과 적절한 ARIA를 기대합니다. :has()는 장식을 담당하고 의미를 대체하지 않습니다.
:has()는 선언적이라 디자이너가 CodePen에서 시도한 선택器를 Eleventy나 Hugo 산출물에 그대로 실을 수 있고 수화 단계가 없습니다. 대가는 정신적 부담입니다. 관계 선택자는 강력하고 과도하게 중첩하기 쉽습니다.「마케팅 템플릿 안에서 :has() 뒤 결합자는 두 개 이하」 같은 집 규칙을 두면 이후 유지보수가 grep 친화적입니다.
컴포넌트 라이브러리와 섞을 때 Portal 루트처럼 자주 붙였다 뗐다 하는 노드에 :has()를 묶지 마세요. 정적 페이지라도 긴 문서의 검색 오버레이가 DOM을 바꿀 수 있습니다. :has() 루트는 카드, 필드 그룹, 표 구역처럼 안정적인 컨테이너에 두세요. 다국어를 유지한다면 번역 길이로 DOM이 달라질 수 있으니 언어별로 한 번씩 테스트하세요.
오늘 붙여 넣을 수 있는 구문 패턴
자기 완결형 컴포넌트부터 시작합니다. 컨트롤 중 하나가 포커스 가시성을 가질 때 빛나는 필드 그룹:
.field-group:has(:focus-visible) {
box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.35);
}
셀에 번역 누락 토큰이 있을 때 행을 표시하는 표:
tr:has(td[data-missing="true"]) {
background: rgba(255, 59, 48, 0.08);
}
:not()와 조합해 배제 상태를 쓸 수 있지만, 순수 CSS의 빈 값 감지는 제약 검증 API보다 제한적입니다. 비즈니스 규칙이 속성 선택자를 넘으면 짧은 스크립트로 클래스 훅을 쓰는 편이 열 개짜리 :has() 체인을 강요하는 것보다 낫습니다.
다크 테마에서는 :has()를 color-scheme, prefers-reduced-motion과 병행할 수 있습니다. 예를 들어 자식에 비디오가 있고 사용자가 동작 줄이기를 원하면 부모 그림자를 약하게 합니다. 이런 패턴은 정적 HTML에서도 동작합니다. 미디어 쿼리와 :has()의 우선순위를 스타일시트에서 분명히 하고, 나중에 로드되는 테마 시트가 덮어쓰지 않게 하세요.
Safari 지원 타임라인과 테스트 메모
- Safari 15.4(2022년 3월)가 macOS 12.3, iOS/iPadOS 15.4에서 :has()를 출하했습니다. 다른 엔진의 Chromium 105급 지원과 리듬이 비슷합니다.
- Monterey 12.3 이전에 묶인 기업 정책이 있다면 분석상 매출 3~4%가 그 빌드에서 나온다면 :has() 없이도 의미가 통하는 테두리 색을 제공하세요.
- WebKit 수정이 Safari Technology Preview에 먼저 올 때가 있습니다. :has() 안의
nth-child관련 버그 리포트를 보면 안정판과 STP를 비교한 뒤 매 정적 배포마다 재테스트를 문서화하세요.
README에 Node·패키지 매니저 버전 옆에 최소 Safari를 적어 외주가 마감 주에 :has()를 통째로 지우는 일을 막습니다. 기업 플릿이 하드웨어를 갱신할 때마다 그 줄을 6개월마다 다시 보세요.
CDN으로 카나리를 한다면 구형 Safari에만 단순화 CSS를 주입하고 HTML을 두 벌 관리하지 않을 수도 있습니다. :has()는 점진 향상으로, 없을 때도 본문을 읽고 폼을 제출할 수 있게 먼저 보장하세요.
의사결정 표: :has(), JS, @container
| 필요 | 선호 | 이유 |
|---|---|---|
| 자식 무효 시 부모 강조 | :has(:invalid) | JS 없음, 오프라인 정적 HTML에서 동작. |
| 사이드바 폭이 바뀔 때 그리드 트랙 재배열 | @container | 폭 기반 레이아웃은 컨테이너 쿼리 영역. |
| 폼 JSON 전송 후 서버 오류 표시 | JavaScript | 네트워크와 ARIA 라이브 업데이트는 CSS 밖. |
| 체크박스가 하나라도 켜지면 아이콘 | :has(:checked) | 선언적. 레이블이 있으면 접근 가능. |
| 스크롤 깊이 분석 비콘 스로틀 | JavaScript | CSS만으로는 비콘을 안전하게 쏠 수 없음. |
:has()와 @container가 함께일 때는 역할을 나누세요. 컨테이너는 거시 레이아웃, :has()는 컴포넌트 트리 안 미시 상태입니다. 같은 요소에 둘을 과하게 겹치는 경우는 드뭅니다.
컨테이너 쿼리와 병용할 때는 다이어그램으로「누가 부모 박스 폭에 반응하고 누가 자식 상태에 반응하는지」를 먼저 정하면 리뷰가 빨라집니다.
성능과 선택자 위생
브라우저는 자손이 바뀔 때 관계 선택자를 다시 평가합니다. 4000+ 노드의 긴 단일 페이지에서도 body:has(.modal[open]) { overflow: hidden; }는 보통 괜찮지만, 여러 :has()를 고빈도 애니메이션에 묶지 마세요. WebKit은 국소 서브트리에 대한 스타일 무효화가 효율적이므로 :has() 루트를 카드와 폼 구역 가까이 두세요. 중급 노트북에서 hover 폭풍 중 보라색 스타일 재계산 막대가 2~3ms보다 계속 크면 래퍼 하나에 위임 리스너로 클래스를 토글하도록 리팩터링하세요.
Stylelint로 선택자 특이도를 제한하면「선택자 수프」를 막을 수 있습니다. 클라우드 Mac 러너에서 CI 단계를 돌리면 macOS에만 있는 퇴행을 머지 전에 잡습니다. 단기 캠페인용으로 노트북을 한 대 더 사는 것보다 Apple Silicon을 임대하는 편이 CapEx에 유리할 때가 많습니다.
보안 참고: :has()는 임의 텍스트 내용으로 매칭할 수 없고 구조와 의사 클래스만 다루므로 그 자체로 데이터 유출 경로는 아닙니다. 그래도 사용자 생성 클래스 이름이 들어 있는 전체 선택자 문자열을 서드파티 분석으로 보내지 마세요.
레거시 BEM 수정자 .card--error를 이전할 때는 두 스프린트 겹침을 계획하세요. 분석 훅을 위해 클래스를 유지하고 시각은 :has()가 담당하게 한 뒤, 이벤트 추적이 data 속성으로 옮겨지면 수정자를 삭제합니다. 정적 사이트는 중복 토글 스크립트를 없앤 뒤 gzip 기준 2~4KB를 줄이는 경우가 흔합니다.
인쇄 스타일에서 :has()를 쓰면 Safari 인쇄 미리보기를 별도로 통과하세요. PDF 보관만 하는 사용자에게 흰 배경에서 오류 테두리가 안 보이는 실수가 생기기 쉽습니다.
클라우드 Mac mini QA 체크리스트
Safari 안정판이 있는 macOS를 임대하고 file://나 로컬 정적 서버로 빌드를 엽니다. 키보드만으로 폼 탐색, 강제 무효 상태, 200% 확대에서 잘림을 확인하세요. 버그 배시에는 1280×720 녹화면 충분합니다. 로컬 Mac이 없는 팀은 SSH와 가끔 VNC를 다른 Safari 글과 같은 플레이북으로 씁니다.
Apple Silicon Mac mini는 야간에 스무 개 HTML 템플릿에 lint와 스타일 재계산 프로파일을 병렬로 돌려도 조용합니다. 클라우드 접속은 계약자들이 국경을 넘겨 노트북을 보내지 않고 한 대를 공유하게 합니다.
자동 스모크 한 단계: 페이지를 열고 샘플 입력을 유효/무효로 바꾼 뒤 :has() 루트에 대해 계산된 스타일 패널 스크린샷을 저장하세요. 릴리스 태그 옆에 PNG를 두는 비용은 거의 없지만 WebKit과 Chromium이 다를 때 25~35분의 회귀 논쟁을 줄입니다.
STP 대 안정판 워크플로와 함께 쓰면 변경 로그에「이번에 STP에서 재확인했는지」를 적어 프리뷰 전용 동작을 안정판 약속과 섞지 않도록 하세요.
FAQ
Safari는 어떤 버전부터 :has()를 지원하나요?
Safari 15.4(macOS 12.3, iOS 15.4, 2022년 3월)부터입니다. Safari 14를 지원하면 :has() 없는 대체가 필요합니다.
폼 JavaScript를 :has()로 대체할 수 있나요?
순수 시각 단서는 종종 가능합니다. 검증 메시지, ARIA 라이브, 서버 왕복에는 JS나 HTML 의미가 더 필요합니다.
:has()가 성능을 해치나요?
거대한 페이지의 깊은 체인은 재계산 비용을 늘릴 수 있습니다. 국소화하고 5k 노드 페이지의 모든 hover에 묶지 말고 Web Inspector에서 측정하세요.
절제 있게 쓰면 :has()는 정적 HTML에서 보일러플레이트를 줄이고 번들을 가늘게 유지합니다. Chromium뿐 아니라 실제 Safari 세션을 짝으로 두어 WebKit 특유의 무효화 이슈를 잡으세요. Apple Silicon Mac mini는 네이티브 WebKit, 낮은 대기 전력, 조용한 운영을 한 번에 제공합니다. MacHTML은 SSH/VNC로 물리 Mac mini를 임대해 릴리스 창에 WebKit 랩을 세웠다가 캠페인이 끝나면 축소할 수 있게 합니다—유휴 하드웨어에 대한 또 다른 CapEx 사이클 없이.
:has() QA에 Safari 실기가 필요하신가요?
Apple Silicon Mac mini를 임대해 실제 WebKit에서 Web Inspector를 돌리고, 지금 쓰는 편집기로 정적 HTML을 계속 작성하세요.