ToolPal
CSS code on a computer screen

CSS 명시도(Specificity) 완전 정복: 왜 내 스타일이 적용 안 될까?

📷 Negative Space / Pexels

CSS 명시도(Specificity) 완전 정복: 왜 내 스타일이 적용 안 될까?

CSS 명시도는 프론트엔드 개발자라면 반드시 이해해야 할 개념입니다. (A,B,C) 시스템의 원리와 !important 남용에서 벗어나는 방법을 실전 예제와 함께 알아봅니다.

2026년 4월 13일4분 소요

CSS를 처음 배울 때는 단순해 보입니다. 선택자를 쓰고, 속성을 정의하고, 브라우저가 적용한다. 그런데 어느 순간 분명히 맞게 쓴 것 같은 스타일이 전혀 적용되지 않는 경험을 하게 됩니다. DevTools를 열어보면 내 규칙에 줄이 그어져 있고, 어딘가에서 온 다른 선택자가 이기고 있습니다. 그래서 !important를 붙입니다. 일단 동작하니까.

6개월 후, 그 코드베이스는 !important투성이가 되어 있고 아무도 건드리기 무서워합니다.

CSS 명시도를 제대로 이해하면 이 상황을 피할 수 있습니다. 어렵지 않습니다. 규칙이 명확하고 논리적입니다. 다만 직관과 약간 다른 부분이 있어서 한 번은 제대로 짚고 넘어가야 합니다.

(A, B, C) 시스템 이해하기

CSS 명시도는 단일 숫자가 아니라 세 개의 숫자로 구성된 값입니다. (A, B, C) 형태로 표기하며, 왼쪽부터 비교합니다. A가 크면 그 규칙이 이기고, A가 같으면 B를 비교하고, B도 같으면 C를 비교합니다.

A — ID 선택자 #header, #main-content 같은 ID 선택자가 여기에 해당합니다. ID 하나가 (1, 0, 0)을 줍니다. 이 열 때문에 스타일링에 ID를 사용하는 것을 피하는 것이 현대 CSS의 일반적인 관행입니다. 한 번 쓰면 덮어쓰기가 매우 어려워집니다.

B — 클래스, 속성, 가상 클래스 클래스 선택자(.nav), 속성 선택자([type="text"]), 가상 클래스(:hover, :focus, :nth-child()) 등이 여기에 포함됩니다. 각각 B에 1씩 추가됩니다.

C — 타입 선택자, 가상 요소 div, p, ul 같은 요소 타입 선택자와 ::before, ::after 같은 가상 요소가 C에 해당합니다.

명시도가 없는 것들 * 유니버설 선택자, > + ~ 같은 결합자는 명시도가 0입니다. :where()도 내부 내용에 관계없이 항상 0입니다.

예제로 확인하기

/* (0, 0, 1) */
p { color: red; }

/* (0, 1, 0) */
.intro { color: blue; }

/* (0, 1, 1) */
.intro p { color: green; }

/* (1, 0, 0) */
#main { color: orange; }

/* (1, 1, 1) */
#main .content p { color: purple; }

(1, 1, 1)이 가장 높으니 마지막 규칙이 이깁니다. (0, 1, 1)은 언뜻 더 구체적으로 보이지만, 첫 번째 열에서 이미 (1, 0, 0)에게 집니다.

자주 하는 실수

"선택자가 길면 더 구체적이다"

흔히 있는 오해입니다. .nav .list .item a.link:hover는 꽤 길고 구체적으로 보이지만, 명시도는 (0, 4, 2)입니다. 반면 #nav는 단 하나의 선택자지만 (1, 0, 0)이어서 앞 선택자를 이깁니다.

"나중에 쓴 규칙이 항상 이긴다"

순서는 명시도가 완전히 같을 때만 의미가 있습니다. 명시도가 다르면 나중에 왔더라도 낮은 쪽이 집니다.

:not(), :is(), :has()의 명시도

:not()은 괄호 안의 인수 명시도를 그대로 가져갑니다. :not(.hidden).hidden 덕분에 (0, 1, 0)입니다.

:is()도 비슷하게 괄호 안에서 명시도가 가장 높은 인수를 취합니다. :is(#header, .nav, p)#header 때문에 (1, 0, 0)이 됩니다. 이 부분은 실수하기 쉽습니다.

:where()는 항상 0입니다. 그래서 베이스 스타일에 :where()를 감싸두면 소비하는 쪽에서 쉽게 덮어쓸 수 있습니다.

실전 디버깅 예제

서드파티 UI 라이브러리를 사용할 때 가장 자주 발생하는 상황입니다.

/* 라이브러리 */
.ui-btn.ui-btn--primary {
  background-color: #0066cc;
}

/* 내 오버라이드 */
.my-button {
  background-color: #ff5500;
}

라이브러리: (0, 2, 0). 내 규칙: (0, 1, 0). 내 스타일이 집니다.

이를 해결하는 올바른 방법들:

  1. 같은 선택자를 이어쓰기: .my-button.my-button — 같은 클래스를 두 번 써서 (0, 2, 0)으로 맞추기
  2. 부모 컨텍스트 추가: .my-app .my-button(0, 2, 0)
  3. !important — 최후의 수단으로만

정석은 라이브러리 쪽이 :where()로 베이스 스타일을 감싸서 명시도를 0으로 유지하는 것이지만, 내가 통제할 수 없는 라이브러리라면 방법 1이나 2가 현실적입니다.

CSS 명시도 계산기 활용하기

머릿속으로 (A, B, C)를 계산하는 것은 처음에 익숙하지 않습니다. CSS 명시도 계산기를 사용하면 선택자를 붙여넣기만 하면 즉시 결과를 볼 수 있습니다.

실용적인 사용 시나리오:

  • 충돌하는 두 선택자 비교: 왜 내 스타일이 안 먹히는지 1초 안에 확인
  • 새 선택자 작성 전 검토: 선택자를 쓰기 전에 넣어보고 명시도가 의도대로인지 확인
  • 학습 목적: 선택자를 수정하면서 숫자가 어떻게 바뀌는지 직접 보는 것이 가장 빠른 학습법

계산기의 한계

대부분의 실무 선택자는 정확하게 계산합니다. 다만 :is() 안에 복잡하게 중첩된 표현식이나 Shadow DOM 관련 선택자(::slotted(), ::part())는 엣지 케이스에서 완벽하지 않을 수 있습니다. 특수한 상황에서는 브라우저 DevTools로 최종 확인하는 것이 좋습니다.

명시도를 낮게 유지하는 CSS 작성법

버그를 고치는 것보다 처음부터 문제가 안 생기게 작성하는 것이 더 좋습니다. 명시도를 낮게 유지하는 몇 가지 원칙을 소개합니다.

ID 선택자는 스타일링에 사용하지 않기 ID는 JavaScript 훅이나 앵커로 사용하되, CSS 선택자로는 쓰지 않는 것이 권장됩니다. 한 번 사용하면 (1, 0, 0)이 생겨서 이후 모든 오버라이드가 까다로워집니다.

선택자는 가능한 짧게 .nav-linknav ul li a.nav-link보다 좋습니다. 짧은 선택자는 명시도가 낮을 뿐만 아니라 마크업이 바뀌어도 덜 깨집니다.

BEM 같은 네이밍 컨벤션 도입 .card__title--large 형태로 클래스를 단독으로 사용하면 거의 모든 선택자가 (0, 1, 0)의 일정한 명시도를 가집니다.

유틸리티 클래스에만 !important 허용 .sr-only, .hidden 같이 항상 이겨야 하는 유틸리티 클래스는 !important가 정당합니다. 그 외의 경우에는 다시 생각해보는 것이 좋습니다.

관련 도구

명시도를 이해했다면 CSS 작업을 더 효율적으로 만드는 도구들도 함께 활용해보세요.


CSS 명시도는 한 번 제대로 이해하면 계속 투자 수익이 돌아오는 개념입니다. !important 도배에서 벗어나고, 선택자가 짧고 깔끔해지고, 왜 스타일이 안 먹히는지 바로 알 수 있게 됩니다. CSS 명시도 계산기를 디버깅 도구로 옆에 두고 활용하세요.

자주 묻는 질문

이 글 공유하기

XLinkedIn

관련 글