:is∙:where∙:has∙:not function
CSS는 점점 더 강력한 언어로 발전하고 있다. 예전에는 단순히 스타일을 지정하는 수준에 그쳤지만, 이제는 조건부 로직, 선택자 그룹화, 부모 요소를 기준으로 한 선택까지 가능해졌다. 그 중심에는 :is(), :where(), :has()와 같은 새로운 CSS 함수들이 있다. 이 함수들은 선택자 작성 방식을 간결하게 만들고, 지금까지 불가능하거나 번거로웠던 스타일링 패턴을 더 직관적으로 구현할 수 있도록 도와준다.
:is() 중복을 줄이고 유지보수성을 높이는 선택자 그룹화
:is() 함수는 여러 선택자를 묶어서 한 번에 스타일링할 수 있다. 단순히 코드 길이를 줄여주는 것 같지만, 실무에서는 UI 컴포넌트의 상태 관리에 자주 활용된다.
예를 들어 버튼 컴포넌트가 있고, 이 버튼은 hover, focus, active 상태에서 모두 같은 강조 스타일을 가져야 한다고 하자. 기존에는 아래처럼 중복 코드를 작성해야 했다.
.button:hover,
.button:focus,
.button:active {
background-color: var(--color-primary);
color: white;
}
:is()를 사용하면 다음처럼 깔끔하게 정리된다.
.button:is(:hover, :focus, :active) {
background-color: var(--color-primary);
color: white;
}
이 패턴은 버튼, 네비게이션 탭, 카드 hover 효과 등 상태별 스타일이 반복되는 컴포넌트에서 특히 유용하다. 코드가 간결해질 뿐만 아니라, 새로운 상태를 추가할 때도 한 군데만 수정하면 되므로 유지보수성이 크게 향상된다.
:where() 안전한 초기화와 유틸리티 스타일
:where() 함수는 :is()와 똑같이 선택자를 그룹화하지만, 특이성(Specificity)을 항상 0으로 만든다는 점이 다르다. 이 특징 덕분에 스타일 초기화(reset)나 유틸리티 성격의 공통 스타일을 정의할 때 매우 유용하다.
예를 들어 프로젝트 초반에 기본적으로 모든 제목 태그의 마진을 제거하고 싶다고 하자. 만약 is()를 쓰면 특정 상황에서 나중에 적용한 스타일보다 강해져서 예기치 못한 덮어쓰기가 발생할 수 있다. 그러나 :where()를 쓰면 특이성이 0이므로 다른 규칙에 쉽게 덮어씌워진다.
:where(h1, h2, h3, h4, h5, h6) {
margin: 0;
font-weight: inherit;
}
실무에서 :where()는 유틸리티 스타일을 정의할 때 자주 활용된다. 예를 들어 모든 링크, 버튼, 혹은 인터랙티브 요소에 기본 전환 애니메이션을 적용하고 싶을 때 :where(a, button, .interactive)에 transition: all 0.2s ease-in-out;과 같은 속성을 선언하면 된다. 이렇게 하면 프로젝트 전반에서 부드러운 인터랙션을 기본값으로 제공할 수 있으며, 필요하다면 개별 요소에서 쉽게 덮어쓸 수 있다.
:has() 부모 선택을 가능하게 하는 강력한 조건부 스타일링
has() 함수는 “부모 요소가 특정 자식을 포함하는지”에 따라 스타일을 적용할 수 있게 해준다. 이는 실무에서 지금까지 자바스크립트로만 가능했던 패턴을 CSS로 대체할 수 있다는 뜻이다.
예를 들어 네비게이션 메뉴에서 현재 페이지를 나타내는 a.active 링크가 있을 때, 그 부모인 li에도 스타일을 적용하고 싶다고 하자. 기존에는 클래스 토글이나 JS 조작이 필요했지만, 이제는 CSS만으로 해결된다.
nav li:has(a.active) {
background-color: var(--color-highlight);
}
또 다른 사례로, 폼 유효성 검사를 생각해볼 수 있다. 체크박스가 선택되지 않았을 때 부모 그룹에 경고 스타일을 주는 경우다.
.form-group:has(input[type="checkbox"]:not(:checked)) {
outline: 2px solid red;
}
has()는 카드 레이아웃, 아코디언, 드롭다운 메뉴에서도 유용하다. 예를 들어 카드 안에 이미지가 있을 때만 특별한 레이아웃을 적용하고 싶다면 아래처럼 작성할 수 있다.
.card:has(img) {
grid-template-rows: auto 1fr;
}
즉, has()는 “부모의 상태를 자식 요소로부터 역으로 추론”할 수 있게 해주며, 덕분에 불필요한 클래스 네이밍이나 JS 개입을 줄일 수 있다.
:not() 특정 조건을 배제하는 선택자
not() 함수는 지정된 선택자와 일치하지 않는 요소를 선택하는 데 사용된다. 말 그대로 “~가 아닌 요소”를 걸러낼 수 있어, 선택 범위를 세밀하게 조절할 때 매우 유용하다.
예를 들어 네비게이션 메뉴에서 활성화된 링크(.active)를 제외한 모든 링크에 스타일을 적용하고 싶다고 하자. 기존에는 .nav a에 기본 스타일을 주고 .nav a.active에 다시 덮어쓰는 방식으로 처리해야 했다. 하지만 not()을 사용하면 더 선언적이고 명확하게 표현할 수 있다.
.nav a:not(.active) {
color: gray;
}
이렇게 하면 활성화되지 않은 링크만 회색으로 처리되고, .active는 별도 규칙을 두지 않아도 기본 상태를 유지한다.
실무에서 not()은 다양한 패턴에 활용된다. 예를 들어 테이블에서 특정 컬럼을 제외하고 스타일을 주거나, 리스트에서 첫 번째 항목을 제외하고 간격을 줄 때 사용할 수 있다.
/* 첫 번째 li를 제외한 모든 항목에 상단 여백 추가 */
.list li:not(:first-child) {
margin-top: 1rem;
}
또한 not()은 is()나 where()와 조합해 더욱 강력한 조건을 만들 수 있다. 예를 들어, 모든 버튼 중에서 .primary나 .danger 클래스를 가진 버튼을 제외한 나머지 버튼에만 스타일을 적용하려면 다음과 같이 작성한다.
button:not(:is(.primary, .danger)) {
background: lightgray;
}
이처럼 not()은 예외 조건을 선언적으로 표현할 수 있어 코드 가독성을 높이고, 불필요한 덮어쓰기 규칙을 줄이는 데 도움을 준다.
블로그의 정보
Ayden's journal
Beard Weard Ayden