본문으로 건너뛰기
개발 머꼬
개발 노트CSS
hohyeon.dev18

부모 요소에 클래스를 붙이려고 JavaScript를 쓰던 코드를 :has()로 지운 과정

  • #CSS
  • #Engineering Note
  • #Selector

문제 발생

이미지가 있는 카드만 레이아웃을 다르게 하고 싶었는데, CSS로는 "자식에 img가 있는 .card"를 고를 수 없어서 렌더링 시점에 클래스를 계산해 붙이고 있었습니다.

// 카드마다 자식을 확인해서 클래스를 붙인다
document.querySelectorAll(".card").forEach((card) => {
  if (card.querySelector("img")) card.classList.add("card--with-image");
});

서버 렌더링과 클라이언트 렌더링에서 결과가 어긋나면 깜빡임이 생겼고, 조건이 늘 때마다 이 스크립트가 길어졌습니다.

원인 분석

CSS 선택자는 오른쪽 끝 요소를 대상으로 삼고 왼쪽으로 조건을 붙이는 구조라, .card img이미지를 고를 뿐 카드를 고르지 못했습니다. "부모 선택자"는 오래된 숙원이었고, 그래서 다들 JavaScript로 우회했습니다.

:has()는 이 방향을 뒤집습니다. 인자로 준 상대 선택자에 일치하는 요소가 하나라도 있으면 그 요소가 선택됩니다. 2023년 12월부터 모든 최신 브라우저에서 사용 가능(Baseline)합니다.

해결 방안

  1. 자식 존재 여부로 부모를 고릅니다. 위의 스크립트가 통째로 사라집니다.
.card:has(img) {
  grid-template-columns: 200px 1fr;
}
  1. 이전 형제도 고를 수 있습니다. 결합자를 인자 안에 넣으면 "뒤에 무엇이 오는가"로 앞 요소를 고를 수 있습니다.
/* 바로 뒤에 p가 오는 h1의 아래 여백만 없앤다 */
h1:has(+ p) { margin-bottom: 0; }
  1. 폼 상태를 부모로 끌어올립니다. 유효성 표시를 위해 상태 클래스를 관리하던 코드가 대부분 여기서 정리됩니다.
.field:has(input:invalid) { border-color: var(--danger); }
.field:has(input:focus-visible) { outline: 2px solid var(--accent); }
  1. 특이도를 계산에 넣습니다. :has():is(), :not()과 같이 인자 중 가장 특이도가 높은 선택자의 특이도를 가져갑니다. .card:has(#hero)는 id 특이도를 갖게 되므로, 나중에 .card로 덮어쓰려다 실패할 수 있습니다.
  2. 두 가지 제약을 기억합니다.
    • :has() 안에 :has()를 중첩할 수 없습니다.
    • 의사 요소(::before 등)는 인자로 쓸 수 없고, 의사 요소를 기준점으로 삼을 수도 없습니다. 조상 스타일에 따라 존재 여부가 달라지는 것들이라 순환 질의가 생기기 때문입니다.
  3. 미지원 브라우저를 신경 써야 한다면 :where()로 감쌉니다. :has()를 모르는 브라우저는 그 선택자 블록 전체를 버리는데, :is()/:where() 같은 관대한 목록 안에 있으면 그 부분만 무시됩니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.