🎨 Frontend CSS1. Cascade & Specificity01 — Cascade Algorithm: 8단 토너먼트

01 — Cascade Algorithm

한 줄 답: 한 요소의 최종 스타일은 단일 비교가 아니라 8단 토너먼트의 결과다. Specificity는 그 토너먼트의 6번째 라운드일 뿐, 그 앞에 origin·layer·scope가 있고 뒤에 order가 있다.


Why — 왜 “단순 비교”로는 안 되는가

CSS 한 요소에 적용되는 선언은 흔히 수십, 수백 개다.

<button class="btn primary" id="cta">Buy</button>

이 버튼에 영향을 줄 수 있는 곳:

출처
브라우저 기본 (UA)button { font: -webkit-small-control }
사용자 스타일시트button { font-size: 18px !important; } (시각장애 사용자)
작성자 CSS.btn { color: white }
라이브러리 (Tailwind, MUI).btn-primary { background: blue }
인라인 style<button style="color: red">
@keyframes 애니메이션to { color: gold }
transitiontransition: color 200ms 중간값

이 7개에서 어떤 값이 최종으로 적용되어야 하는가? — 이게 cascade의 문제다.

단순히 “specificity 높은 게 이긴다”로는 부족하다. 사용자의 접근성 설정이 작성자보다 우선해야 할 때가 있고, 애니메이션은 specificity와 무관하게 작동해야 한다. 그래서 8단계가 필요했다.


How — 8단 토너먼트

CSS Cascade Level 5/6 명세에 따른 정식 순서다. 위에서 결판나면 아래는 보지도 않는다.

1. Origin & Importance     (가장 강한 분류)
2. Context (Encapsulation) (Shadow DOM, Slot 등)
3. Element-attached styles (transition / animation)
4. Cascade Layers          (@layer)
5. Cascade Scope (proximity)
6. Specificity (a, b, c)
7. Order of Appearance     (마지막에 선언된 게 이김)

(실무 기준 7단계로 외워도 충분. 명세는 context를 한 단으로 분리해 8단으로 본다.)

1) Origin & Importance — 1라운드

순위출처비고
1 (최강)Transition (실행 중)어떤 것도 못 이긴다
2UA !important브라우저가 강제하는 안전장치
3User !important사용자가 작성자를 이긴다
4Author !important작성자가 쓴 !important
5Animation (실행 중)@keyframes 중간 값
6Author (normal)평소 작성자 CSS
7User (normal)사용자 스타일시트
8 (최약)UA (normal)브라우저 기본

핵심 반전: !important가 붙으면 순서가 뒤집힌다. User > Author > UA. 접근성 우선 설계다.

/* 사용자 스타일시트 (브라우저 설정) */
* { font-size: 18px !important; }    /* 작성자가 어떤 !important를 써도 못 막음 */

2) Context — 2라운드

Shadow DOM과 Slot의 인캡슐레이션 우선순위. 대부분의 일반 페이지에선 의식할 일 없다.

  • Slot으로 내려온 콘텐츠의 스타일 vs Shadow DOM 내부 스타일 → outer가 이긴다.

3) Element-attached styles — 3라운드

transition 중인 값은 현재 진행 중일 때만 별도 라운드에서 최강이 된다.

.btn { color: red; transition: color 1s; }
.btn:hover { color: blue; }

hover 시작 후 0.5초 시점에는 color: purple(중간값)이 적용되는데, 이 중간값은 이후 모든 선언을 무시한다. transition이 끝나면 일반 cascade로 돌아온다.

4) Cascade Layers — 4라운드

@layer reset, framework, components, utilities;
 
@layer utilities {
  .text-red { color: red; }       /* 가장 늦게 선언된 레이어가 강함 */
}
@layer components {
  .btn { color: blue; }
}

같은 origin·!important 분류 안에서 나중에 선언된 레이어가 이긴다. specificity를 보기 전에 layer가 결정한다 — 즉 낮은 layer의 #id #id #id도 높은 layer의 .class에 진다.

레이어에 속하지 않은 규칙은 모든 layer보다 강하다(layer 없음 = “unlayered” = 가장 강한 layer 취급).

!important를 쓰면 layer 순서가 역전된다. 가장 먼저 선언된 layer의 !important가 이긴다. (이유: !important의 정신은 “근본을 보호” — 가장 먼저 정한 reset/base가 보호되도록.)

5) Cascade Scope — 5라운드

@scope (.card) to (.card-footer) {
  h1 { color: red; }       /* 카드 안의 h1만 */
}

@scope로 묶인 규칙들 사이에서, 스코프 루트에 더 가까운(proximity가 짧은) 규칙이 이긴다. specificity 동률 시 적용된다.

6) Specificity — 6라운드

(a, b, c) 튜플 비교. 자세한 건 다음 문서.

카테고리점수
a — ID#header(1,0,0)
b — class/attr/pseudo-class.btn, [type], :hover(0,1,0)
c — type/pseudo-elementbutton, ::before(0,0,1)
inline stylestyle="..."(1,0,0,0) — a보다 위

비교는 사전식. (1,0,0) > (0,99,99). 단일 ID 하나가 클래스 99개를 이긴다.

7) Order of Appearance — 7라운드

위 6라운드까지 동률이면 나중에 선언된 규칙이 이긴다.

.btn { color: red; }
.btn { color: blue; }    /* 이게 이김 */

@import 순서, <link> 순서, <style> 순서가 모두 영향을 준다. 동일 파일 안에서는 위→아래.


What — 실제 사고 추적

예 1: 라이브러리 vs 자체 CSS

{/* 헤더에서 */}
<link rel="stylesheet" href="bootstrap.css">  {/* .btn { color: white } */}
<link rel="stylesheet" href="site.css">        {/* .btn { color: black } */}

site.css.btn은 specificity 동률, order 더 늦음 → black 이김.

예 2: 한 줄에 !important 추가

/* bootstrap.css */
.btn { color: white !important; }
 
/* site.css */
.btn { color: black; }

bootstrap의 !important가 normal을 이긴다 → white 이김. site.css가 더 늦게 로드돼도 무관.

예 3: @layer로 해결

@layer framework, site;
 
@layer framework {
  .btn { color: white !important; }
}
@layer site {
  .btn { color: black; }                /* !important 없이도 이김 */
}

site 레이어가 더 늦게 선언됨 → 레이어 라운드에서 site 승. !important가 framework에 있어도 같은 분류(normal vs important) 안에서 비교되므로… 어? framework가 important면 site는 normal이라 라운드 1에서 짐.

/* 올바른 해결 */
@layer framework, site;
 
@layer framework {
  .btn { color: white !important; }     /* important에서는 먼저 선언된 layer가 강함 */
}
@layer site {
  .btn { color: black !important; }     /* 같은 important + 나중 layer = 짐 */
}
/* → !important 분류에서 framework가 이긴다. white 적용 */

교훈: !important + @layer는 직관과 반대다. 가급적 !important를 안 쓰는 구조를 만들자.

예 4: 인라인 style은 어떻게 분류되나

<div style="color: red"></div>
<style>
  div { color: blue !important; }
</style>
  • 인라인 style: author normal, specificity (1,0,0,0) — class보다 강함
  • <style>!important: author important

라운드 1에서 important 분류가 더 강함 → blue 이김.

예 5: animation의 위치

.btn { color: red; }
@keyframes shake {
  to { color: green; }
}
.btn { animation: shake 1s infinite; }

animation 중에는 animation 분류가 author normal보다 위(라운드 1) → green이 적용되며 흔들림.

하지만:

.btn { color: red !important; }

이건 author important → animation을 이긴다 → red 고정.


What-if — 잘못 알면

사고 1: “ID는 최강”이라고 믿기

#sidebar a { color: red; }
.nav a.active { color: blue !important; }

specificity는 (1,0,1) vs (0,2,1)로 ID가 이기지만, !important 때문에 라운드 1에서 결판 → blue. specificity는 6라운드에서나 보인다.

사고 2: !important를 만능 해머로 쓰기

라이브러리가 !important를 안 쓰는데도 .btn { color: red !important }를 도배하면:

  • 향후 디자인 변경 때 다른 !important로 덮어야 한다 → 폭주.
  • 사용자 접근성 설정을 무력화한다 (사용자 normal은 작성자 important에 짐).

해결: @layer로 우선순위를 구조적으로 표현.

사고 3: animation이 transition을 이긴다고 착각

순위는 transition (3라운드) > animation (1라운드의 5순위). transition은 element-attached로 별도 처리되어 더 강하다.

.btn {
  background: red;
  animation: pulse 1s infinite;          /* red ↔ green */
  transition: background 500ms;
}
.btn:hover { background: blue; }

hover하면 transition이 발동 → animation 중간값을 무시하고 blue로 매끄럽게 전이.


Insight — 왜 8단인가 (역사)

CSS1 (1996)은 단순했다. cascade는 origin(작성자/사용자/UA) + specificity + order의 3단계였다.

연도추가된 라운드이유
1996origin, specificity, orderCSS1
1998!important 도입사용자 우선권 보장
2009transition/animation모션 추가
2022@layer (CSS Cascade 5)작성자 내부 우선순위를 코드로
2023+@scope proximity (CSS Cascade 6)컴포넌트 격리

중요한 흐름: 라운드는 위에 추가되었지 아래에 추가되지 않았다. specificity는 26년간 같은 자리에 있다. 추가된 것은 그보다 더 거시적인 분류 기준들이다.

이는 CSS가 “규칙 하나가 이기는 모델”에서 “규칙 그룹들이 협상하는 모델”로 진화했음을 보여준다. 디자인 시스템과 라이브러리가 평화롭게 공존할 도구가 필요했던 것이다.


요약 + Mermaid

  • 8단 토너먼트: origin/!important → context → element-attached → layer → scope → specificity → order.
  • !important는 라운드 1에서 origin 순서를 뒤집는다 (user > author > UA).
  • @layer는 라운드 4 — specificity 전에 결판난다.
  • @scope는 라운드 5 — proximity로 동률 해결.
  • Specificity는 6라운드. 위에 4개의 더 강한 게이트가 있다.

다음: 02-specificity에서 (a,b,c) 튜플을 정확히 계산한다.