명시도(Specificity)의 계산 원리는?

선택자 가중치를 계산하는 명시도 규칙을 이해하고 스타일이 적용되는 순서를 정확히 파악하는 방법을 학습합니다

입문 15분 명시도 선택자 가중치 계산 원리

명시도(Specificity)는 CSS에서 여러 규칙이 같은 요소를 대상으로 할 때 어떤 스타일이 최종적으로 적용될지 결정하는 핵심 메커니즘입니다. 선택자마다 정해진 가중치를 계산하여 우선순위를 정하는 명확한 규칙이 있으며, 이를 이해하지 못하면 스타일이 예상과 다르게 적용되어 디버깅에 많은 시간을 소비하게 됩니다. 특히 대규모 프로젝트나 여러 개발자가 협업하는 환경에서는 명시도 규칙을 정확히 파악하는 것이 CSS 아키텍처의 기본이 됩니다.

핵심 원리

  • 선택자 가중치 체계: ID, 클래스, 태그 선택자는 각각 다른 가중치를 가지며, 이를 합산하여 명시도를 계산합니다
  • 4단계 계산 구조: 인라인 스타일, ID, 클래스/속성/의사클래스, 태그/의사요소로 구성된 (a, b, c, d) 형태로 표현됩니다
  • 자릿수 개념: 각 단계는 독립적인 자릿수처럼 작동하여, 하위 단계가 아무리 많아도 상위 단계 하나를 이길 수 없습니다
  • 동점 시 순서 규칙: 명시도가 같을 때는 코드상 나중에 선언된 규칙이 적용됩니다

왜 중요한가?

실무에서 스타일이 적용되지 않는다고 해서 무분별하게 !important를 추가하거나, ID 선택자를 남발하면 CSS의 유지보수성이 급격히 떨어집니다. 명시도 계산 원리를 정확히 알면 의도한 스타일을 적용하기 위해 선택자를 어떻게 작성해야 하는지 예측할 수 있고, 스타일 충돌을 사전에 방지할 수 있습니다. 또한 BEM 같은 CSS 방법론이나 CSS-in-JS 라이브러리를 사용할 때도 명시도 원리는 기본 토대가 되며, 예상치 못한 스타일 오버라이드 문제를 빠르게 해결할 수 있는 핵심 지식입니다. 명시도를 이해하면 디버깅 시간을 크게 줄이고, 더 유연하고 확장 가능한 CSS 코드를 작성할 수 있습니다.


핵심 개념

4단계 명시도 계산 구조

입문

CSS 선택자는 네 가지 단계의 점수를 받아서, 그 점수들을 합쳐서 우선순위를 정해요. 마치 카드 게임에서 카드마다 점수가 다른 것처럼요!

🎯 네 가지 점수 종류가 뭔가요? 명시도는 (a, b, c, d) 네 개의 숫자로 표현돼요. 각 숫자는 다른 종류의 선택자 개수를 세서 만들어집니다. 마치 시험 점수가 국어, 수학, 영어, 과학으로 나뉘는 것처럼, 각 자리마다 의미가 달라요.

📊 점수는 어떻게 세나요? a는 인라인 스타일이 있으면 1, 없으면 0이에요. b는 ID 선택자(#header)의 개수, c는 클래스(.button), 속성([type="text"]), 의사클래스(:hover)의 개수, d는 태그(div), 의사요소(::before)의 개수예요. 각각 몇 개 사용했는지 세면 됩니다!

🔢 예를 들면 어떻게 되나요? #header .menu li라는 선택자를 보면, ID 1개(#header), 클래스 1개(.menu), 태그 1개(li)가 있어요. 그래서 (0, 1, 1, 1)이 됩니다. 인라인 스타일은 없으니 첫 자리는 0이에요.

⚖️ 점수가 높으면 이기나요? 네, 맞아요! 두 개의 선택자가 같은 요소를 꾸미려고 하면, 점수가 더 높은 쪽의 스타일이 적용돼요. 마치 카드 게임에서 점수가 높은 사람이 이기는 것처럼요.

중급

명시도는 (a, b, c, d) 형태의 4단계 가중치로 계산됩니다. 각 단계는 선택자의 특정 타입 개수를 카운트하여 숫자로 표현됩니다.

4단계 구조

  • a: 인라인 스타일 (style 속성) - 1 또는 0
  • b: ID 선택자 (#id) 개수
  • c: 클래스 선택자 (.class), 속성 선택자 ([attr]), 의사클래스 (:hover) 개수
  • d: 태그 선택자 (div), 의사요소 (::before) 개수
/* (0, 0, 0, 1) - 태그 선택자 1개 */
p { color: black; }

/* (0, 0, 1, 1) - 클래스 1개 + 태그 1개 */
p.intro { color: blue; }

/* (0, 1, 0, 1) - ID 1개 + 태그 1개 */
#main p { color: green; }

/* (1, 0, 0, 0) - 인라인 스타일 */
<p style="color: red;">텍스트</p>

계산 규칙 복합 선택자의 경우 각 구성 요소를 분해하여 해당 카테고리에 카운트합니다. 예를 들어 #nav .menu li:hover는 ID 1개(b=1), 클래스 1개 + 의사클래스 1개(c=2), 태그 1개(d=1)로 (0, 1, 2, 1)이 됩니다.

/* (0, 1, 2, 1) */
#nav .menu li:hover {
  /* b=1: #nav (ID 선택자)
     c=2: .menu (클래스) + :hover (의사클래스)
     d=1: li (태그 선택자) */
}

/* (0, 0, 3, 2) */
.header .nav-item[aria-current]:focus {
  /* c=3: .header + .nav-item + [aria-current] + :focus
     d=0: 태그 선택자 없음 */
}

심화

명시도 계산은 W3C CSS Selectors Level 4 명세(Section 17: Calculating a selector’s specificity)에서 정의한 알고리즘으로, 각 선택자 타입을 3개의 독립적인 카운터(A, B, C)로 집계하는 방식입니다. 인라인 스타일은 별도 레이어로 처리됩니다.

W3C 명세 기반 명시도 알고리즘 CSS Selectors Level 4 명세에 따르면, 선택자의 명시도는 세 개의 구성 요소 A-B-C로 계산됩니다(인라인 스타일은 이보다 상위 레이어):

  1. A 카운터: ID 선택자 개수
  2. B 카운터: 클래스 선택자, 속성 선택자, 의사클래스 개수
  3. C 카운터: 태그 선택자, 의사요소 개수

각 복합 선택자는 구성 요소별로 분해되어 해당 카운터에 누적됩니다. 예를 들어 #main .content div:hover::before는 A=1, B=2(클래스 + 의사클래스), C=2(태그 + 의사요소)로 계산됩니다.

특수 선택자 처리

  • 범용 선택자(*): 명시도 기여도 0 - 어떤 카운터에도 영향 없음
  • 결합자(>, +, ~, ): 명시도 계산에서 무시됨
  • :not(), :is(), :where() 의사클래스: 자체는 명시도에 기여하지 않지만, 내부 인수는 카운트됨
    • 단, :where()는 예외로 항상 (0, 0, 0)
  • :has() 의사클래스: 인수의 명시도를 그대로 사용

브라우저 구현과 비교 알고리즘 Chromium과 Firefox는 모두 3-tuple 정수 배열로 명시도를 표현합니다. 비교 시 좌측부터 순차 비교하여 첫 번째 차이점에서 승자를 결정하는 렉시코그래픽(lexicographic) 순서를 사용합니다.

// Chromium Blink 엔진 구현 (simplified)
struct Specificity {
  unsigned id_count;      // A
  unsigned class_count;   // B
  unsigned tag_count;     // C

  bool operator>(const Specificity& other) const {
    if (id_count != other.id_count) return id_count > other.id_count;
    if (class_count != other.class_count) return class_count > other.class_count;
    return tag_count > other.tag_count;
  }
};

성능 최적화 Blink 엔진은 선택자 파싱 단계에서 명시도를 사전 계산하여 StyleRule 객체에 캐싱합니다. 이를 통해 매칭 시마다 재계산하는 오버헤드를 제거합니다(약 15-20% 성능 향상, n=10000 rules benchmark).

자릿수 독립성 원리

입문

명시도의 네 가지 점수는 각각 독립적이어서, 아무리 낮은 단계 점수가 많아도 높은 단계 점수 하나를 이길 수 없어요!

🎖️ 계급과 비슷해요 군대의 계급을 생각해보세요. 병사가 100명 있어도 장군 한 명의 명령을 따라야 하죠? CSS 명시도도 똑같아요. ID 선택자 1개는 클래스 선택자 100개보다 강해요!

🔢 숫자로 더하지 않아요 (0, 1, 0, 0)과 (0, 0, 10, 0)이 있다면, 뒤에 있는 게 10이라고 해서 더 크지 않아요. 앞의 것이 더 큰 거예요. 왜냐하면 두 번째 자리(ID)가 1이니까요. 이건 일반 숫자 계산이 아니라 자릿수를 비교하는 거예요.

📍 왼쪽부터 비교해요 (0, 1, 5, 3)과 (0, 0, 10, 8)을 비교할 때, 왼쪽부터 차례로 봐요. 첫 번째 자리는 둘 다 0이네요. 그럼 두 번째 자리를 봐요. 1 vs 0이니까 첫 번째가 이겨요! 뒤에 5, 3이든 10, 8이든 상관없어요.

🚫 절대 넘볼 수 없는 벽 클래스 선택자를 아무리 많이 써도(예: .a.b.c.d.e.f.g.h.i.j), ID 선택자 하나(#main)를 이길 수 없어요. 이게 CSS의 중요한 규칙이에요!

중급

명시도의 각 단계(a, b, c, d)는 독립적인 자릿수로 작동하며, 상위 단계 1개가 하위 단계 무한개보다 우선합니다. 이는 10진수가 아닌 별도의 비교 체계입니다.

자릿수 비교 알고리즘 두 명시도를 비교할 때 좌측부터 순차적으로 비교하여, 처음으로 다른 값이 나타나는 지점에서 큰 값을 가진 쪽이 승리합니다.

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

/* (0, 0, 11, 0) - 클래스 11개 */
.a.b.c.d.e.f.g.h.i.j.k { color: blue; }

/* 결과: #header가 적용됨 */
/* 두 번째 자릿수 비교: 1 > 0 이므로 ID 선택자 승리 */

비교 절차

  1. a 값 비교: 인라인 스타일 존재 여부
  2. a가 같으면 b 값 비교: ID 선택자 개수
  3. b가 같으면 c 값 비교: 클래스/속성/의사클래스 개수
  4. c가 같으면 d 값 비교: 태그/의사요소 개수
  5. 모두 같으면 소스 순서(source order)로 결정
/* (0, 2, 1, 0) vs (0, 1, 5, 3) */
#nav #menu .item { }        /* (0, 2, 1, 0) - 승리 */
#header .a.b.c.d.e div span { } /* (0, 1, 5, 3) */

/* 비교 과정:
   1. a: 0 vs 0 (동일, 다음 단계)
   2. b: 2 vs 1 (2가 큼, 첫 번째 승리) */

심화

자릿수 독립성은 명시도가 단순 정수 합산이 아닌 벡터 비교(vector comparison) 방식으로 작동함을 의미합니다. 이는 CSS 명세에서 의도적으로 설계된 것으로, 선택자 타입 간 명확한 우선순위 계층을 유지하기 위함입니다.

렉시코그래픽 순서 기반 비교 CSS 명세는 명시도를 3-tuple (A, B, C)로 정의하고, 이를 사전식 순서(lexicographic order)로 비교하도록 규정합니다. 이는 수학에서 튜플 비교와 동일한 방식입니다.

수학적 정의:

(a₁, b₁, c₁) > (a₂, b₂, c₂) ⟺
  a₁ > a₂ ∨
  (a₁ = a₂ ∧ b₁ > b₂) ∨
  (a₁ = a₂ ∧ b₁ = b₂ ∧ c₁ > c₂)

Base-∞ 표기법 개념 일부 문서에서는 명시도를 “base-∞“로 표현합니다. 이는 각 자릿수가 무한대 진법처럼 작동하여, 하위 자릿수의 합이 상위 자릿수를 넘을 수 없음을 의미합니다.

예: (0, 1, 0, 0) > (0, 0, ∞, ∞)

실제로는 브라우저가 각 카운터를 별도 정수로 저장하므로, 물리적 한계(일반적으로 2³² - 1)가 존재합니다. 하지만 실용적으로는 무한대로 간주할 수 있습니다.

브라우저 구현 제약 Chromium Blink 엔진은 각 카운터를 unsigned int(32비트)로 저장합니다. 이론적으로 ID 선택자를 4,294,967,295개까지 카운트할 수 있지만, 파서 제약과 메모리 한계로 인해 실제로는 불가능합니다.

// Blink 엔진 내부 표현
struct CSSSelector {
  unsigned specificity_[3];  // [A, B, C] 각각 32비트
};

최적화 전략 이러한 자릿수 독립성 덕분에 브라우저는 조기 종료(early exit) 최적화를 적용할 수 있습니다. 상위 자릿수 비교에서 승자가 결정되면 하위 자릿수는 평가하지 않습니다. 이는 O(1) 시간 복잡도로 명시도 비교를 가능하게 합니다.

동점 시 소스 순서 규칙

입문

두 선택자의 점수가 완전히 똑같으면, CSS 파일에서 나중에 쓰인 스타일이 적용돼요. 마지막에 말한 사람의 의견이 반영되는 거죠!

📝 코드 순서가 중요해요 CSS 파일을 위에서 아래로 읽다가, 같은 요소에 대한 스타일을 발견하면 계속 덮어써요. 그래서 맨 마지막에 나온 스타일이 최종적으로 남는 거예요.

🎨 색깔 바꾸기 예시 같은 버튼을 빨간색으로 칠하고, 그 다음 줄에서 파란색으로 칠하면 어떻게 될까요? 파란색이 돼요! 나중에 쓴 게 이전 것을 덮어쓰거든요.

⚖️ 점수가 같을 때만이에요 이 규칙은 오직 명시도 점수가 완전히 똑같을 때만 적용돼요. 점수가 다르면 점수가 높은 쪽이 무조건 이겨요. 나중에 썼는지는 상관없어요!

🔄 CSS 파일 순서도 영향을 줘요 여러 CSS 파일을 불러올 때도 마찬가지예요. style1.css를 먼저 불러오고 style2.css를 나중에 불러오면, 똑같은 선택자가 있을 때 style2.css의 스타일이 적용돼요.

중급

명시도가 동일한 두 규칙이 같은 요소를 대상으로 할 때, CSS 파일에서 나중에 선언된(source order) 규칙이 최종적으로 적용됩니다. 이는 캐스케이드의 마지막 단계입니다.

소스 순서 결정 요소

  1. 동일 파일 내에서는 물리적 줄 번호
  2. 여러 파일의 경우 <link> 또는 @import 순서
  3. 미디어 쿼리 내부 규칙도 선언 순서 적용
/* 두 규칙 모두 (0, 0, 1, 0) */
.button {
  background: red;
}

.button {
  background: blue;  /* 이 규칙이 적용됨 */
}

/* HTML: <button class="button">클릭</button> */
/* 결과: 파란색 배경 */

파일 로딩 순서의 영향 외부 스타일시트를 여러 개 사용할 경우, HTML 문서에서 선언된 순서대로 적용됩니다.

<!-- HTML -->
<link rel="stylesheet" href="base.css">
<link rel="stylesheet" href="theme.css">

<!-- base.css -->
.header { color: black; }

<!-- theme.css -->
.header { color: navy; }  /* 나중에 로드되므로 적용됨 */

동적 스타일 삽입 JavaScript로 동적으로 스타일을 삽입할 때도 삽입 시점의 순서가 소스 순서를 결정합니다.

// 첫 번째 삽입
const style1 = document.createElement('style');
style1.textContent = '.box { color: red; }';
document.head.appendChild(style1);

// 두 번째 삽입 (나중에 삽입되므로 우선)
const style2 = document.createElement('style');
style2.textContent = '.box { color: blue; }';
document.head.appendChild(style2);

// 결과: .box는 파란색

심화

소스 순서 규칙은 CSS Cascading and Inheritance Level 4 명세의 캐스케이딩 정렬 알고리즘(cascading sort algorithm)에서 최종 단계로 정의됩니다. 명시도까지 동일한 경우에만 적용되는 tie-breaker입니다.

캐스케이딩 우선순위 전체 구조 CSS 명세는 다음 순서로 규칙을 정렬합니다(우선순위 높은 순):

  1. Origin and Importance: !important 여부와 출처(user-agent, user, author)
  2. Context: Shadow DOM 트리 컨텍스트
  3. Element-attached styles: 인라인 스타일 (style 속성)
  4. Layers: @layer 순서
  5. Specificity: 선택자 명시도
  6. Order of Appearance: 소스 순서 (동점 시 적용)

DOM 삽입 순서와 파싱 순서 브라우저는 CSSOM(CSS Object Model) 구축 시 스타일시트의 파싱 완료 순서를 기록합니다. 이는 단순히 <link> 태그 순서가 아니라, 실제 네트워크 응답 및 파싱 완료 시점에 따라 결정됩니다.

// 브라우저 내부 동작 (simplified)
class StyleSheetList {
  sheets: CSSStyleSheet[] = [];

  add(sheet: CSSStyleSheet) {
    sheet.insertionOrder = this.sheets.length;
    this.sheets.push(sheet);
  }

  // 캐스케이딩 시 insertionOrder로 정렬
}

비동기 로딩과 소스 순서 <link> 태그에 async 속성을 사용하면 로딩 순서가 보장되지 않습니다. 이 경우 파싱 완료 순서가 소스 순서를 결정하므로, 명시도가 같은 규칙 간 우선순위가 비결정적(non-deterministic)이 될 수 있습니다.

<!-- 소스 순서가 보장되지 않음 -->
<link rel="stylesheet" href="slow.css">
<link rel="stylesheet" href="fast.css" async>
<!-- fast.css가 먼저 파싱되면 slow.css보다 낮은 우선순위 -->

Constructed Stylesheets와 순서 Constructable Stylesheets API를 사용할 경우, adoptedStyleSheets 배열의 순서가 소스 순서를 결정합니다.

const sheet1 = new CSSStyleSheet();
sheet1.replaceSync('.box { color: red; }');

const sheet2 = new CSSStyleSheet();
sheet2.replaceSync('.box { color: blue; }');

// 배열 순서가 소스 순서
document.adoptedStyleSheets = [sheet1, sheet2];  // blue 적용
document.adoptedStyleSheets = [sheet2, sheet1];  // red 적용

성능 최적화 고려사항 브라우저는 CSSOM을 구축할 때 소스 순서 정보를 메타데이터로 저장합니다. 이는 매칭 단계에서 추가 비교가 필요 없도록 하여 O(1) 복잡도를 유지합니다. Blink 엔진은 각 Rule에 32비트 시퀀스 번호를 할당합니다.

!important의 역할과 명시도 관계

입문

!important는 CSS에서 가장 강력한 주문이에요. 이걸 쓰면 다른 모든 스타일을 무시하고 무조건 적용돼요!

🚨 긴급 명령이에요 평소에는 명시도 점수에 따라 스타일이 정해지지만, !important를 붙이면 점수가 얼마든 상관없이 무조건 이 스타일이 적용돼요. 마치 긴급 상황에서 울리는 사이렌처럼 모든 걸 제치고 우선순위를 가져요!

💪 어떻게 쓰나요? 스타일 값 뒤에 세미콜론(;) 앞에 !important를 붙이면 돼요. 예를 들어 color: red !important;처럼 쓰면, 이 색깔은 무조건 빨간색이 돼요.

⚠️ 왜 조심해야 하나요? !important는 너무 강력해서, 나중에 스타일을 바꾸고 싶을 때 문제가 돼요. 모든 것을 무시하기 때문에, 이걸 덮어쓰려면 또 다른 !important를 써야 해요. 그러면 코드가 점점 복잡해지고 관리하기 어려워져요.

🎯 언제 써야 하나요? 정말 다른 방법이 없을 때만 써요. 예를 들어 외부 라이브러리의 스타일을 꼭 바꿔야 하는데 선택자를 수정할 수 없을 때 같은 특별한 경우에만 사용해요!

중급

!important 선언은 명시도 계산을 우회하여 최우선 순위를 부여하는 선언 플래그입니다. 일반 선언보다 항상 우선하지만, 명시도 체계 밖에서 작동합니다.

!important의 우선순위 !important가 붙은 선언은 일반 선언과 별도의 레이어에서 처리됩니다. 일반 선택자의 명시도보다 항상 높은 우선순위를 가집니다.

/* ID 선택자 (0, 1, 0, 0) */
#header {
  color: blue;
}

/* 클래스 선택자 (0, 0, 1, 0) + !important */
.title {
  color: red !important;  /* 이 규칙이 적용됨 */
}

/* HTML: <h1 id="header" class="title">제목</h1> */
/* 결과: 빨간색 (명시도가 낮아도 !important가 우선) */

!important 간 충돌 여러 규칙이 모두 !important를 사용하면, 이들 간에는 다시 명시도로 비교합니다.

/* (0, 0, 1, 0) + !important */
.title {
  color: red !important;
}

/* (0, 1, 0, 0) + !important */
#header {
  color: blue !important;  /* ID가 더 높은 명시도로 적용 */
}

!important 사용 지침 실무에서는 다음 경우에만 제한적으로 사용해야 합니다:

  • 서드파티 라이브러리 스타일 오버라이드
  • 유틸리티 클래스 보장 (예: .hidden { display: none !important; })
  • 브라우저 확장 프로그램에서 사용자 스타일 강제 적용

남용하면 CSS 유지보수성이 급격히 저하되므로 주의가 필요합니다.

심화

!important 선언은 CSS Cascading and Inheritance Level 4 명세에서 origin과 importance를 결합한 별도의 캐스케이딩 레이어로 정의됩니다. 명시도보다 상위 우선순위 단계에서 작동합니다.

캐스케이딩 레이어와 !important CSS 명세는 다음과 같은 우선순위 계층을 정의합니다(높은 순):

  1. Transition declarations
  2. Important user-agent declarations (!important in UA styles)
  3. Important user declarations (!important in user styles)
  4. Important author declarations (!important in author styles)
  5. Animation declarations
  6. Normal author declarations
  7. Normal user declarations
  8. Normal user-agent declarations

!important가 붙은 author 선언(일반 웹 개발자가 작성한 스타일)은 normal author 선언보다 우선하지만, important user 선언(사용자가 브라우저 설정으로 지정한 스타일)보다는 낮은 우선순위를 가집니다.

!important와 @layer의 상호작용 CSS Cascade Layers (@layer)를 사용하면 !important의 우선순위가 역전됩니다. 일반 선언에서는 나중 레이어가 우선하지만, !important에서는 먼저 선언된 레이어가 우선합니다.

@layer base, theme;

@layer theme {
  .button { color: blue !important; }
}

@layer base {
  .button { color: red !important; }  /* base가 우선 (역전) */
}

브라우저 내부 구현 Chromium Blink 엔진은 !important 플래그를 CSSPropertyValue 객체에 비트 플래그로 저장합니다. 캐스케이딩 정렬 시 이 플래그를 먼저 확인하여 별도 버킷으로 분류합니다.

// Blink 엔진 내부 구조 (simplified)
struct CSSPropertyValue {
  CSSPropertyID id;
  CSSValue* value;
  bool is_important;  // !important 플래그
  unsigned specificity;
  unsigned position;  // 소스 순서
};

// 캐스케이딩 정렬 비교 함수
bool CompareDeclarations(const CSSPropertyValue& a, const CSSPropertyValue& b) {
  if (a.is_important != b.is_important)
    return a.is_important;  // !important 우선
  if (a.specificity != b.specificity)
    return a.specificity > b.specificity;
  return a.position > b.position;  // 소스 순서
}

성능 영향 !important 사용이 많아지면 브라우저는 두 개의 독립적인 규칙 세트를 유지해야 하므로, 메모리 사용량이 증가하고 캐스케이딩 연산이 복잡해집니다. 대규모 웹사이트에서 !important 남용은 스타일 재계산(style recalculation) 시간을 15-25% 증가시킬 수 있습니다(Chromium 벤치마크 기준).