CSS 선택자 | 기본, 속성, 가상, 결합, 특이도, :has, 성능 (HTML/CSS 시리즈)
이 글의 핵심
선택자는 어떤 DOM 노드에 선언이 적용될지를 가리키며, 특이도·캐스케이드·@layer와 함께 읽을 때만 실무에서 스타일이 예측 가능해진다.
시리즈 안내
#02(선택자 심화) | 전체 목차 | CSS 기초·HTML 기본과 함께 읽기
이 글은 CSS 기초에서 다루는 캐스케이드·특이도 맥락을 전제로, 선택자 문법을 한 번에 훑는 HTML/CSS 시리즈 보조편이다. 박스 모델·Flexbox로 넘어가기 전에, “어떤 요소에 규칙이 먹는지”를 문법 수준에서 확실히 잡아 두면, 레이아웃 디버깅이 훨씬 단순해진다. 솔직히 처음엔 표와 쳇시트에만 충실한 글이면 충분하다고 생각했는데, 요즘은 “사람 냄새”가 나는 쪽이 오래 읽힌다—그래서 아래에 참 이야기도 몇 군데 박아 두었다.
들어가며
CSS 선택자(Selector)는 스타일시트의 “주소”에 해당한다. 선언 블록 { ... } 앞에 붙는 패턴이 DOM 트리의 어떤 노드(요소)를 대상으로 할지를 정의한다. 동일한 color: red;라도, 선택자가 다르면 적용 범위가 달라지고, 특이도(specificity)가 달라지며, 캐스케이드에서 이기고 지는 관계가 달라진다.
이 글의 목표는 (1) 문법 지도를 머릿속에 올리고, (2) 특이도를 손으로 계산할 수 있게 하며, (3) 의사 클래스·결합자로 실제 UI 패턴을 짜는 것이다. “암기”보다 “브라우저가 매칭을 어떻게 돌리는가”에 가깝게 읽으면, 나중에 Shadow DOM이나 프레임워크가 생성한 DOM에서도 혼란이 줄어든다. 저는 시트가 길어질수록 “명세”보다 DevTools에 밑줄이 어디에 그어지는지를 먼저 믿는 편이다. 그게 곧 팀이 나중에 고생할지 말지를 가늠하는 현장 지표가 되곤 한다.
IE11 지원하다가 선택자 때문에 울었던 날
예전에 B2B 관리 콘솔을 IE11까지 끌고 가야 할 때가 있었다. 당시 저는 nav li:first-child와 li:first-of-type의 차이를 머리로는 알고 있었는데, 문서 한 구석의 주석/공백 노드가 끼면서 nth 계산이 엇나가는 케이스에 손댈 때마다 멘탈이 갈렸다. IE/구형 Trident 쪽은 지금의 Chromium처럼 친절한 Specificity 툴팁도 희박했고, “왜 여기 li만 스트라이프가 끊기지?” 같은 문제를 콘솔로 DOM을 뜯으며 잡는 날이 반복됐다.
또 기억에 남는 건 not이 Selectors 3에선 단일 단순 선택자만 넣는 제약이었다는 점이다. button:not(.primary, .ghost)를 써 본 적이 있었다면(혹은 옆자리 누군가가) 스타일이 “조용히” 빠질 수 있다. “선택자가 틀렸다”가 아니라 “이 브라우저에선 문법이 달랐다” 쪽에 가깝다. 팀이 당시에 한 일은, 자주 쓰는 안전한 조합을 짧은 팀 약속(문서)으로 고정하는 것—:is를 못 쓰면, 그만큼 class 토큰과 BEM/데이터 훅에 마음이 기우는 쪽이었다. 지금은 IE가 사라지고 대부분 Selectors 4 가정으로 돌아가지만, “선택자는 문법일 뿐, 동시에 배포/타깃의 계약”이었다는 감각은 남는다. 지원 매트릭스가 바뀌어도, 그 계약을 한 번씩 Browserslist에 다시 눈으로 찍는 습관은 이어갈 만하다.
1. 선택자 개요 — CSS에서 선택자의 역할
선택자는 선택자 목록(Selector List)으로 쓰인다. 쉼표로 구분하면 여러 조건 중 하나에 맞기만 해도 동일한 선언이 적용된다.
h1,
.page-title,
[role='heading'][aria-level='1'] {
font-weight: 700;
}
- 스타일 적용의 범위: 위 예에서 세 패턴 중 하나에 해당하는 요소는 모두 동일한 선언을 공유한다.
- 캐스케이드와의 연결: 이때 각 선택자 조각은 자체 특이도를 갖는다(복합·결합·의사는 합산이 뒤이어서 설명한다).
- DOM과의 관계: HTML 파서가 만든 요소 트리에 대해, 스타일 엔진이 스타일 규칙을 “매칭”한다. JavaScript의
querySelector(All)가 같은 Selectors 문법을 쓰는 이유이기도 하다.
요약: 선택자는 “규칙이 설명하는 집합”이며, 그 집합이 겹칠수록 캐스케이드·특이도 판정이 앞으로 나온다.
1.1 복합(Compound) vs 복합자(Complex) — 용어 정리
한 규칙의 선택자(Complex selector)는 결합자로 이어지는 복합(Compound) 선택자들의 연쇄로 읽는다. 예를 들어 article.card > h2.title는 article과 .card가 AND로 붙은 복합 선택자 하나(article.card)가, 자식 결합자 > 뒤에 또 하나의 복합 선택자(h2.title)로 이어진 복합자 구조다.
- SE(단순 선택자): 타입(태그) 선택자, 범용
*,class,id, 속성, 가상 클래스, 가상 요소 등 한 “토큰” 단위. - Compound: SE가 한 요소(상자)에 동시에 붙는 곱(AND, 공백 없이 이어쓰기).
- Complex: 결합자(공백,
>,+,~)로 연결해 트리를 따라 범위를 잡는다.
문서/도구(예: Selectors Level 4)에서 말하는 Subject는(문맥에 따라) 최우측의 복합 선택자가 꾸미는(스타일이 적용되는) 앵커 요소를 가리키는 일이 많다. :has()가 등장한 뒤에는 “부모/조상이 subject가 될 수 있나?” 같은 질문이 늘었는데, 명세가 ! subject 표기로 누가 꾸밈 대상인지를 밝힌다. 실무에서는 “DevTools Styles 탭에 밑줄이 어느 요소에 그어지는가?”로 확인하는 것이 가장 빠르다.
1.2 매칭 “방향”에 대한 직관(과장하지 않기)
흔히 “CSS는 오른쪽에서 왼쪽으로 매칭한다”고 말한다. 이는 최적화 관점(키가 되는 오른편 후보를 먼저 좁힌다)의 휴리스틱이지, Selectors 자체를 절대 그런 식으로만 읽는다는 뜻은 아니다. 중요한 것은, 의미는 문법 그대로 왼쪽→오른쪽으로 트리 제약을 누적한다는 점이고, “느리다/빠르다”는 프로파일 전까지는 단정하지 않는다(§ 17). 현장에선 규칙이 생기는 흐름(선택자 → 매칭 → 캐스케이드)만 머릿속에 잡고, 느리다고 느껴질 땐 Performance 패널로 가는 쪽이 정신 건강에 이롭다.
1.3 JS API와의 공통문법
Element.matches(), querySelector(All)()는 CSS 선택자(일부 보안/성능 제한)와 같은 문맥을 공유한다. “서버는 HTML A, 클라이언트는 스타일 B”처럼 스타일과 스크립트가 같은 훅(data-state, aria-*, class)을 쓰면, 회귀를 한 번에 잡을 수 있다.
2. 기본 선택자
2.1 전체 선택자 *
모든 요소에 일치한다. 리셋이나 박스 모델의 box-sizing: border-box 전역화에서 자주 쓰인다.
*,
*::before,
*::after {
box-sizing: border-box;
}
- 주의:
*단독으로 글꼴·여백을 바꾸면, 의도보다 범위가 지나치게 넓어질 수 있다. “전역”은 항상 최소 권한으로 설계하는 것이 좋다. *::before/*::after는 가상 요소에 대한 매칭이므로, 전역box-sizing의 관용 패턴에 포함하는 경우가 많다.
2.2 타입(태그) 선택자
요소 이름(로컬 네임)이 일치하면 선택한다. HTML에서는 p, div, a 등.
a {
text-decoration: none;
color: var(--link);
}
- 이름 공간/대소문자: HTML는 보통 대소문자 비구분이지만, XML/SVG·foreign content에서는 대소문자를 구분할 수 있다(환경·문서 모드에 따름). 실무에서는 소문자가 안전하다.
- 너무 넓은 타입 선택자는 (라이브러리와 충돌하거나) 나중에 특이도 전쟁의 출발점이 될 수 있어, 운영 코드에서는
class·data-*와 함께 범위를 좁히는 편이 낫다.
2.3 클래스 .class
class 속성의 토큰 목록 중 하나로 일치하면 선택된다(토큰은 공백으로 구분).
.btn { display: inline-flex; }
.btn.primary { background: #2563eb; }
- 다중 클래스:
.btn.primary는class에btn과primary가 둘 다 있을 때(순서 무관) 매칭되는 합성(AND)이다. - 재사용성: 디자인 시스템에서 가장 흔한 베이스 단위이며,
id보다 재사용이 자연스럽다. - 접두사:
ds-·c-등 팀 컨벤션으로 충돌을 피하는 것이 일반적이다.
2.4 ID #id
id 속성이 정확히 일치하는 단일 요소에 대한 선택자(문서에 동일 id가 둘 이상이면 유효하지 않다—HTML에선 오류). 특이도가 클래스보다 높아 “한 번에 이기고 싶을 때” 쓰기 쉬우나, 재사용·테스트와 맞지 않는 경우가 많다.
#app-root { min-height: 100dvh; }
- 실무 루브릭: 진입 루트(마운트 포인트)나 스크롤/측정 앵커에
#를 남기고, 컴포넌트 스타일은class·data-*가 유지보수에 유리하다. - 특이도:
#id는 나중 클래스만 추가한 규칙이 이기기 어렵다. 가능하면 id보다 class + 레이어를 선호한다.
*는 “전부”이고, 태그는 “이름이 이거일 때”이며, class는 “토큰 하나만 맞아도”이고, id는 “문서에 하나뿐인 앵커”에 가깝다. 운영 코드에선 *로 타이포를 뒤집는 실수(범위 폭주)랑, 태그만으로 라이브러리와 부딪치는 사고(특이도 전쟁)는 제가 많이 본 편이다.
2.5 *와 html, :root의 실무 감각
htmlvs:root:html은 타입(태그) 선택자이고,:root는 문서 루트를 잡는 가상 클래스다. 둘 다 일반 HTML 문서에선 같은<html>요소에 매칭되나, 특이도는 다르다(명세/도구: 태그 1단 vs 가상 클래스 1단—따라서 동일 선언이면:root가html단독보다 이길 수 있다). CSS 변수(커스텀 프로퍼티) 전역 토큰은 관용적으로:root { --… }에 둔다(§ 15).*의 한계:*는 의미는 단순하나, 캡슐화된(Shadow) 트리 밖/안의 이슈는 별도(§ 19.5). 전역*리셋은 CSS 기초의 @layer와 함께 읽는 것이 안전하다.
2.6 타입·클래스·ID를 섞는 “베이스 + 스코프” 패턴
/* 1) 문맥(스코프)을 먼저 제한: 컴포넌트 루트 */
[data-component='date-picker'] .dp-cell { }
/* 2) 팀/제품 프리픽스로 전역 충돌을 줄인다: .acme-btn */
.acme-btn { }
- 권장:
article.blog .content a같이 문맥+타입을 3단 이상으로 길게 끌지 않는다(대신 BEM, Blockblock__el--mod또는 data- 기반). 길이가 곧 특이도 누적과 취약한 결합이 된다.
3. 속성 선택자 (Attribute Selectors)
[attr], 정확 일치, 접두/접미/부분문자열 등 속성 기반으로 집단을 잡는다. 시맨틱 UI(ARIA)나 data-* 디자인과 잘 맞는다.
3.1 [attr]
속성 존재만으로 매칭한다.
[hidden] { display: none !important; }
[aria-disabled='true'] { pointer-events: none; opacity: 0.5; }
- ARIA/접근성: 역할·상태는 스타일과 보조기술이 같은 정보를 쓰도록 연결하는 데 유효하다(단, ARIA 남용은 피할 것).
hidden은 UA 스타일이 있을 수 있으나, 컴포넌트 일관성을 위해 팀이 재정의하기도 한다.
3.2 [attr="value"] (정확 일치)
속성 값이 대소문자를 구분하는지는 CSS 명세/문서에 따른다(HTML에서는 대다수 속성이 ASCII 대소문자 비대응이나, type의 일부 값은 구분). 실무에선 i 플래그가 붙는 [attr="val" i](대소문자 무시)를 기억해 두면 좋다.
input[type='checkbox'] { accent-color: #16a34a; }
a[rel~='external'] { /* rel 토큰 목록 */ }
3.3 |= (언어·하이픈 구분) · *=(부분) · ^=(접두) · $=(접미)
|=:attr이 정확히value이거나value-뒤에 붙는 언어코드 형태(예:en,en-US등)에 사용되던 패턴.*=: 부분문자열.class에btn이 포함되면 잡힌다(의도와 다를 수 있으니 class 전용으로는 권장하지 않는다).^=/$=:href의 프로토콜/경로, 파일 확장자, 내부 앵커 등.
[href^='https://'] { /* … */ }
[href$='.pdf']::after { content: ' (PDF)'; }
[class*='card'] { /* class 토큰 “문자열”이 아닌, 속성 값 부분 */ }
- 성능/명확성:
*=·^=는 “문서 전체”에서 쓰면 매칭 범위가 커질 수 있다. 가능하면data-variant="card"처럼 정확한 키-값을 쓰는 것이 읽기 쉽다.
속성 문법의 골자는 존재([a]) → 정확/토큰/접두·접미/부분일치로 뻗는다. [a|="p"]는 p 또는 p-…에 쓰고, i·s는 대소문자 민감도를 끈다. 표로 외울 가치는 있으나, 실무에선 DevTools에서 매칭이 맞는지를 보는 쪽이 더 빨리 체에 남는다.
3.4 [attr~="t"] vs .class — 토큰 vs 클래스 선택자
[class~="foo"]는 class 속성 값이 공백으로 나뉜 토큰 목록에 foo가 하나 있으면 매칭한다. 의미는 .foo와 비슷하나, 특이도는 class·속성·의사 가산이 달라질 수 있다(§ 7). “왜 .active는 먹는데 [class~='active']는 다른 규칙에 지는가?”는 캐스케이드 누적 표로 푼다.
3.5 ^= / *= / $=에 i(대소문자 무시) 붙이기
[href$='.PDF' i] { /* ... */ }
[data-code*='abc' i] { /* ... */ }
외부 파일 링크·쿼리스트링이 대소문자가 섞인 환경에서 유용하다. 반대로 의도적으로 구분하려면 s 플래그(XML 등)를 쓰는 문서 유형을 확인한다.
4. 가상 클래스 (Pseudo-classes) — :hover, :focus, :nth-child, :first-child
가상 클래스는 특정 상태·트리에서의 위치에 기반해 요소를 고른다. 구문은 하나의 콜론 :(레거시로 단일 콜론 가상 요소가 일부에 남아 있으나, 가상 요소는 :: 권장).
4.1 :hover, :focus, :focus-visible, :active
- :hover: 포인터가 위에 있을 때(터치는 기기·브라우저에 따라 “누르고 있는” 동안 등, 상이).
- :focus: 키보드/프로그램 포커스가 올라왔을 때. 클릭으로도 포커스가 잡힐 수 있음.
- :focus-visible: 키보드 등 “보이는 포커스 링”이 필요한 경우에만(접근성·디자인 동시에 중요).
- :active: “누르는” 순간(마우스 다운~업 사이).
.btn:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
a:hover { text-decoration: underline; }
a:active { color: #1d4ed8; }
- 권장:
:focus { outline: none; }만 주는 것은 접근성상 위험. 반드시focus-visible으로 대체하거나, WCAG 2.4.7/2.4.11 요구에 맞는 눈에 보이는 초점을 남겨야 한다. - 순서: 일반적으로 LVHA
link, visited, hover, active(링크에 한함) 순서가 오버라이드에 영향(나중이 이김)을 줄 수 있어, 시트 내 배치에 유의.
4.1.1 :link, :visited, :any-link, :local-link
앵커 a[href]는 URL 상태에 따라 서로 다른 가상 클래스로 잡힌다(브라우저·프라이버시·사용자 테마에 따라 :visited 스타일이 제한될 수 있다).
- :link — 방문 전
href가 있는 링크에 매칭(나머지a는link·visited정의에 따라). - :visited — 방문 기록이 있는(동일 출처/보안 규칙이 적용) 링크.
- :any-link —
link또는visited둘 중 하나(현대 “그냥 모든 하이퍼링크”에 쓰기 쉬움). - :local-link —(지원/문맥 제한) 같은 문서·같은 사이트 링크 등, 필터가 명세/UA에 따름.
/* LVHA: 나중이 이긴다(일반 루틴) */
a:link { color: #2563eb; }
a:visited { color: #6b21a8; }
a:hover { text-decoration: underline; }
a:active { color: #1d4ed8; }
/* 유지보수성: "방문"과 무관하게 동일 기본 */
a:any-link { text-underline-offset: 2px; }
- 접근성/보안:
:visited에 “정확한 색/배경”에만 의존해 콘텐츠를 구분하지 말 것. 콘텐츠 구분은 문구·아이콘+alt로.
4.1.2 :target, :target-within, 스크롤/딥링크
- :target —
URL프래그먼트(#section-id)가 가리키는 요소(해당id를 가진 요소). - :target-within —(지원) 자손에 target이 있는 조상(드롭다운·모달 내부 앵커와 함께 쓰기도).
id="이름"+:target는 딥링크 하이라이트·ToC(목차) 현재 점에 쓰인다(단, position·scroll-margin는 별도).
section:target { scroll-margin-top: 5rem; }
:target h2 { outline: 2px solid #93c5fd; }
4.2 :first-child · :last-child · :only-child
부모의 자식 집단에서의 서수·한정.
ul.features > li:first-child { margin-top: 0; }
- :first-child: 부모의 첫 자식이 동시에
li이어야 한다. “첫 번째 li”를 원하면:first-of-type과 구분이 필요하다(아래 12장과 연결). - :only-child: 부모의 자식이 하나뿐인 요소(타입 무관). “형제가 없는 카드”에 여백을 다르게 줄 때.
4.2.1 :focus-within — 자식이 포커스일 때 조장 꾸미기
.search-bar:focus-within { box-shadow: 0 0 0 2px #2563eb; }
- :focus-within — 요소 자체 또는 자손에 포커스가 있을 때(키보드·프로그램 모두, 브라우저 정책에 따름). 검색창·콤보박스에서 “바깥 테두리”를 같이 켜는 패턴.
4.2.2 :user-invalid vs :invalid (폼 14장과 연결)
- :invalid — 제약(constraint) 위배면 상시(초기 로드 시점에 빨갛게는 UX상 위험).
- :user-invalid —(지원) 사용자 상호 이후(§ 14). “제출 전 입력 중 불필요한 붉은 테두리”를 줄이려면 둘을 나누는 팀이 많다.
4.3 :nth-child(An + B of S) (12장에서 상세)
3n+1처럼 식으로 “줄무늬”·그리드 행/열 느낌의 패턴을 잡는다(상세는 § 12).
tbody tr:nth-child(odd) { background: #f8fafc; }
5. 가상 요소 (Pseudo-elements) — ::before, ::after, ::first-line
가상 요소는 “요소 안의 가상의 상자”를 가리킨다. :: 문법이 표준(구형 :도 일부 호환).
5.1 ::before / ::after
content와 함께 장식·아이콘·배지에 자주 쓰인다. 콘텐츠가 content:''이어도 display·크기·배경으로 “도형”을 그릴 수 있다.
.external::after {
content: ' ↗';
font-size: 0.9em;
}
- 접근성: 장식이면
aria-hidden·불필요한 스크린리더 노출을 피하도록 설계. 중요한 텍스트는::aftercontent에만 넣지 말고, DOM 텍스트에 둔다. - 특이도:
::는 의사로서 특이도에 가산된다(§ 7).
5.2 ::first-line / ::first-letter ::selection
- ::first-line: 블록의 시각적 첫 줄에 대해(줄 나눔은 뷰포트에 따름) 스타일.
- ::first-letter: 첫 글자에 대한 드롭캡等.
::selection: 사용자가 드래그한 구간(브라우저마다color제약이 있을 수 있음).
p.lead::first-line { font-weight: 600; }
5.3 가상 요소 심화 메모
- 이미지 대체로
::before만 쓰면, alt를 없애는 실수로 이어질 수 있으니<img alt>·aria가 우선. position: absolute한::는 부모(원 요소)의 positioning context와 같이 겹침을 position 글에서 본다.
5.4 ::placeholder, ::file-selector-button, ::part() (힌트)
::placeholder—input/textarea의 플레이스홀더 텍스트(색은opacity·color대비 WCAG에 유의). 콘텐츠를 의미 있게placeholder에만 두지 말 것.::file-selector-button—<input type="file">의 버튼 조각(브랜딩;::button아님).::part(name)— Shadow 내부 부품에 스타일(선택자 범위는 캡슐화 문서에 따름). 디자인 시스템·웹 컴포넌트가 넘어가는 지점(§ 19.5).
input::placeholder { color: #64748b; }
input::file-selector-button { margin-right: 0.5rem; }
5.5 ::marker — 리스트/요약 글머리 스타일
ul/ol/li와 summary의 불릿/번호를 꾸민다(브라우저 UA·i18n에 따라 모양이 달라질 수 있음).
ul.plain { list-style: none; padding-left: 0; }
ul.plain li::marker { color: #94a3b8; }
- 접근성:
list-style: none는 의미·SR에 영향이 있을 수 있어(맥락별), 시맨틱 HTML과 함께 검토한다.
5.6 ::backdrop — dialog·풀스크린 뒤
<dialog>의 ::backdrop은 오버레이를 그린다(포커스 트랩·inert는 JS·HTML 쪽과 짝이 필요).
dialog::backdrop { background: rgba(15, 23, 42, 0.45); }
6. 결합자 (Combinators)
6.1 자손(후손) 결합자 (Space)
A B — A 안쪽 임의의 깊이에 있는 B.
nav a { text-decoration: none; }
6.2 자식 >
A > B — 직접 자식만.
ul.menu > li { border-bottom: 1px solid #e5e7eb; }
6.3 인접 형제 + · 일반 형제 ~
A + B:A바로 뒤에 오는 같은 부모의B한 개.A ~ B:A뒤에 오는 모든 형제B.
h2 + p { margin-top: 0; } /* h2 직후 첫 p */
.error ~ .help { color: #b91c1c; }
- 흔한 실수:
+는 “문서에 붙어 있어야” 한다(중간에 다른 요소가 끼면 끊김). 이때는 :has나 class 토글이 더 견고할 수 있다.
읽을 때는 (공백)이 자손, >가 자식, +가 바로 옆 형제, ~가 뒤따라오는 형제—정도만 몸에 붙이면, 표를 다시 열지 않아도 된다.
6.4 (참고) || 열 결합자 — 표 레이아웃
Selectors 4의 || (column combinator)는, 다중 열 레이아웃(주로 table 열 그룹) 맥락에서 열과 셀의 관계를 표현한다. 지원/사용은 제한적이니, “표가 아닌 일반 flex/grid”를 기대하며 ||를 쓰지 않는다. 실무 표 스타일은 col, colgroup, th/td+:nth-child 조합이 흔하다.
6.5 결합자와 :is() / :where() — 가독성 래핑
/* "nav 안의 a 또는 button"을 한 줄에 */
nav :is(a, button) { }
- :is로 괄호 안 대안을 묶고, 결합자는 바깥에 유지한다(특이도 주의—§ 9).
7. 우선순위 — Specificity(특이도) 계산
같은 출처·중요도·레이어 안에서, “어느 선언이 이기나”는 특이도로 결정되는 경우가 많다. 직관용으로는 (a, b, c, d) 네 점수(Selectors Level 4)를 쓰되, 크로스 비교는 절대 “100이면 10개 클래스” 같은 십진 덧셈이 아님에 주의(명세는 쌍 비교).
7.1 (현대) 개념적 계산(요지)
- ID 선택자: +1 (가장 강한 일반 케이스)
- 클래스, 속성, 가상 클래스(
:where제외 등): +1 (팀마다 “b 카운트”) - 타입·가상 요소: +1 (타입/요소)
*·:where()·(combinator): +0에 가산되지 않음에 가깝다(개념상 “특이도 없음/귀속 없음”).
실제 비교: (a, b, c) 튜플을 사전식으로 비교한다(큰 a가 먼저, 같으면 b…).
예시(직관): div#a.b vs div#a
둘 다 id 1, 요소 1, 후자는 class 한 개 덜 있으면 앞이 크거나/작거나는 b·c 누적에 따라. DevTools “Specificity” 열이 이해를 돕는다.
7.2 복잡한 선택자
.a.b#x :is(.y, #z) div::is안 가장 큰 특이도가 반영(#z쪽)된다.- :not(.foo):
not자체는 0이 아니라, 인자의 가장 큰 기여를 반영(Selectors 4). 구형 브라우저와 해석이 다를 여지가 있으니, 오래된 지원을 보며 문서화한다.
7.3 인라인 style과 !important
- 인라인
style="color:red"는 대부분의 선택자보다 우선한다(캐스케이드의 별 층). !important:importance축이 먼저(§ 8).
실무: 특이도를 올리기 위해 쓸데없이 id를 붙이는 해법은 빚이 쌓인다. class + @layer + 나중에 선언이 더 건강하다.
7.4 “손으로” 세는 연습 (Selectors Level 4 스타일)
튜플 (A, B, C) — A: ID 개수, B: 클래스·속성·가상 클래스(대부분), C: 타입·가상 요소 개수. (구형 4-튜플 문서는 인라인·!important를 별 축으로 뺀다.)
| 선택자 | A | B | C | 직감(메모) |
|---|---|---|---|---|
p | 0 | 0 | 1 | 태그 1 |
.btn | 0 | 1 | 0 | 클래스 1 |
#app | 1 | 0 | 0 | ID 1 |
a:hover | 0 | 1 | 1 | 의사+타입 |
ul li::before | 0 | 0 | 3 | ul+li+::before |
:where(p, .x) | 0 | 0 | 0 | :where=0 |
:is(p, .x) | 0 | 1 | 0 | 가장 “큰” 인자(.x) |
article:has(.error) | — | — | — | :has는 의사 1 + 인자 중 가장 큰 특이도 합산(Selectors 4)(단순 A·B·C 한 줄에 넣기 어려움—DevTools “Specificity” 확인 권장) |
- A가 크면 우선이고, 동일이면 B, 그다음 C. 또 동일이면(동일 레이어) 뒤에 선언된 규칙이 이긴다.
:is()/:not()/:where()는 인자마다 특이도 규칙이 달라 눈대중이 틀릴 수 있으니, 최신 브라우저 DevTools “Specificity”를 1차 근거로 삼는다.
7.5 inherit·currentColor·revert는 특이도와 다른 축
“어느 선언이 이겼는가”와 별도로, 속성 값이 inherit·revert·revert-layer인지는 캐스케이드 5 값 층이다. “특이도로 이겼는데 색이 부모를 닮는다”면 inherit 일치·초기값 혼동이 아닌지 본다.
8. !important — 사용과 남용
!important는 캐스케이드에서 importance 축을 바꾼다. “진짜 한 번 이기게” 할 수 있지만, 유지보수는 급격히 악화된다.
.u-hidden { display: none !important; }
- 괜찮은 케이스(상대적): 유틸리티에서 접근성/레이아웃 깨짐을 막는 최소 오버라이드(예:
display:noneon.hidden[aria-hidden=true]), 써드파티에서 끄기 어려운 스타일의 핫픽스(임시). - 나쁜 케이스: “왜인지는 모르겠는데” 여기에만 이기게
!important—대개 다음 사람이 또!important로 응답한다.
대안
- @layer로 초기/컴포넌트/오버라이드 층을 만든다.
- 동일 특이도라면, 뒤에 선언한다(소스/임포트 순서).
- 구체성을 한 단계만 올린다(“id 추가”는 최후).
- :where()로 “베이스”를 0 특이도에 둔다(§ 9–10).
9. :is()과 :where() — 그룹 선택자
둘 다 다중 선택자를 한 덩어리로 쓰게 하며, 짧게 쓰기 좋다.
:is(h1, h2, h3) { line-height: 1.2; }
:where(a, button) { text-underline-offset: 2px; }
- :is(): 인자 중 가장 높은 특이도로 부훈(그룹의 “최댓값”).
- :where(): 항상 0 특이도(“베이스에 넣을 때” 덮기 쉬움).
언제 :is vs 반복
article :is(h1, h2, h3)는 가독성이 좋다. 다만, 팀이 “특이도 함정”을 싫어하면:where로 0 층을 쓰고, 컴포넌트 층에서class로 덮는 패턴도 흔하다.
:where(p) { margin: 0; }
p.intro { margin-bottom: 1rem; } /* 한 클래스로 덮기 쉬움(특이도 비슷) */
9.1 :is() 안에 ::before — 가상 요소는 하나만(문법 제약)
/* OK: h2와 h3 각각에 ::first-line (구체 문법/지원은 엔진 [문서](https://www.w3.org/TR/selectors-4/) 확인) */
:is(h2, h3)::first-line { font-weight: 600; }
- 잘못된 직관:
::before:is(...)형태는 “가상 요소에 의사”처럼 읽혀 문법이 깨질 수 있다. “복합 선택자의 오른끝”에::가 오는 형태를 명세/린터로 확인한다(§ 1.1 Subject). - 팀 규칙: “
:is로 태그 묶기,::는 맨 끝” 정도의 최소 컨벤션이 리뷰에 도움된다.
9.2 :where()+:is() 중첩 — “베이스 0, 컴포넌트 일부만 :is”
:where(:is(.prose p, .prose li)) { line-height: 1.7; }
- 의도: “
.prose안의 p/li만” 띄엄을 통일. :where가 최상단이면(인자) 0 층에 가깝고, 안쪽 :is는 B 누적을 최소화·최대화하는 팀마다. 끝은 DevTools로 검증한다.
10. :has() — “부모” 매칭(Selectors Level 4, 일명 CSS4)
:has()는 “이 요소 아래에(자손) 이런 조건이 있는가”로 앵커 요소를 고른다. 부모/조상을 자식 상태로 스타일하기 좋다.
/* figure 안에 figcaption이 있을 때 */
figure:has(figcaption) { border: 1px solid #e5e7eb; }
/* 체크된 체크박스 뒤의 라벨 */
label:has(input[type='checkbox']:checked) { font-weight: 600; }
- 성능/복잡도: “문서 전체 + 매우 깊고 일반적인
:has”는 스타일 무효화 비용이 커질 수 있다(엔진·DOM 규모 의존). 클래스 토글·data-state로 옮기면 정적 분석이 쉬워진다. - 접근성/유지보수: 스타일만으로 상태를 표현할 수 있으나, 접근성 속성(aria)과 동기화되게 설계한다.
브라우저: 최신 Chromium·Firefox·Safari가 지원(세부 Can I use 참고, 시점은 변동). 폴리필 불가에 가까우니, 필수 UI는 class 절차를 병행한다.
10.1 :has() 범위·퍼포먼스 (요약)
- “너무 넓은”
main:has(a)—문서 주요 루트 전체를 다시 훑게 만들 수 있다. “스코프”를section.card:has(a.btn)처럼 의미 블록으로 좁힌다. - 대안: State는 class
is-expanded, datadata-open로, :has는 정적·희귀 조합(예: “캡션이 있는figure만 테두리”)에 둔다 — § 7·17과 같이 읽는다.
/* OK: ‘카드’ 단위·조건이 명시적 */
section.teaser:has(a[aria-current='page']) { background: #f1f5f9; }
11. :not() — 부정(negation) 선택자
일치하지 않는 집단을 빼 낸다. Selectors 4에서는 복잡한 인자를 허용.
/* 포커스 링: 포커스이지만, 마우스로만 “보이는” 포커스는 제외하고 싶다면 팀 룰에 맞게 조합 */
button:not([disabled]) { cursor: pointer; }
:not(.a.b)vs:not(.a):not(.b): 의미는 다릅니다(드모르간에 해당). “.a와.b를 동시에 갖지 않는다”와 “.a도 없고.b도 없다”는 서로 다르다.- 특이도:
not인자의 높은 쪽이 반영.
11.1 복합 인자(Selectors 4) — not 안에 , 대안 묶기
/* a도 아니고 button도 아닌(포커스 가능한) 커스텀 — 팀/접근성에 따라 주의 */
[role='button']:not(a, button) { cursor: default; }
- 이전(Selectors 3):
not인자가 단일 단순 선택자에 제한됐다는 기록이 많다. 현대 프로젝트는 4 문법 전제로 Babel 타깃 확인한다.
11.2 :is()+:not() 조합 — “이 또는 저 중 A가 아닌”
:where(h2, h3):not(.no-underline) { border-bottom: 1px solid #e5e7eb; }
- :where 바깥 + :not 안의 최댓값—§ 7 표로 검산한다.
12. :nth-*() — 패턴 선택자 상세
12.1 nth-child / nth-last-child
nth-child(An + B): 앞에서 1, 2, 3, … 번째 자식 중 식에 맞는 요소.odd/even: 2n+1, 2n+0(짝) 축약.nth-last-child: 끝에서부터 셈.
ul.striped > li:nth-child(2n) { background: #f1f5f9; }
12.2 of <selector-list>(Selectors 4)
형제 전체 중 “특정 타입”만 셀 때 사용한다.
/* li 중에서 3n번째 “li”에만(형제에 다른 태그가 끼어도 li만 세기) */
ul.thumbs > li:nth-child(3n of .thumb) { /* … */ }
- :first-of-type / :nth-of-type와의 차이: of 구문이 “필터 후 서수” 쪽으로 사고를 맞출 수 있게 해 준다.
12.3 :first-child vs :first-of-type
- :first-child: “첫 자식”이고“동시에 이 선택자(예: p)”에 맞는가.
- :first-of-type: 부모 아래에서 같은 타입 중 “첫 번째”.
<section>
<h2>제목</h2>
<p>첫 p</p>
<p>둘째 p</p>
</section>
p:first-child→ false(첫 자식은h2).p:first-of-type→ 첫 p는 true.
12.4 nth 패턴 “반 나누기” 요령
- 1-based: 첫 항은 1.
3n+1첫 항: n=0일 때 1, 이후 4, 7, …- 끝에서 m개만:
nth-last-child(-n+3)등 감(negative) n 패턴(치트시트 많음—기억보다 도구/문서 권장).
12.5 자주 쓰는 nth 레시피 (읽는 법이 핵심)
| 목적 | 식(예) | n이 증가할 때 |
|---|---|---|
| 홀/짝 | 2n+1 / 2n or odd / even | 격자 스트라이프 |
| 처음 k개 | nth-child(-n+3) | 1,2,3 만 |
| 4번째부터 | nth-child(n+4) | 4,5,6,… |
| 끝 k개 | nth-last-child(-n+2) | 마지막 2개 |
| 매 3번째(1,4,7…) | 3n+1 | 주기 3 |
| “마지막이 딱 하나일 때” | :only-child 또는 nth-child(1):nth-last-child(1) | 카드/그리드 단일 |
- 흔한 실수:
nth-child(0n+1)와nth-of-type(0n+1)— 필터 대상(타입/태그) 이 다르면 다른 집단을 셀 수 있다(§ 12.3). - of 키워드(Selectors 4):
li:nth-child(2 of .item)— li 중에서.item인 것만 골라 서수 (브라우저 지원 확인).
/* "마지막 행"만(행 길이가 일정일 때) 같은 패턴이 유효. 가변이면 :has나 클래스 */
.grid > *:nth-last-child(-n+3) { /* 예: 3칸 끝 묶음 */ }
12.6 :nth-of-type / :nth-last-of-type
같은 태그 타입만 서수에 포함한다(형제에 다른 태그가 끼어도 p만 센다).
/* section 안에서 "두 번째 p" */
section p:nth-of-type(2) { margin-top: 1.5rem; }
- :nth-child(2 of p)(지원)와 사고를 맞출 수 있으나, 팀은 하나로 통일해 가독성을 유지한다.
13. 상태 선택자 — :enabled, :disabled, :checked
- :enabled / :disabled: 폼 컨트롤의 활성.
disabled속성·fieldsetdisabled상속. - :checked: 체크박스/라디오 선택 상태. :indeterminate(일부 요소)도 함께 기억.
input:disabled { opacity: 0.6; }
input[type='checkbox']:checked + label { color: #15803d; }
- 접근성:
disabled는 일부 보조기술에서 “비활성”이므로, 툴팁으로 이유를 주려면aria-disabled+읽을 수 없게 막는 조합을 디자인 팀과 합의한다(단순 복붙 금지).
14. 폼 선택자 — :required, :valid, :invalid, :user-*
- :required / :optional —
required속성 기준. - :valid / :invalid — HTML5 제약 검사 결과(브라우저/메시지 현지화에 따라 미세 차).
- :user-invalid — 사용자가 상호한 뒤에 유효하지 않다고 보는 상태(“제출 전 붉은 테두리” 완화에 유효, 지원 브라우저 확인).
input:required:invalid:not(:focus) { border-color: #dc2626; }
input:user-invalid { /* 상호 이후 */ }
- JS 연동: React 등에서 “제어 컴포넌트”이면, 네이티브
validity와 동기화에 주의(클래스 기반 검증이 더 명시적인 경우).
15. 구조 선택자 — :empty, :root
- :empty — 자식이 없는 요소(주석/처리 instruction 제외 규칙—공백 텍스트 노드 포함 여부는 명세·버전; 실무에선
min-height만 주고 완결이 안 되는 리스트 스켈레톤에 쓰기도). - :root — 문서 트리의 루트(
html과 동일 요소). 전역 CSS 변수:root { --x:1; }의 관용.
:root { color-scheme: light dark; }
:empty { display: none; } /* 데이터 로딩 전 플래스홀더 — 주의 깊게 */
- :empty만으로 “로딩 중” UI를 만들면 스크롤/CLS 이슈가 생길 수 있어, 높이는 별도 설계.
16. 실전 패턴 — 선택자 조합 예제
16.1 카드 리스트(호버+포커스)
<a class="card" href="…">
<h3 class="card__title">제목</h3>
<p class="card__meta">2026-04-16</p>
</a>
.card { display: block; border: 1px solid #e5e7eb; }
.card:hover .card__title,
.card:focus-visible .card__title { text-decoration: underline; }
- 이유: 키보드 사용자 동등—
:hover만이면 불완전.
16.2 테이블 스트라이프 + thead 고정(개념)
tr:nth-child(odd td)X — tr에 nth가 일반적.- sticky thead는 position과 함께 설계.
16.3 네비 “현재 항”
- aria-current=“page” +
[aria-current="page"]조합이 시맨틱+스타일 동시에 깔끔.
nav a[aria-current='page'] { font-weight: 700; }
16.4 조건부 레이아웃(:has)
/* 메인이 사이드바를 “갖는” 2칸 */
.layout:has(.sidebar) { display: grid; grid-template-columns: 240px 1fr; }
- 접근/폴백:
:has미지원 시, classlayout--with-sidebar를 JS로 동기화하는 팀도 많다.
16.5 fieldset:disabled와 하위 input — 그룹 비활성
/* fieldset disabled 시 그룹 전체가 :disabled (UA·명세/지원 [확인](https://html.spec.whatwg.org/)) */
fieldset:disabled input { cursor: not-allowed; }
- :disabled는 속성·조상 fieldset 상속 맥락과 함께 읽는다(§ 13). “왜 회색인가” 디버깅 시 HTML 구조를 먼저 본다.
16.6 체크가 뒤에 있는 마크업 + 인접 결합자
<label class="row">
<input type="checkbox" />
<span>동의</span>
</label>
input[type='checkbox']:checked + span { font-weight: 600; }
+제약이 빡빡하면label:has(:checked) span(§ 10) 또는 class 토글이 견고하다(프레임워크 이벤트·vDOM 재배치 주의).
16.7 스스로 말하는 data-state (조합)
data-state="error" 또는 data-state="error loading"처럼 공백 구분 토큰을 쓰면, [data-state~='error']로 일부 상태만 집는 선택자를 쓸 수 있다(§ 3.4, § 3.2의 [attr~="t"]와 동일 패턴).
[data-state~='error'] { border-color: #b91c1c; }
[data-state~='error'] .hint { color: #b91c1c; }
- 흔한 실수: 대괄호 안
attr이름 앞뒤에 스페이스를 넣지 않는다([ data-state ]형태는 문법·팀에 따라 다름—ESLint/Stylelint로 고정 권장). - 팀에선
data-ui="error"단일 값, 또는 BEMblock--error한 토큰으로 고정해 쿼리·가독성 둘을 맞추는 경우가 많다.
17. 성능 고려사항 — 선택자 “속도”
엔진이 규칙을 어떻게 최적화하든, 체감이 나쁜 순간은 비슷하다. 개인적으로 가장 기억에 남는 건, 가상 스크롤로 DOM을 갈아끼우는 테이블에서 hover·:nth-child·광범위한 형제 결합자가 엮이며 Recalculate Style이 프레임을 잡아먹던 케이스다. “선택자 한 줄”이 범인이라기보다, 같은 요소에 후보 규칙이 잔뜩 쌓이고(특이도 전쟁), 그 상자가 빈번히 바뀌며 의존 그래프를 넓게 무효화하는 조합이 문제였다. Chrome Performance에서 초록 막대(스타일)이 길게 늘어질 때, 저는 먼저 선택자보다 왜 이 노드가 이렇게 자주 쓸모없이 다시 잡히지? 쪽을 본다.
main 꼭대기에 전역에 가까운 [class*='…'] 같은 부분일치를 얹은 채, 아래 가상 리스트의 행·셀이 계속 교체되는 화면도 흔한 악몽이다. “전체 문서”를 훑는 매칭은 느슨할수록 위험하다. 스코프(§ 2.6)를 section·[data-v-root]·컴포넌트 루트로 쪼개지 않은 채, 그 위에 :has를 몇 겹 씌우면(§ 10.1) 스타일 엔진이 “이 DOM 변화가 저 조상/형제 규칙에도 불을 켤까?”를 자주 점검한다. Shadow·웹 컴포넌트는 오히려 그 범위를 뚜껑 닫아 주는 쪽이어서, 제품이 컴포넌트로 잘 쪼개질수록(의도적이든 아니든) 선택자 설계의 부담이 점점 줄어든다.
transition·:hover를 뿌리듯 쓰면, 마우스만 움직여도 불필요한 재계산이 쌓이기 쉽다. 실무에선 is-open 같은 class 한 방이 의존 그래프를 얇게 만드는 경우가 더 많았다. 다만 언제나 class가 이긴다는 뜻은 아니다—프로파일을 한 번 돌려 보면 Lighthouse가 아니라 Performance/Rendering·가능하면 RUM으로 “긴 Long Task”가 어떤 상호작용에서 붙는지 잡는 편이 낫다. 팀이 Stylelint selector-max-*에 최대 깊이·id 남용·(필요 시) :has 금지를 올리는 이유는, 속도라기보다 계약을 CI에 박아 두기 위해서다. 리뷰에서 “이 문법, 우리 Browserslist에 진짜 괜찮지?”는 여전히 Can I use와 실기기 쪽에 답이 있다. :has는 PostCSS로 대체되지 않는다는 말(로직·class로 폴백)을 한 번씩은 README에 남기는 팀이 많다.
17.1 무효화(Invalidation)만 한 번 씹어보면
DOM의 한 노드가 바뀌면 브라우저는 “이 규칙 중 어느게 이제 이 요소·형제·조상에 영향이 있는가”를 다시 짠다. 결합자·:has·형제가 넓을수록 의존 그래프가 커진다. 스코프는 § 2.6, § 10.1을 참고해 줄이고, 상태는 data-state·is-* 한 출처에 두는 편이, 나중에 Performance를 다시 열 때 마음이 편하다.
18. 실전 팁
사람마다 취향이 갈리지만, 저는 베이스를 :where(…)로 특이도 0에 가깝게 두고(덮기 쉬움), 컴포넌트 쪽은 :is(…)로 묶되 베이스를 너무 쎄게 만들지 않는 선에서 타협하는 편이다. id는 이기는 쪽이지 읽힌다는 뜻은 아니어서, 루트·측정 앵커 말고는 손이 덜 가게 된다. !important는 정말 유틸 한 줄·접근성 비상, 써드파티 핫픽스—그 밖엔 CSS 기초의 @layer에 기대는 쪽이 다음 동료를 덜 울린다.
:focus-visible은 “키보드가 왔다 갔다하는 화면”의 기본 예의이고, outline: none 만 긋고 끝내면(대체 링 없이) WCAG에 찍히는 건 나뿐이 아니다. aria-current·[aria-pressed]·data-state로 현재 항을 색만으로 쓰지 말고, 라벨은 for/label·숨긴 보조 텍스트 쪽. 오류는 :invalid 만으로 첫 화면부터 붉게 쏟아 붓기보다, :user-invalid(지원이면)나 aria-invalid+class 팀 합의가 덜 살벌하다. 터치는 @media (hover: hover)로 호버 전용 장난감을 구분—손끝에서 :hover가 끈적이는 건 반응형 글의 옛 밈이 아니라, 여전히 현장 이슈다. DOM 역할·구조는 HTML 기본과 짝으로 본다.
19. 일반적 문제(트러블슈팅)
19.1 “선택자가 맞는 것 같은데 스타일이 안 먹는다”
- 캐스케이드 순서를 먼저 점검한다(§ 7, CSS 기초):
!important, @layer, 선언 순서, 인라인 스타일, 써드파티 스타일시트. - 특이도: DevTools Styles 패널의 취소선으로 어느 규칙이 이겼는지 본다.
- 상속·초기값:
color는 상속되지만,background는 기본이 투명이어서 “부모에 배경을 줬는데 자식이 안 보인다”는 착시가 생기기 쉽다.
19.2 :nth-child가 이상하다
- 형제 집단이 생각과 다를 수 있다. 중간에 텍스트 노드·주석·다른 태그가 끼면 n번째 자식이 달라진다.
nth-child는 1부터 센다. “0-based”로 읽는 습관이 있으면 오해가 난다.- :nth-of-type이나 :nth-child(An + B of S)(§ 12)로 전환해 보는 것이 해법인 경우가 많다.
19.3 ::before가 보이지 않는다
content가 없다. 장식이어도content: ''가 필요한 경우가 많다.- 부모(또는 자기 자신)가
display: none인 경우. overflow: hidden이나 포지셔닝으로 잘렸는지박스 모델과 함께 확인한다.
19.4 모바일에서 :hover가 “걸린” 것처럼 보인다
- 터치 기기는
:hover가 끈적하게 남는 경우가 있어,@media (hover: hover) { ... }로 호버 전용 효과를 제한하는 패턴이 흔하다(반응형).
19.5 Shadow DOM 안·밖에서 스타일이 먹지 않는다
- :host,
::slotted()등 별도 문법이 있다. 이 글 범위를 넘으므로, 웹 컴포넌트·프레임워크 문서의 스타일 가이드를 따른다.
19.6 “특이도로 이겼는데” @layer에 질 때
CSS 기초의 @layer는, 같은 캐스케이드 맥락에서 “어느 규칙 층이 먼저냐”를 특이도와 별도로 정하는 축이다(명세: CSS Cascade 5). !important가 섞이면 캐스케이드 정렬 표로 따로 읽어야 한다.
- 점검 순서(실무): (1)
@layer선언·@import순서 → (2) 동일 층 안의 특이도 → (3) 소스 순서 → (4)!important. base·components등 레이어로 층을 나눈 팀은, “베이스보다 유틸이 특이도만으로 이긴다”는 혼란을 줄이는 편이다(이길 이유는 특이도뿐 아니라 어느 레이어에 속했는지 먼저).
정리
CSS 선택자는 “이 선언이 어느 요소에 적용될지”를 정의하는 필터이며, 특이도·캐스케이드·필요 시 @layer와 한 덩어리로 읽어야 실무에서 예측이 가능하다. *, 타입, 클래스, id로 범위를 잡고, 속성·가상 클래스/요소·결합자로 세밀하게 조정한다. :is(), :where(), :has(), :not()은 읽기 쉬운 규칙과 낮은 베이스 특이도를 동시에 노리는 데 유용하다. !important와 과한 id는 “억지로 이기는” 쪽으로 빚이 쌓이기 쉬우니, 레이어와 클래스로 흐름을 잡는 편이 유지보수에 이롭다. 밤늦게 DevTools에 박힌 취소선을 혼자 응시하다 보면, 결국 선택자는 문법책이 아니라 팀이 같이 쓰는 약속이라는 생각이 든다. 다음은 CSS 박스 모델에서 상자 규칙을 고정하고, Flexbox·Grid로 배치를 강화하는 흐름이 자연스럽다.
더 읽기
- CSS 기초 | 선택자, 속성, 색상, 폰트
- HTML 기본 태그
- CSS 박스 모델
- W3C Selectors Level 4
- W3C CSS Cascade 5
FAQ(요약)
- :where()와 :is()의 차이는? :where()는 인자 선택자의 특이도를 0으로 만든다(베이스에 두고 덮기 쉬움). :is()는 인자 중 가장 높은 특이도를 반영한다.
- :has()는 성능에 영향이 있나? DOM이 크고 갱신이 잦을수록 스타일 재계산 비용이 커질 수 있다. 복잡한 조건은 상태 클래스·data-*로 옮기는 팀이 많다.
- 첫 번째 요소는
:first-child로 고르면 되나? 형제 집단에 다른 태그가 있으면, 첫 자식과 같은 타입의 첫 요소는 다를 수 있다(§ 12, :first-of-type 참고).