@layer, :where(), 스코프, !important 등 우선순위 결정 알고리즘의 내부 규칙을 깊이 파고든다.
캐스케이드 심화 — 레이어, 스코프, 우선순위 알고리즘
CSS가 충돌하는 규칙들 사이에서 어떤 스타일을 최종적으로 적용할지 결정하는 과정을 "캐스케이드(Cascade)"라고 합니다. 입문 과정에서 클래스·아이디·태그 선택자의 명시도 점수 계산법을 익혔다면, 이번 강에서는 그 계산이 전체 알고리즘의 극히 일부에 불과하다는 사실을 확인할 것입니다. 브라우저는 명시도보다 먼저 origin(출처)과 레이어(layer)를 비교하며, !important는 이 순서를 완전히 뒤집습니다. 이 구조를 이해하지 못하면 아무리 명시도를 높여도 예상치 못한 결과를 만나게 됩니다.
이 레슨에서는 캐스케이드 정렬 전체 알고리즘을 단계별로 해부하고, @layer로 우선순위를 의도적으로 설계하는 방법, :where()와 :is()의 명시도 0 패턴, 새롭게 표준화된 @scope, 그리고 상속 키워드의 정확한 의미까지 다룹니다.
학습 목표
- 캐스케이드 정렬 알고리즘(origin, importance, layer, specificity, order)의 전체 흐름을 단계별로 설명할 수 있다.
@layer선언으로 우선순위를 예측 가능하게 설계하고, 서드파티 스타일과의 충돌을 제어할 수 있다.- **
:where()와:is()**의 명시도 차이를 이해하고 재정의하기 쉬운 컴포넌트 스타일을 작성할 수 있다. @scope규칙으로 스타일 격리를 구현하고 BEM 없이 컴포넌트 스코핑을 관리할 수 있다.inherit / initial / unset / revert / revert-layer키워드의 정확한 차이를 구분하여 상속을 명시적으로 제어할 수 있다.
캐스케이드 정렬 알고리즘 전체 흐름
브라우저가 특정 요소의 특정 프로퍼티에 적용할 선언을 결정할 때, 경쟁하는 선언들을 아래 단계 순서로 필터링합니다. 앞 단계에서 하나만 남으면 뒤 단계는 실행하지 않습니다.
| 단계 | 비교 기준 | 설명 |
|---|---|---|
| 1 | Origin + Importance | 출처(user-agent, user, author)와 !important 여부를 결합한 6단계 우선순위 |
| 2 | Context | 쉐도우 DOM 내부/외부 컨텍스트 |
| 3 | Element-attached styles | 인라인 style="" 속성 |
| 4 | @layer | 레이어 선언 순서(나중 레이어가 높은 우선순위) |
| 5 | Specificity | 명시도 점수 |
| 6 | Order of appearance | 동일 명시도일 때 소스 순서(나중 것 우승) |
Origin + Importance 의 6단계
!important 없는 일반 선언과 !important 선언의 origin 우선순위는 반대로 역전됩니다.
낮음 ──────────────────────────────────── 높음
일반: [user-agent] [user] [author]
important: [author!] [user!] [user-agent!]
즉 author !important는 user !important보다 낮고, user-agent !important가 가장 강합니다. 이것이 !important를 남발하면 안 되는 핵심 이유입니다.
/* user-agent 기본 스타일 (브라우저 내장) */
input[type="text"] {
/* 브라우저가 !important 붙인 UA 스타일은 author !important도 못 이김 */
}
/* author 스타일시트 */
.input-field {
color: red !important; /* author important — user important보다 낮음 */
}
⚠️ 주의
!important를 사용할수록 나중에 더 강한!important가 필요한 악순환이 생깁니다. Origin 단계를 이해한 뒤에는@layer를 통해!important없이 우선순위를 제어하는 방식으로 전환하세요.
@layer로 우선순위를 의도적으로 설계하기
@layer는 CSS Cascade Level 5에서 도입된 규칙으로, 선언 집합에 이름 붙인 레이어를 할당하고 레이어 간 우선순위를 명시적으로 정의합니다.
레이어 선언과 순서
/* ✅ 레이어 순서를 파일 최상단에서 한 번에 선언 */
@layer reset, base, components, utilities;
@layer reset {
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
}
}
@layer base {
body {
font-family: system-ui, sans-serif;
line-height: 1.5;
}
a { color: inherit; }
}
@layer components {
.btn {
display: inline-flex;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
background: hsl(220 90% 56%);
color: #fff;
}
}
@layer utilities {
.mt-4 { margin-top: 1rem; }
.text-center { text-align: center; }
}
핵심 규칙: 같은 명시도일 때 나중에 선언된 레이어가 이깁니다. 위 코드에서 utilities가 가장 강하고 reset이 가장 약합니다. 레이어 밖 스타일(unlayered)은 암묵적으로 모든 레이어보다 높은 우선순위를 가집니다.
/* ❌ 레이어 순서 없이 나열하면 파일 순서에 의존하게 됨 */
@layer components { .btn { color: blue; } }
@layer utilities { .btn { color: red; } } /* utilities가 나중이라 red 적용 */
/* ✅ 상단에서 순서를 먼저 선언하면 어디에 코드가 있어도 의도대로 동작 */
@layer components, utilities;
서드파티 라이브러리 격리
Tailwind, Bootstrap 같은 라이브러리를 레이어 안에 가두면 자신의 스타일이 항상 이깁니다.
/* ✅ 외부 라이브러리를 가장 낮은 레이어로 격리 */
@layer tailwind-base, tailwind-components, tailwind-utilities, app;
@import "tailwind.css" layer(tailwind-base);
@layer app {
/* 이 안의 스타일은 명시도에 관계없이 tailwind보다 강함 */
.card { padding: 2rem; }
}
!important와 레이어의 역전 현상
!important가 붙으면 레이어 우선순위가 반전됩니다. 레이어 선언 순서가 reset → components라면, 일반 선언에서는 components가 이기지만, !important가 붙으면 reset !important가 components !important보다 강해집니다.
@layer reset, components;
@layer reset {
a { color: red !important; } /* ✅ components !important보다 강함 */
}
@layer components {
a { color: blue !important; } /* ❌ reset !important에 짐 */
}
💡 TIP 이 역전 현상을 활용하면
reset레이어에!important를 붙여 어떤 컴포넌트 스타일도 덮어쓸 수 없는 "잠긴(locked)" 스타일을 만들 수 있습니다. 접근성 관련 강제 색상 같은 경우에 유용합니다.
:where()와 :is()의 명시도(0) 활용 패턴
:is()와 :where()는 기능적으로 동일한 선택자 목록 매칭을 수행하지만 명시도가 다릅니다.
| 함수 | 명시도 | 설명 |
|---|---|---|
:is(a, .link, #nav) | 인수 중 가장 높은 명시도 채택 | #nav가 있으면 ID 명시도 적용 |
:where(a, .link, #nav) | 항상 0 | 내부 선택자 명시도 무시 |
재정의 가능한 컴포넌트 기반 스타일
라이브러리나 디자인 시스템을 만들 때 :where()로 명시도 0 스타일을 제공하면 사용자가 클래스 하나로도 덮어쓸 수 있습니다.
/* ✅ :where()로 명시도 0 — 어떤 선택자로도 덮어쓸 수 있음 */
:where(.card) {
padding: 1rem;
border-radius: 0.5rem;
background: white;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
}
/* 사용자가 클래스 하나로 손쉽게 재정의 가능 */
.card-compact {
padding: 0.5rem; /* 명시도 0,1,0 > 0,0,0 이므로 우선 적용 */
}
/* ❌ :is()를 쓰면 내부 가장 높은 명시도를 물려받아 재정의가 어려워짐 */
:is(#app .card, .card) {
padding: 1rem; /* #app .card 때문에 명시도 1,1,0 이 되어버림 */
}
/* 이제 이 선언은 힘없이 무시됨 */
.card-compact { padding: 0.5rem; } /* 명시도 0,1,0 — 패배 */
복합 선택자에서의 :is() 활용
:is()의 "최고 명시도 상속" 특성은 복잡한 조합 선택자를 단순화할 때 유용합니다.
/* ❌ 반복이 많은 기존 방식 */
header h1, header h2, header h3,
nav h1, nav h2, nav h3,
main h1, main h2, main h3 {
line-height: 1.2;
}
/* ✅ :is()로 간결하게 — 명시도는 h3 기준(태그)로 동일 */
:is(header, nav, main) :is(h1, h2, h3) {
line-height: 1.2;
}
💡 TIP 디자인 시스템 기본 스타일은
:where()로, 테마 오버라이드는 일반 클래스로, 컴포넌트별 강제 스타일은@layer최후방으로 설계하면 명시도 전쟁 없이 예측 가능한 CSS 아키텍처를 만들 수 있습니다.
@scope와 스타일 격리 메커니즘
@scope는 CSS Cascade Level 6의 기능으로, 특정 루트 요소 하위에서만 적용되는 스타일 블록을 정의합니다. BEM 클래스 접두어나 CSS Modules 없이도 컴포넌트 스코핑을 구현할 수 있습니다.
기본 문법
/* ✅ .card 내부에서만 동작하는 스타일 */
@scope (.card) {
/* 암묵적으로 :scope 선택자가 적용됨 */
:scope {
padding: 1rem;
border-radius: 0.5rem;
}
/* .card 내부의 img만 대상 */
img {
width: 100%;
border-radius: 0.25rem;
}
h2 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
}
<div class="card">
<img src="photo.jpg" alt=""> <!-- @scope 적용됨 -->
<h2>카드 제목</h2> <!-- @scope 적용됨 -->
</div>
<img src="hero.jpg" alt=""> <!-- @scope 적용 안 됨 -->
스코프 하한(scope limit) 설정
@scope (상한) to (하한) 문법으로 특정 하위 요소까지만 스타일을 적용하고 그 안쪽은 제외할 수 있습니다.
/* ✅ .card 내부에서 .card-footer 이전까지만 적용 */
@scope (.card) to (.card-footer) {
p {
color: hsl(220 10% 40%);
line-height: 1.6;
}
}
<div class="card">
<p>이 단락에 적용됨</p> <!-- 스코프 내부 -->
<div class="card-footer">
<p>이 단락엔 적용 안 됨</p> <!-- 하한 하위 — 제외 -->
</div>
</div>
@scope와 명시도
@scope 내부 선택자는 외부와 동일한 명시도 계산을 따릅니다. 다만 **근접도(proximity)**라는 추가 기준이 있어, 동일 명시도일 때 DOM 트리에서 더 가까운 스코프 루트에서 나온 스타일이 이깁니다.
/* ❌ BEM 방식 — 접두어 반복이 피로함 */
.card { }
.card__title { }
.card__body { }
.card__footer { }
/* ✅ @scope 방식 — 자연스러운 선택자, 스코프로 격리 */
@scope (.card) {
:scope { }
.title { }
.body { }
.footer { }
}
⚠️ 주의 2024년 기준 Chrome 118+, Firefox 128+, Safari 17.4+ 에서 지원합니다. 프로덕션 적용 전 대상 브라우저 지원 범위를 확인하세요.
@supports또는 점진적 향상(progressive enhancement) 전략을 병행하세요.
상속 제어 키워드의 정확한 의미
CSS 값에는 inherit, initial, unset, revert, revert-layer 다섯 가지 전역 키워드가 있으며, 각각 다른 방식으로 값을 초기화하거나 상속합니다.
| 키워드 | 의미 |
|---|---|
inherit | 부모 요소의 계산된 값(computed value) 을 그대로 사용 |
initial | CSS 명세가 정의한 초기값으로 설정 (브라우저 기본이 아님) |
unset | 상속 가능한 프로퍼티면 inherit, 아니면 initial |
revert | 현재 origin(author)의 모든 선언을 무시하고, 더 낮은 origin(user, 그 다음 user-agent)에서 적용될 값으로 되돌림 (실무에선 user 스타일이 드물어 대개 UA 값으로 떨어짐) |
revert-layer | 현재 @layer의 선언을 롤백하여, 더 낮은 레이어(일치하는 선언이 없으면 언레이어드 및 하위 origin)에서 적용되었을 값으로 되돌림 |
inherit vs initial의 함정
/* color는 상속 가능 프로퍼티 */
.parent { color: hsl(220 90% 56%); }
.child-inherit { color: inherit; } /* 부모 색 hsl(220 90% 56%) 사용 */
.child-initial { color: initial; } /* CSS 명세 초기값: CanvasText (보통 검정) */
/* ❌ initial은 "브라우저 기본"이 아닌 "CSS 명세 초기값" — 다를 수 있음 */
/* display는 상속 불가 프로퍼티 */
.child-unset-display { display: unset; } /* initial과 동일: inline */
.child-unset-color { color: unset; } /* inherit과 동일: 부모 색 */
revert — 하위 origin(user, 그 다음 UA) 값으로 복귀
revert는 현재 origin(보통 author)의 선언이 없었던 것처럼 캐스케이드를 되돌립니다. 즉 author 선언을 모두 무시하고 더 낮은 origin인 user 스타일, user 스타일도 없으면 user-agent 스타일에서 적용될 값으로 떨어집니다. 실무에서는 사용자 정의 스타일시트(user 스타일)가 드물어 대개 UA 값으로 복귀하지만, user 스타일이 존재하면 그 값이 우선한다는 점이 revert의 정확한 의미입니다.
/* author 선언을 지우고 하위 origin(user → UA) 값으로 돌아가고 싶을 때 */
.reset-btn {
all: revert; /* author 스타일을 무시 → user 스타일이 없으면 버튼의 UA 기본 복원 */
}
/* ✅ 유용한 패턴: 컴포넌트 안에서 리스트 스타일 복원 */
.prose ul {
list-style: revert; /* author 선언 무시 → 하위 origin(보통 UA)의 disc bullet 복원 */
padding-left: revert; /* author 선언 무시 → 하위 origin(보통 UA)의 들여쓰기 복원 */
}
revert-layer — 레이어 간 되돌리기
@layer base, components;
@layer base {
.btn {
background: hsl(220 90% 56%);
color: white;
}
}
@layer components {
.btn-ghost {
background: revert-layer; /* base 레이어 이전으로 되돌림 → transparent */
color: revert-layer; /* base 이전 → CanvasText */
border: 2px solid currentColor;
}
}
💡 TIP
revert-layer는@layer기반 아키텍처에서 특정 레이어의 선언을 선택적으로 취소할 때 강력합니다.unset이 명세 초기값으로 가는 것과 달리,revert-layer는 현재 레이어의 선언을 롤백하여 그보다 낮은 레이어(일치하는 선언이 없으면 언레이어드 및 하위 origin)에서 적용되었을 값으로 되돌립니다. 반드시 직전 한 단계만 내려가는 것이 아니라, 일치하는 선언을 찾을 때까지 계속 아래로 내려갑니다.
캐스케이드 알고리즘 종합 시각화
실제로 브라우저가 값을 결정하는 흐름을 의사 코드로 표현하면 다음과 같습니다.
function resolveValue(element, property):
declarations = collectAll(element, property) // 모든 선언 수집
// 1단계: Origin + Importance 정렬
sort by ORIGIN_IMPORTANCE_RANK:
user-agent normal < user normal < author normal
author !important < user !important < user-agent !important
// 2단계: Context (shadow DOM 경계)
filter by context
// 3단계: 인라인 style="" 우선
if inline style exists: return inline value
// 4단계: @layer 정렬 (레이어 없는 것이 최우선)
sort by LAYER_ORDER (unlayered > last-declared-layer > ... > first-declared-layer)
// 5단계: 명시도
sort by SPECIFICITY (id > class/attr/pseudo-class > type/pseudo-element)
// 6단계: 소스 순서
return last declaration
이 흐름에서 명시도는 5번째 단계에 불과합니다. @layer 설계가 명시도보다 앞에 오기 때문에, 레이어 아키텍처를 잘 잡으면 명시도 전쟁 자체를 피할 수 있습니다.
요약
- 캐스케이드는 Origin+Importance, Context, 인라인 스타일, @layer, 명시도, 소스 순서의 6단계로 작동하며, 명시도는 5번째다.
@layer에서!important가 붙으면 레이어 우선순위가 역전되므로, 레이어 설계 시!important사용을 최소화해야 한다.:where()는 명시도 0을 유지하여 재정의가 쉬운 기본 스타일에 적합하고,:is()는 내부 가장 높은 명시도를 채택하여 복합 선택자 단순화에 적합하다.@scope는 특정 루트 하위에서만 적용되는 스타일 블록을 정의하며, 하한 설정으로 중첩된 컴포넌트를 격리할 수 있다.revert는 현재 origin(author) 선언을 무시하고 하위 origin(user → UA) 값으로(실무에선 대개 UA),revert-layer는 현재 레이어를 롤백하여 더 낮은 레이어(없으면 언레이어드·하위 origin)에서 적용되었을 값으로 되돌리며,unset은 상속 가능 여부에 따라inherit또는initial로 동작한다.- 레이어 기반 아키텍처(
reset → base → components → utilities)를 설계하면 명시도 전쟁 없이 예측 가능한 CSS를 유지할 수 있다.
연습문제
- 다음 코드에서
.title요소의 최종color값은 무엇이며, 그 이유를 캐스케이드 알고리즘 단계로 설명하세요.
@layer base, components;
@layer base {
.title { color: red; }
}
@layer components {
.title { color: blue; }
}
.title { color: green; }
힌트 레이어 밖 선언(unlayered)의 우선순위를 떠올려 보세요.
- 다음 두 코드 블록 중 어느 것이 외부 라이브러리 스타일을 더 안전하게 격리하는지 선택하고, 그 이유를 설명하세요.
/* 방법 A */
@import "library.css";
.my-component { color: red !important; }
/* 방법 B */
@layer library, app;
@import "library.css" layer(library);
@layer app { .my-component { color: red; } }
힌트
!important확장 문제와@layer의 레이어 간 우선순위를 비교하세요.
- 아래 코드에서
.card p의color값이 예상과 다르게 나올 수 있습니다. 문제를 찾고:where()를 사용해 수정하세요.
:is(#app .card, .card) p {
color: hsl(220 10% 40%);
}
.highlight {
color: hsl(0 80% 50%);
}
<div id="app">
<div class="card">
<p class="highlight">이 텍스트는 빨간색이어야 합니다.</p>
</div>
</div>
힌트
:is()내부에 ID 선택자가 있을 때 명시도가 어떻게 계산되는지 확인하세요.
revert와revert-layer의 차이를 코드 예제로 보여주고, 각각 어떤 상황에서 더 적합한지 설명하세요.
힌트
revert는 현재 origin(author)을 무시하고 하위 origin(user → UA)을,revert-layer는 현재 레이어를 롤백하여 더 낮은 레이어(없으면 언레이어드·하위 origin)를 기준으로 한다는 점을 떠올려 보세요.
💡 연습문제 풀이
불러오는 중…
댓글 0
“CSS 심화” 강좌에 대한 댓글입니다.