Cascading and inheritance/Specificity

김동현·2026년 3월 18일

mdn 학습 번역 - CSS

목록 보기
52/190

명시도 (Specificity)

안녕하세요! 프론트엔드 개발의 세계에 오신 것을 환영합니다. 오늘 배울 내용은 CSS를 작성하다 보면 누구나 한 번쯤 겪게 되는 "도대체 왜 내 스타일이 안 먹히는 거야?!"라는 좌절감을 해결해 줄 마법의 열쇠, 바로 명시도(Specificity)입니다.

명시도(Specificity)는 브라우저가 특정 요소에 가장 '적절한(관련성 높은)' CSS 선언이 무엇인지 결정할 때 사용하는 알고리즘입니다. 이 결정에 따라 요소에 최종적으로 적용될 속성값이 정해지죠. 명시도 알고리즘은 CSS 선택자(selector)의 가중치(weight)를 계산해서, 서로 충돌하는 CSS 선언들 중 어떤 규칙을 요소에 적용할지 판가름합니다.

참고: 브라우저는 명시도를 계산하기 전에 캐스케이드 출처와 중요도(cascade origin and importance)를 먼저 따집니다. 다시 말해, 서로 경쟁하는 속성 선언이 있을 때 명시도는 우선순위가 가장 높은 단일 캐스케이드 출처 및 레이어 내에 있는 선택자들 사이에서만 의미가 있고 비교됩니다. 스코프 근접성(Scoping proximity)과 코드 작성 순서(order of appearance)는 우선순위를 가진 캐스케이드 레이어 내에서 경쟁하는 선언들의 선택자 명시도가 완전히 똑같을 때만 승패를 가르는 기준이 됩니다.

💡 강사님의 꿀팁: 명시도는 일종의 '점수 대결'이라고 생각하시면 편합니다! CSS 파일 여기저기서 하나의 요소를 꾸미겠다고 나설 때, 브라우저는 이 명시도 점수를 계산해서 가장 점수가 높은 스타일을 적용해 주는 거랍니다.


명시도는 어떻게 계산되나요? (How is specificity calculated?)

명시도는 주어진 CSS 선언에 적용될 가중치(weight)를 계산하는 알고리즘입니다. 이 가중치는 요소(또는 의사 요소)와 일치하는 선택자 내에 있는 각 가중치 카테고리별 선택자의 개수에 따라 결정됩니다. 만약 동일한 요소에 서로 다른 속성값을 제공하는 선언이 두 개 이상 있다면, 가장 높은 알고리즘 가중치를 가진 선택자의 스타일 블록 내 선언 값이 적용됩니다.

명시도 알고리즘은 기본적으로 세 가지 유형의 선택자에 해당하는 ID, CLASS, TYPE이라는 세 가지 카테고리(또는 가중치)를 가진 '3열(three-column) 값' 시스템입니다. 이 값은 각 가중치 카테고리별 선택자 구성 요소의 개수를 나타내며, 보통 ID - CLASS - TYPE 형태로 표기합니다. 이 세 개의 열(column)은 요소와 일치하는 선택자들 내에서 각 카테고리에 해당하는 구성 요소의 개수를 세어 만들어집니다.


선택자 가중치 카테고리 (Selector weight categories)

선택자 가중치 카테고리를 명시도가 높은 순서대로 나열하면 다음과 같습니다:

ID 열 (ID column)

#example과 같은 ID 선택자만 포함합니다. 일치하는 선택자 내에 ID가 하나 있을 때마다 가중치 값에 1-0-0을 더합니다.

CLASS 열 (CLASS column)

.myClass와 같은 클래스 선택자, [type="radio"][lang|="fr"]과 같은 속성(attribute) 선택자, 그리고 :hover, :nth-of-type(3n), :required와 같은 의사 클래스(pseudo-classes)를 포함합니다. 일치하는 선택자 내에 클래스, 속성 선택자, 의사 클래스가 하나 있을 때마다 가중치 값에 0-1-0을 더합니다.

TYPE 열 (TYPE column)

p, h1, td와 같은 타입 선택자(태그 선택자)::before, ::placeholder 및 이중 콜론(::) 표기법을 사용하는 모든 의사 요소(pseudo-elements)를 포함합니다. 일치하는 선택자 내에 타입 선택자나 의사 요소가 하나 있을 때마다 가중치 값에 0-0-1을 더합니다.

값 없음 (No value)

전체 선택자(*)와 :where() 의사 클래스 및 그 매개변수들은 가중치를 계산할 때 아예 개수에 포함되지 않습니다. 즉, 이들의 가중치 값은 0-0-0입니다. 요소를 선택하긴 하지만, 명시도 가중치 값에는 아무런 영향을 주지 않습니다.

결합자(Combinators)인 +, >, ~, " " (공백), 그리고 || 기호들은 대상을 더 구체적으로 선택하게 해줄 순 있어도, 명시도 가중치에는 어떤 값도 더하지 않습니다.

& 중첩(nesting) 결합자 자체도 명시도 가중치를 추가하지 않지만, 중첩된 규칙 자체는 가중치를 추가합니다. 명시도와 기능 측면에서 중첩은 :is() 의사 클래스와 매우 유사하게 작동합니다.

중첩과 마찬가지로 :is(), :has(), 부정 의사 클래스인 :not() 자체는 가중치를 추가하지 않습니다. 하지만 이 선택자들 괄호 안에 들어가는 매개변수(parameters)들은 가중치를 추가합니다! 이들의 명시도 가중치는 괄호 안의 쉼표로 구분된 선택자 목록 중에서 가장 명시도가 높은 선택자의 값을 가져옵니다. 이와 유사하게 중첩된 선택자의 경우에도, 중첩된 선택자가 추가하는 명시도 가중치는 쉼표로 구분된 중첩 선택자 목록 중 가장 명시도가 높은 선택자의 가중치가 됩니다.

:not(), :is(), :has() 및 CSS 중첩 예외에 대해서는 아래에서 더 자세히 다루겠습니다.

💡 강사님의 꿀팁: 이 계산법을 아주 쉽게 비유해 드릴게요.

  • ID는 '백의 자리' 숫자입니다. (예: 1-0-0)
  • CLASS(클래스, 가상클래스, 속성선택자)는 '십의 자리' 숫자입니다. (예: 0-1-0)
  • TYPE(태그명, 가상요소)은 '일의 자리' 숫자입니다. (예: 0-0-1)
    이렇게 100점, 10점, 1점짜리 점수판이라고 생각하고 개수를 더해나가면 계산이 정말 쉬워집니다!

일치하는 선택자 (Matching selector)

명시도 가중치는 요소와 '일치하는(matching)' 선택자에서 나옵니다. 쉼표로 구분된 세 개의 선택자가 있는 다음 CSS 코드를 예로 들어보겠습니다:

[type="password"],
input:focus,
:root #myApp input:required {
  color: blue;
}

위의 선택자 목록에서 명시도 가중치가 0-1-0[type="password"] 선택자는 모든 비밀번호 입력(input) 타입에 color: blue 선언을 적용합니다.

타입에 상관없이 모든 input 요소가 포커스(focus)를 받으면, 목록의 두 번째 선택자인 input:focus와 일치하게 되며 명시도 가중치는 0-1-1이 됩니다. 이 가중치는 :focus 의사 클래스(0-1-0)와 input 타입(0-0-1)으로 이루어집니다. 만약 비밀번호 input이 포커스를 받으면 input:focus와 일치하게 되어 color: blue 스타일 선언의 명시도 가중치는 0-1-1이 됩니다. 그 비밀번호 input이 포커스를 잃으면 명시도 가중치는 다시 0-1-0으로 돌아갑니다.

id="myApp" 속성을 가진 요소 안에 중첩된 필수(required) input의 명시도는 1-2-1입니다. (ID 1개, 의사 클래스 2개, 요소 타입 1개 기준).

만약 required 속성이 있는 비밀번호 input 타입이 id="myApp"을 가진 요소 안에 중첩되어 있다면, 포커스가 있든 없든 명시도 가중치는 1-2-1이 됩니다. (ID 1개, 의사 클래스 2개, 요소 타입 1개 기준). 이 경우 명시도 가중치가 왜 0-1-1이나 0-1-0이 아니라 1-2-1이 될까요? 명시도 가중치는 일치하는 선택자들 중에서 가장 명시도 가중치가 높은 선택자에서 가져오기 때문입니다. 가중치는 세 개의 열 값을 왼쪽에서 오른쪽으로 비교하여 결정됩니다.

[type="password"] {
  /* 0-1-0 */
}
input:focus {
  /* 0-1-1 */
}
:root #myApp input:required {
  /* 1-2-1 */
}

세 개의 열 비교 (Three-column comparison)

관련된 선택자들의 명시도 값이 결정되면, 각 열(column)에 있는 선택자 구성 요소의 개수를 왼쪽에서 오른쪽으로 비교합니다.

#myElement {
  color: green; /* 1-0-0  - WINS!! (승리) */
}
.bodyClass .sectionClass .parentClass [id="myElement"] {
  color: yellow; /* 0-4-0 */
}

첫 번째 열은 ID 구성 요소의 값으로, 각 선택자에 있는 ID의 개수입니다. 경쟁하는 선택자들의 ID 열 숫자를 먼저 비교합니다. 다른 열의 값과 상관없이 ID 열의 값이 더 큰 선택자가 무조건 승리합니다. 위 예제에서, yellow를 만드는 선택자가 총 구성 요소 개수는 더 많음에도 불구하고, 오직 첫 번째 열의 값만이 중요하기 때문에 green이 승리합니다.

만약 경쟁하는 선택자들의 ID 열 숫자가 같다면, 아래 예제처럼 다음 열인 CLASS 열을 비교하게 됩니다.

#myElement {
  color: yellow; /* 1-0-0 */
}
#myApp [id="myElement"] {
  color: green; /* 1-1-0  - WINS!! (승리) */
}

CLASS 열은 선택자에 있는 클래스 이름, 속성 선택자, 의사 클래스의 개수입니다. ID 열의 값이 같을 때, TYPE 열의 값과 상관없이 CLASS 열의 값이 더 큰 선택자가 승리합니다. 아래 예제에서 이를 확인할 수 있습니다.

:root input {
  color: green; /* 0-1-1 - CLASS 열의 숫자가 더 크므로 승리! */
}
html body main input {
  color: yellow; /* 0-0-4 */
}

경쟁하는 선택자에서 CLASSID 열의 숫자가 모두 같다면, 마지막으로 TYPE 열이 중요해집니다. TYPE 열은 선택자에 있는 요소 타입(태그명)과 의사 요소의 개수입니다. 처음 두 열의 값이 같다면, TYPE 열의 숫자가 더 큰 선택자가 승리합니다.

경쟁하는 선택자들이 세 열 모두에서 동일한 값을 가진다면, 근접성 규칙(proximity rule)이 적용되어 마지막에 선언된 스타일(맨 아래에 작성된 코드)이 우선순위를 갖게 됩니다.

input.myClass {
  color: yellow; /* 0-1-1 */
}
:root input {
  color: green; /* 0-1-1 - 둘의 점수가 같지만 나중에 작성되었으므로 승리! */
}

💡 강사님의 꿀팁: RPG 게임으로 치면 ID는 '전설급 장비', CLASS는 '에픽급 장비', TYPE은 '일반 장비'라고 생각하세요. 일반 장비(태그)를 100개 껴입어도(0-0-100), 전설 장비(ID) 단 1개(1-0-0)를 절대 이길 수 없습니다! 각 자릿수끼리만 1:1 매치를 한다는 점, 절대 잊지 마세요.


:is(), :not(), :has() 그리고 CSS 중첩 예외 (The :is(), :not(), :has() and CSS nesting exceptions)

일치 항목(matches-any) 의사 클래스 :is(), 관계형(relational) 의사 클래스 :has(), 부정(negation) 의사 클래스 :not()은 명시도 가중치 계산 시 의사 클래스(CLASS 열)로 취급되지 않습니다. 이 함수들 자체는 명시도 방정식에 어떤 가중치도 추가하지 않아요.

하지만! 의사 클래스의 괄호 안에 전달된 선택자 매개변수(selector parameters)는 명시도 알고리즘에 포함됩니다. 이 의사 클래스들의 명시도 값은 괄호 안에 있는 매개변수들 중 가중치가 가장 높은 것의 가중치를 고스란히 가져옵니다.

p {
  /* 0-0-1 */
}
:is(p) {
  /* 0-0-1 */
}

h2:nth-last-of-type(n + 2) {
  /* 0-1-1 */
}
h2:has(~ h2) {
  /* 0-0-2 */
}

div.outer p {
  /* 0-1-2 */
}
div:not(.inner) p {
  /* 0-1-2 */
}

위의 CSS 짝을 살펴보면, :is(), :has(), :not() 의사 클래스가 제공하는 명시도 가중치는 의사 클래스 자체의 값이 아니라 내부의 선택자 매개변수의 값이라는 것을 알 수 있습니다.

이 세 가지 의사 클래스는 모두 복잡한 선택자 목록(쉼표로 구분된 선택자 목록)을 매개변수로 받을 수 있습니다. 이 기능을 활용하면 선택자의 명시도를 끌어올릴 수 있어요:

:is(p, #fakeId) {
  /* 1-0-0 */
}
h1:has(+ h2, > #fakeId) {
  /* 1-0-1 */
}
p:not(#fakeId) {
  /* 1-0-1 */
}
div:not(.inner, #fakeId) p {
  /* 1-0-2 */
}

위의 CSS 코드 블록에서 우리는 선택자에 #fakeId를 슬쩍 끼워 넣었습니다. 이 #fakeId는 각 단락의 명시도 가중치에 무려 1-0-0을 추가해 줍니다!

CSS 중첩(CSS nesting)으로 복잡한 선택자 목록을 만들 때도, 이 방식은 :is() 의사 클래스와 완전히 똑같이 동작합니다.

p,
#fakeId {
  span {
    /* 1-0-1 */
  }
}

위의 코드 블록에서 복잡한 선택자 p, #fakeId의 명시도는 #fakeId에서 값을 가져오고 거기에 span이 더해져서, p span#fakeId span 모두에 대해 1-0-1의 명시도를 생성합니다. 이는 :is(p, #fakeId) span 선택자를 쓴 것과 완전히 동일한 명시도입니다.

일반적으로는 명시도를 최소한으로 낮게 유지하는 것이 좋지만, 특정한 이유로 요소의 명시도를 꼭 높여야 한다면 이 세 가지 의사 클래스가 아주 유용한 도구가 될 수 있습니다.

a:not(#fakeId#fakeId#fakeID) {
  color: blue; /* 3-0-1 */
}

이 예제에서 모든 링크(a)는 파란색이 될 것입니다. 단, 3개 이상의 ID를 가진 링크 선언이 이 규칙을 덮어쓰거나, a 태그에 일치하는 색상 값이 !important 플래그를 포함하고 있거나, 링크에 인라인 스타일로 색상이 지정된 경우는 제외하고요. 만약 여러분이 실무에서 이런 엄청난 꼼수(hack) 기법을 사용한다면, 나중에 코드를 볼 사람들을 위해 왜 이런 짓이 필요했는지 꼭 주석을 남겨주세요!


@scope 블록이 명시도에 미치는 영향 (How @scope blocks affect specificity)

규칙(ruleset)을 @scope 블록 안에 넣는다고 해서 그 선택자의 명시도가 변하지는 않습니다. 스코프 루트(scope root)와 한계(limit) 안에 어떤 선택자가 사용되었는지와 무관하게 말이죠.

하지만, 여러분이 :scope 의사 클래스를 코드에 직접 명시적으로 추가하기로 했다면, 명시도를 계산할 때 이 녀석을 포함해야 합니다. :scope는 다른 일반적인 의사 클래스들과 마찬가지로 0-1-0의 명시도를 가집니다. 예를 들어볼까요:

@scope (.article-body) {
  /* :scope img 의 명시도는 0-1-0 + 0-0-1 = 0-1-1 입니다. */
  :scope img {
  }
}

더 자세한 정보는 @scope의 명시도(Specificity in @scope) 문서를 참고하세요.


명시도로 인한 두통 해결 팁 (Tips for handling specificity headaches)

!important를 남발하는 대신, 캐스케이드 레이어(cascade layers)를 활용하고 전체적인 CSS의 명시도 가중치를 낮게 유지하는 것을 고려해 보세요. 그러면 약간 더 구체적인 규칙을 추가하는 것만으로도 스타일을 쉽게 덮어쓸 수 있습니다. 시맨틱 HTML(의미론적 HTML)을 사용하면 스타일을 적용할 때 튼튼한 기준점(앵커)을 제공하여 명시도 관리에 큰 도움이 됩니다.

명시도를 추가하지 않고(혹은 추가하며) 선택자를 구체적으로 만들기 (Making selectors specific with and without adding specificity)

선택하려는 요소 앞에 그 요소가 속한 문서의 특정 섹션을 명시하면 규칙이 더 구체적이게 됩니다. 이 코드를 어떤 방식으로 추가하느냐에 따라 명시도를 약간만 더할 수도, 엄청나게 더할 수도, 아예 안 더할 수도 있습니다. 아래를 보시죠:

<main id="myContent">
  <h1>Text</h1>
</main>
#myContent h1 {
  color: green; /* 1-0-1 */
}
[id="myContent"] h1 {
  color: yellow; /* 0-1-1 */
}
:where(#myContent) h1 {
  color: blue; /* 0-0-1 */
}

어떤 순서로 코드를 작성하든 간에 h1은 초록색(green)이 될 것입니다. 첫 번째 규칙의 명시도가 압도적으로 가장 높기 때문이죠!

ID 명시도 낮추기 (Reducing ID specificity)

명시도는 선택자의 '형태'에 기반합니다. 요소의 id#이 붙은 ID 선택자로 쓰는 대신 [id="..."] 형태의 속성 선택자(attribute selector)로 포함시키면, 불필요하게 높은 명시도를 더하지 않고도 요소를 구체적으로 선택할 수 있는 아주 좋은 팁입니다. 이전 예제에서 [id="myContent"] 선택자는 ID를 선택하긴 하지만, 명시도를 계산할 때는 '속성 선택자'로 취급되어 0-1-0만 추가됩니다.

만약 선택자를 더 구체적으로 만들고 싶지만 명시도 점수는 단 1점도 올리고 싶지 않다면, id나 선택자의 어떤 부분이든 명시도 조절 의사 클래스인 :where()의 매개변수로 감싸버리면 됩니다.

💡 강사님의 꿀팁: 컴포넌트를 설계할 때 최상위 래퍼(wrapper)의 명시도가 너무 높으면 하위 스타일을 덮어쓰기가 굉장히 고통스러워집니다. 이럴 때 :where(.wrapper) 형태로 묶어주면 우선순위를 0으로 만들 수 있어, 유지보수가 훨씬 쉬운 유연한 CSS를 짤 수 있답니다!

선택자 중복으로 명시도 높이기 (Increasing specificity by duplicating selector)

명시도를 끌어올리기 위한 특별한 꼼수(special case)로, CLASSID 열의 가중치를 의도적으로 복제(duplicate)할 수도 있습니다. 복합 선택자 내에서 id, 클래스, 의사 클래스 또는 속성 선택자를 반복해서 쓰면 명시도가 올라갑니다. 내가 통제할 수 없는 외부 라이브러리 등의 매우 구체적인 선택자를 억지로 덮어써야 할 때 이 방법을 쓸 수 있습니다.

#myId#myId#myId span {
  /* 3-0-1 */
}
.myClass.myClass.myClass span {
  /* 0-3-1 */
}

이 방법은 정말 어쩔 수 없는 최후의 수단으로만, 아주 아껴서 사용하세요. 선택자 중복 기법을 사용했다면 미래의 나와 동료들을 위해 반드시 CSS에 주석을 달아두어야 합니다.

부모 요소에 id를 추가할 수 없는 상황이라도, :is():not() (그리고 :has())을 활용하면 명시도를 영리하게 올릴 수 있습니다:

:not(#fakeID#fakeId#fakeID) span {
  /* 3-0-1 */
}
:is(#fakeID#fakeId#fakeID, span) {
  /* 3-0-0 */
}

서드파티 CSS보다 우선순위 높이기 (Precedence over third-party CSS)

명시도 전쟁을 벌이지 않고도 한 스타일 세트가 다른 세트보다 확실한 우위를 점하게 하는 최신 표준 방법은 바로 캐스케이드 레이어(cascade layers)를 적극 활용하는 것입니다! 레이어 안에 들어간 일반(normal) 제작자 스타일은, 레이어 밖에 있는(unlayered) 일반 제작자 스타일보다 우선순위가 항상 낮습니다.

만약 여러분이 수정할 수 없거나 속을 알 수 없는 외부 스타일시트(부트스트랩 같은 프레임워크 등)의 스타일을 덮어써야 한다면, 내가 통제할 수 없는 그 외부 스타일들을 캐스케이드 레이어 안으로 격리(import)시키는 전략을 쓰세요. 나중에 선언된 레이어일수록 우선순위가 높고, 레이어에 속하지 않은 일반 스타일은 같은 출처의 모든 레이어 스타일을 씹어먹고 최종 우위를 점하게 됩니다.

서로 다른 레이어에 있는 두 선택자가 같은 요소를 겨냥할 때는 출처(origin)와 중요도(importance)가 최우선으로 적용되므로, 싸움에서 진 레이어에 있는 선택자의 명시도는 아무리 높아 봐야 완전 무용지물이 됩니다.

@import "TW.css" layer();
p,
p * {
  font-size: 1rem;
}

위 예제에서, 단락(p)과 그 안의 중첩된 콘텐츠는 TW 스타일시트(테일윈드 등)에 얼마나 명시도가 높은 무시무시한 클래스 이름이 붙어있든 상관없이, 무조건 1rem의 폰트 크기를 갖게 됩니다. 레이어 바깥 코드의 힘이죠!


!important 피하고 덮어쓰기 (Avoiding and overriding !important)

가장 훌륭한 접근법은 애초에 !important를 사용하지 않는 것입니다. 위에서 설명해 드린 명시도에 대한 지식들이 이 나쁜 플래그를 사용하지 않고, 발견하더라도 깔끔하게 걷어내는 데 큰 도움이 될 거예요.

!important를 써야만 할 것 같은 충동을 지우려면 다음 방법 중 하나를 택해보세요:

  • 이전에 !important를 쓰려던 선언의 선택자 명시도를 살짝 높여서, 경쟁하는 다른 선언들보다 점수를 높게 만듭니다.
  • 명시도를 똑같이 맞춘 다음, 덮어쓰려는 선언보다 내 코드를 더 아래쪽(나중에)에 배치합니다.
  • 내가 덮어쓰려고 끙끙대는 그 대상(원래 코드)의 명시도를 낮춥니다. (이게 베스트입니다!)

이 모든 방법은 앞선 섹션들에서 자세히 다뤘습니다.

만약 여러분이 수정 권한이 없는 다른 사람의 스타일시트에 이미 !important 플래그가 덕지덕지 붙어있어서 도저히 지울 수 없다면, 그 스타일을 이길 수 있는 유일한 방법은 내 코드에도 !important를 쓰는 것뿐입니다. 이럴 때는 !important 선언들을 덮어쓰기 위한 전용 캐스케이드 레이어를 하나 만드는 것이 환상적인 해결책이 될 수 있습니다. 두 가지 방법이 있어요:

방법 1 (Method 1)

  1. 여러분이 지울 수 없었던 외부의 !important 선언들을 저격해서 덮어쓸 내 !important 선언들만 모아놓은 아주 짧고 독립적인 스타일시트를 하나 만듭니다.
  2. CSS 파일의 맨 꼭대기에서, 다른 스타일시트들을 링크하기 전에 layer() 함수와 함께 @import 구문을 사용해 이 스타일시트를 가장 먼저 불러옵니다. 이렇게 하면 이 중요한 오버라이드(overrides) 코드가 가장 첫 번째 레이어로 가져와지게 됩니다.
@import "importantOverrides.css" layer();

방법 2 (Method 2)

  1. 스타일시트 선언의 맨 앞부분에, 아래와 같이 이름이 있는 캐스케이드 레이어를 생성합니다:
@layer importantOverrides;
  1. 골칫거리인 !important 선언을 덮어써야 할 때마다, 이 명명된 레이어 안에서 내 스타일을 선언하세요. 오직 !important 규칙들만 이 레이어 안에 선언해야 합니다.
[id="myElement"] p {
  /* 평범한 스타일들은 여기에 작성합니다 */
}
@layer importantOverrides {
  [id="myElement"] p {
    /* !important 스타일들은 여기에 격리합니다 */
  }
}

왜 첫 번째 레이어에 넣어야 하나요? > 잊지 마세요! !important 선언의 세계에서는 레이어의 우선순위가 일반 스타일과 완전히 반대로 뒤집힙니다. 즉, 가장 먼저 선언된 레이어(첫 번째 레이어)에 있는 !important가 가장 나중에 선언된 레이어의 !important를 이기게 됩니다!

레이어 안에서 !important 스타일을 작성할 때는 덮어쓰려는 요소와 일치하기만 한다면 선택자의 명시도 점수가 낮아도 괜찮습니다. 일반적인 레이어들은(normal layers) 이 오버라이드 레이어 바깥쪽에 선언되어야 합니다. 일반 스타일의 세계에서는 레이어에 속한 스타일이 레이어 바깥의 스타일보다 우선순위가 낮기 때문이죠.


인라인 스타일 (Inline styles)

요소에 직접 추가된 인라인 스타일(예: <p style="font-weight: bold;">)은 개발자 스타일시트에 있는 그 어떤 '일반(normal)' 스타일도 무조건 덮어씁니다. 따라서 사실상 가장 높은 명시도를 가졌다고 볼 수 있죠. 인라인 스타일의 명시도 가중치를 아예 별도의 차원인 1-0-0-0이라고 생각하시면 이해하기 편합니다.

이 무적의 인라인 스타일을 CSS 파일에서 덮어쓸 수 있는 유일한 방법은 !important를 사용하는 것뿐입니다.

많은 JavaScript 프레임워크나 라이브러리들이 DOM에 인라인 스타일을 멋대로 주입하곤 합니다. 이럴 때 해당 인라인 스타일을 겨냥하는 속성 선택자(attribute selector)처럼 매우 타겟팅된 선택자와 함께 !important를 사용하는 것이 이런 고집 센 인라인 스타일을 제압하는 한 가지 방법입니다.

<p style="color: purple"></p>
p[style*="purple"] {
  color: rebeccapurple !important;
}

이런 방식으로 !important 플래그를 쓸 수밖에 없었다면, 나중에 코드를 유지보수할 동료(혹은 미래의 나)가 "왜 여기서 CSS의 안티 패턴(anti-pattern)을 썼는지" 이해할 수 있도록 반드시 주석을 달아주세요!


!important 예외 (The !important exception)

중요(important)하다고 표시된 CSS 선언은 동일한 캐스케이드 레이어와 출처(origin) 내에 있는 다른 모든 선언들을 뭉개버립니다. 기술적으로 엄밀히 말하자면 !important 는 명시도(specificity) 자체와는 아무런 관련이 없지만, 명시도 그리고 캐스케이드와 아주 직접적으로 얽혀서 상호작용합니다. 이 녀석은 스타일시트의 캐스케이드(cascade) 순서를 통째로 역전시켜버리죠.

동일한 출처와 동일한 캐스케이드 레이어에서 충돌이 발생했는데 한쪽 속성값에 !important 플래그가 붙어있다면, 명시도가 아무리 낮더라도 !important 선언이 무조건 승리합니다. 만약 똑같은 출처와 똑같은 레이어에서 !important가 붙은 두 선언이 한 요소에서 충돌한다면? 그때서야 둘 사이의 명시도를 비교해서 더 높은 쪽이 적용됩니다.

단지 명시도 싸움에서 이기기 위해 !important를 남발하는 것은 끔찍한 안티 패턴(bad practice)으로 간주되며 무조건 피해야 합니다. 명시도와 캐스케이드의 원리를 정확히 이해하고 효과적으로 활용하면 !important 플래그를 써야 할 일 자체를 없앨 수 있습니다.

외부 CSS(Bootstrap이나 normalize.css 같은 외부 라이브러리)의 명시도가 너무 높아서 억지로 !important를 써서 덮어쓰려 하지 말고, 차라리 그 서드파티(third-party) 스크립트들을 통째로 캐스케이드 레이어(cascade layers) 안으로 집어넣어(import) 버리세요.

그럼에도 불구하고 CSS에 !important를 꼭 써야만 하는 피치 못할 상황이라면, 왜 이 선언에 중요 표시를 했는지 주석을 달아두세요. 그래야 미래의 코드 유지보수 담당자가 그 의도를 알고 함부로 덮어쓰려 하지 않을 테니까요. 그리고 무엇보다도, 다른 개발자들이 가져다 써야 하는 플러그인이나 프레임워크를 개발할 때는 절대로, 네버, !important를 쓰지 마세요! 사용자가 스타일을 제어할 권한을 완전히 뺏겨버리게 되니까요.


:where() 예외 (The :where() exception)

명시도를 자유자재로 조절할 수 있는 마법의 의사 클래스 :where()는 자신의 명시도를 언제나 완벽한 제로(0-0-0)로 만들어버립니다. 이 녀석을 사용하면 요소의 타겟팅은 아주 구체적으로 콕 집어서 하면서도, 얄미운 명시도 점수는 단 1점도 올리지 않을 수 있어요.

코드를 수정할 권한이 없는 다른 개발자들이 가져다 쓸 서드파티(third-party) CSS 컴포넌트를 만들 때는, 가능한 한 명시도를 가장 낮게 세팅해서 코드를 짜는 것이 훌륭한 모범 사례(good practice)입니다. 예를 들어, 여러분이 배포하는 테마에 아래와 같은 CSS가 포함되어 있다고 해봅시다:

:where(#defaultTheme) a {
  /* 0-0-1 */
  color: red;
}

이렇게 :where()로 명시도를 죽여놓으면, 이 위젯을 가져다 쓰는 개발자는 복잡한 선택자나 !important 없이 아주 단순한 타입(태그) 선택자만으로도 링크 색상을 쉽게 덮어쓸 수 있게 됩니다. 얼마나 친절한가요!

footer a {
  /* 0-0-2 - 0-0-1을 가볍게 이깁니다! */
  color: blue;
}

DOM 트리 근접성은 무시됩니다 (Tree proximity ignorance)

선택자에서 참조하는 다른 부모/조상 요소와 실제 타겟 요소 사이의 거리(근접성)는 명시도 점수에 아~무런 영향을 주지 않습니다.

body h1 {
  color: green;
}

html h1 {
  color: purple;
}

위의 경우 <h1> 요소는 보라색(purple)이 됩니다. 왜냐하면 둘 다 명시도 점수가 0-0-2로 똑같기 때문에, 무승부 원칙에 따라 파일의 가장 마지막에 선언된 선택자(last declared selector)가 우선권을 가지기 때문입니다. bodyhtml보다 h1에 더 물리적으로 가깝게 붙어있다고 해서 어드밴티지를 주지 않는다는 뜻이죠!


직접 타겟팅된 요소 vs. 상속된 스타일 (Directly targeted elements vs. inherited styles)

요소를 '직접' 가리켜서 부여한 스타일은, 조상으로부터 스멀스멀 물려받은(inherited) 스타일을 무조건 항상 이깁니다. 물려받은 스타일의 명시도 점수가 아무리 수백 점이 넘어간다 해도 말이죠! 아래의 CSS와 HTML을 볼까요:

#parent {
  color: green; /* 명시도: 1-0-0 */
}

h1 {
  color: purple; /* 명시도: 0-0-1 */
}
<html lang="en">
  <body id="parent">
    <h1>Here is a title!</h1>
  </body>
</html>

#parent의 명시도가 압도적으로 높지만 h1 텍스트는 보라색(purple)이 됩니다. 왜냐하면 h1 선택자는 요소를 직접 콕 집어서(targeted specifically) 색상을 지정했지만, 초록색(green)은 단순히 #parent에 선언되었다가 자식에게 은근슬쩍 상속(inherited)된 것에 불과하기 때문입니다. 직접 타격이 상속을 이깁니다!


예제 (Examples)

아래 CSS를 보면, 색상을 지정하기 위해 <input> 요소를 타겟팅하는 세 개의 선택자가 있습니다. 특정 input이 있을 때 우선순위를 갖는 색상 선언은, 그 요소와 일치하는 선택자 중 명시도 가중치가 가장 높은 것이 됩니다.

#myElement input.myClass {
  color: red;
} /* 1-1-1 */
input[type="password"]:required {
  color: blue;
} /* 0-2-1 */
html body main input {
  color: green;
} /* 0-0-4 */

만약 위의 세 선택자가 모두 똑같은 input 요소 하나를 동시에 겨냥하고 있다면, 이 input은 빨간색(red)이 됩니다. 첫 번째 선언이 가장 강력한 등급인 ID 열에서 가장 높은 값을 가졌기 때문이죠.

가장 아래에 있는 html body main input 선택자는 무려 네 개의 TYPE 구성 요소를 가지고 있습니다. 정수 값 4로 숫자 자체는 제일 높지만, TYPE 열(일의 자리)의 개수가 150개로 늘어난다고 해도 CLASS 열(십의 자리) 1개를 절대 이길 수 없으며, 당연히 ID 열(백의 자리)도 이길 수 없습니다. 열의 값들은 반드시 왼쪽(높은 등급)부터 오른쪽으로 순서대로 비교된다는 사실을 명심하세요.

만약 위 예제 코드에서 첫 번째 코드의 id 선택자를 속성(attribute) 선택자로 바꾼다면, 첫 번째와 두 번째 선택자는 아래와 같이 동일한 명시도를 갖게 됩니다:

[id="myElement"] input.myClass {
  color: red;
} /* 0-2-1 */
input[type="password"]:required {
  color: blue;
} /* 0-2-1 */

이렇게 여러 선언이 똑같은 명시도를 가질 때는, CSS 파일에서 가장 마지막에 작성된(발견된) 선언이 요소에 적용됩니다. 두 선택자가 동일한 <input>에 일치하는 상황이라면, 최후의 승자인 색상은 파란색(blue)이 됩니다.


추가 참고 사항 요약 (Additional notes)

명시도와 관련하여 꼭 기억해야 할 5가지 핵심 사항입니다:

  1. 명시도는 동일한 요소가 동일한 캐스케이드 레이어나 출처에서 여러 선언의 타겟이 될 때만 승패를 가릅니다. 명시도는 출처(origin), 중요도(importance), 그리고 캐스케이드 레이어가 모두 동일한 선언들 사이에서만 의미가 있습니다. 만약 선택자들이 서로 다른 출처(예: 사용자 CSS vs 개발자 CSS)에 있다면, 명시도가 아니라 캐스케이드(cascade) 알고리즘이 우선순위를 알아서 결정합니다.
  2. 동일한 캐스케이드 레이어와 출처에 속한 두 선택자가 명시도마저 완전히 똑같다면, 그다음엔 스코프 근접성(scoping proximity)을 계산합니다. 스코프 루트와 더 가까운(근접성이 낮은) 규칙 세트가 승리합니다.
  3. 스코프 근접성마저도 두 선택자가 똑같다면? 드디어 소스 코드 작성 순서(source order)가 나설 차례입니다. 다른 모든 조건이 평등할 땐, 가장 나중에 선언된(가장 밑에 있는) 선택자가 최종 승리합니다.
  4. CSS의 대원칙에 따라, 직접 타겟팅된 요소에 부여된 스타일은 조상으로부터 은근슬쩍 상속받은 스타일을 항상, 무조건 이깁니다.
  5. 문서 트리에서 요소들 간의 근접성(물리적 거리)은 명시도 점수에 티끌만 한 영향도 주지 않습니다.

스펙 (Specifications)

명세 (Specification)
Selectors Level 4 (선택자 레벨 4) # specificity-rules

참고 자료 (See also)

명시도와 캐스케이드는 CSS를 지배하는 절대 규칙입니다! 더 탄탄한 기초를 원하신다면 아래 문서들을 꼭 정독해 보세요.


MDN 개선에 도움을 주세요 (Help improve MDN)

이 문서가 학습에 도움이 되셨나요? (Was this page helpful to you?)
[Yes][No]

문서 기여 방법 알아보기: Learn how to contribute
최종 수정일: 2025년 12월 16일 (MDN contributors 작성)
이 문서를 GitHub에서 보기 | 문서의 문제점 신고하기

profile
프론트에_가까운_풀스택_개발자

0개의 댓글