main-logo

공통 컴포넌트의 CSS 확장성 높이기

:where(), :is(), :has()로 알아보는 선택자와 명시도

profile
sunny
2026년 10월 24일 · 0 분 소요

들어가며

프로젝트를 진행하면서 디자인 시스템 컴포넌트의 스타일을 재정의해야 하는 상황이 있었습니다. 컴포넌트의 스타일을 살펴보던 중 :where(.btn-a), :is(.btn-a, .btn-b), :has(svg)와 같은 CSS 선택자들을 알게 되었는데, 어떤 역할을 하는지 궁금해졌고 앞으로 공통 컴포넌트를 설계할 때 고려하면 좋을 CSS 작성 방식과 스타일 관리 방법도 함께 정리해 보려 합니다.

1. :where() — 명시도에 영향을 주지 않는 선택자

:where()는 전달된 선택자 목록 중 하나와 일치하는 요소를 선택하는 CSS 함수형 선택자입니다. 가장 큰 특징은 선택자 내부에 어떤 선택자가 들어가더라도 명시도가 항상 0이라는 점입니다.

.btn-a {
  color: red;
}

:where(.btn-a) {
  color: #333;
}

두 선택자가 같은 요소를 대상으로 할 때, :where(.btn-a)의 명시도는 0이고 .btn-a는 클래스 선택자의 명시도를 가집니다. :where(.btn-a)의 선언이 CSS에서 더 나중에 작성되었더라도, 다른 캐스케이드 조건이 같다면 명시도가 더 높은 .btn-a의 color: red;가 최종적으로 적용됩니다.

:where()는 기본 스타일의 명시도를 낮춰, 사용하는 쪽에서 별도로 복잡한 선택자를 작성하지 않고도 스타일을 재정의하기 쉽게 만들어 줍니다.

실제로 제가 살펴본 공통 컴포넌트 대부분에도 :where()가 적용되어 있었습니다. 대부분의 컴포넌트에서 선언되어있어서 css를 사용하는데 엄청 복잡해 보였는데요. 그 이유를 알아보니 컴포넌트를 사용하는 환경에서 스타일을 보다 쉽게 확장하고 재정의할 수 있도록 고려한 설계였다는 점을 알게 되었습니다.

2. :is() — 여러 선택자를 간결하게 작성하기

:is()는 전달된 선택자 목록 중 하나와 일치하는 요소를 선택하는 함수형 선택자입니다. 여러 선택자에 동일한 스타일을 적용해야 할 때 중복을 줄일 수 있습니다.

예를 들어 제목 요소마다 같은 스타일을 적용하고 싶다면 다음과 같이 작성할 수 있습니다.

.card h1,
.card h2,
.card h3 {
  margin-bottom: 16px;
}

/* :is() 활용 */
.card :is(h1, h2, h3) {
  margin-bottom: 16px;
}

:is()의 명시도는 인자로 전달된 선택자 중 가장 명시도가 높은 선택자를 기준으로 결정됩니다. 실제로 일치한 선택자의 명시도를 따르는 것은 아닙니다. 인자에 명시도가 높은 선택자가 포함되면 예상보다 스타일 재정의가 어려워질 수 있습니다.

처음에는 이 내용을 모르고 :where, :is의 조합으로 인해 어떤 경우에서는 스타일 오버라이드가 도저히 안되더라구요. :is에 묶인 명시도가 높은 선택자 때문인 걸 모르고 무작정 듀얼 클래스를 써야겠다고 생각했는데, 공통 스타일을 낮은 명시도로 작성하면 사용하는 화면에서 더 복잡한 선택자를 작성하거나 !important를 추가하지 않고도 스타일을 재정의하기 쉬워집니다.

3. :has() — 특정 요소의 존재 여부에 따라 스타일 적용하기

:has()는 지정한 선택자와 일치하는 요소를 포함하는지 등 의 조건을 기준으로 요소를 선택할 수 있는 함수형 선택자입니다. 특히 하위 요소의 상태나 구조를 기준으로 상위 요소에 스타일을 적용할 수 있다는 점이 특징입니다.

예를 들어, 상태에 따른 클래스가 구분이 되어있지 않은데, 버튼 요소에서 아이콘이 있는 경우 예외적인 스타일을 줘야 하는 경우에서 필요합니다.(두가지 케이스의 디자인 스타일이 통일되거나, 공통 컴포넌트에서 hasIcon 이라는 클래스만 있더라면 편하겠지만....프로젝트를 하다보면 늘 예외적인 케이스들이 발생합니다.)

.btn-a {
    padding: 4px 12px;
}

.btn-a:has(svg) {
    padding: 4px 8px;
}

 

유효하지 않은 입력 요소를 포함한 폼 필드에 테두리 색상을 적용하려면 다음과 같이 작성할 수 있습니다.

.form-field:has(input:invalid) {
  border: 1px solid red;
}

 

이처럼 :has()를 활용하면 DOM 구조에 따라 스타일을 선언적으로 구성할 수 있습니다. 특정 상태를 표현하기 위해 JavaScript로 별도의 클래스를 추가해야 하는 상황을 줄이는 데에도 도움이 됩니다.

다만 :has()는 조건에 따라 스타일을 적용하는 기능이지, 그 자체로 CSS 명시도를 낮추는 기능은 아닙니다. 명시도는 :has()안에 전달된 선택자 중 가장 높은 명시도를 기준으로 계산됩니다.공통 컴포넌트에서 사용할 때는 선택 조건뿐 아니라 명시도와 유지 보수성도 함께 고려하는 것이 좋습니다.

 

공통 컴포넌트 설계 시 고려할 점

공통 컴포넌트는 여러 화면에서 재사용되는 만큼, 기본 스타일을 정의하는 것뿐 아니라 사용하는 쪽에서 스타일을 얼마나 쉽게 확장할 수 있는지도 고려해서 작성해야 한다는것 알게 되었습니다.

예를 들어 공통 컴포넌트의 기본 스타일을 높은 명시도로 작성하면, 사용하는 화면에서는 이를 재정의하기 위해 더 높은 명시도의 선택자를 작성해야 할 수 있습니다. 이러한 방식이 반복되면 CSS가 복잡해지고 유지보수도 어려워질 수 있습니다.(우리가 흔히 생각하는 듀얼 클래스를 사용하는 방식겠죠?)

이를 줄이기 위해 다음과 같은 점을 고려할 수 있습니다.

  • 기본 스타일의 명시도 낮추기: 필요한 경우 :where()를 활용해 외부에서 재정의하기 쉬운 구조를 만듭니다.
  • 선택자 복잡도 관리하기: :is()를 활용해 반복을 줄이되, 인자에 포함된 선택자가 명시도에 미치는 영향을 확인합니다.
  • 예외 스타일의 기준 명확히 하기: :has()를 활용할 수 있지만, 단순히 DOM 구조에 의존하기보다 컴포넌트의 상태나 변형을 명시적인 API로 제공하는 편이 적절한지도 함께 검토합니다.

 

물론 모든 스타일을 낮은 명시도로 작성하거나 CSS 선택자만으로 해결해야 하는 것은 아니겠지만, 스타일을 재정의하는 과정에서 불필요하게 복잡한 선택자나 !important에 의존하지 않도록 컴포넌트의 기본 스타일과 확장 지점을 함께 고민해 설계하는 것이 중요하다고 느꼈습니다.

마치며

공통 컴포넌트의 스타일을 재정의하는 과정에서 시작한 궁금증이 CSS 선택자의 특징과 명시도, 스타일 관리 방법까지 살펴보는 계기가 되었습니다.

이번에 알아본 내용을 바탕으로 앞으로 공통 컴포넌트를 설계할 때는 단순히 스타일을 재사용하는 것에 그치지 않고, 컴포넌트를 사용하는 환경에서도 스타일을 유연하게 확장하고 재정의할 수 있도록 함께 고민하며 설계해야겠습니다.

 

관련 자료