04 — Cascade Layers
한 줄 답:
@layer는 specificity보다 먼저 보는 추가 우선순위 축이다. 같은 origin 안에서 나중에 선언된 layer가 이긴다 — 그래서 라이브러리의#id #id .class도 우리 layer의.btn에 진다. Tailwind v4, Open Props, 모든 모던 reset이@layer로 통째로 감싸는 이유.
Why — !important 폭주를 끝내기 위해
2010년대 모든 프론트엔드 팀이 같은 사고를 겪었다.
1. Bootstrap을 깐다.
2. 디자인을 맞추려고 .btn-primary를 덮어쓴다.
3. 안 먹힌다. specificity 부족.
4. !important를 붙인다. ✅
5. 다른 곳에서 hover state를 덮어야 한다.
6. !important + 더 긴 selector. ✅
7. 3개월 후, 모든 줄에 !important가 있다. 🔥!important는 non-important 영역에서 specificity를 더 쓰지 않으려는 회피책이지만, 부작용이 컸다 — 접근성 사용자의 스타일까지 무력화한다 (사용자 normal은 작성자 important에 진다).
근본 원인은: CSS에 “작성자 안에서의 우선순위”를 표현할 문법이 없었다. Bootstrap도, 우리 코드도, MUI도 모두 같은 author origin에 들어가서 specificity와 order로만 싸웠다.
@layer(2022)는 이 한 가지를 해결한다. 작성자 안에 논리적 origin을 만들 수 있다.
How — @layer 동작 규칙
1) 기본 문법
/* 1. 순서 먼저 선언 (선택) */
@layer reset, base, framework, components, utilities;
/* 2. 각 layer에 규칙 추가 */
@layer reset {
* { margin: 0; padding: 0; }
}
@layer framework {
.btn { color: white; background: blue; }
}
@layer utilities {
.text-red { color: red; }
}우선순위:
unlayered (가장 강함)
↓
utilities (가장 늦게 선언된 layer)
↓
components
↓
framework
↓
base
↓
reset (가장 먼저 선언된 layer, 가장 약함)같은 origin·important 분류 안에서 나중에 선언된 layer가 이긴다. @layer ... { } 밖의 규칙(unlayered)은 모든 layer보다 강하다.
2) layer 안에서는 specificity가 작동
@layer base {
button { color: blue; } /* (0, 0, 1) */
.btn { color: red; } /* (0, 1, 0) — 이김 in base */
}layer 내부는 일반 cascade — specificity와 order로 결판. layer는 layer끼리만 비교된다.
3) layer 간에는 specificity가 무력
@layer framework {
#cta { color: white; } /* (1, 0, 0) — framework에선 강함 */
}
@layer site {
.btn { color: red; } /* (0, 1, 0) — site가 더 늦은 layer */
}
/* → .btn이 이긴다. ID도 layer 차이엔 못 이긴다 */4) 순서 미리 선언이 핵심
/* 권장 */
@layer reset, base, theme, components, utilities;
/* 이후 어디서든 추가 가능 */
@layer components {
.card { ... }
}
@layer base {
body { ... }
}미리 선언하면 파일 로드 순서에 무관하게 우선순위 고정. 안 하면 최초 등장 순으로 정해진다.
5) 익명 layer
@layer {
.foo { color: red; } /* 익명 — 다시 추가 불가 */
}이름 없이 쓰면 그 자리에서만 존재하는 layer 생성. 나중에 같은 layer에 추가 불가. 보통 안 쓴다.
6) 중첩
@layer framework {
@layer base, components;
@layer base {
body { ... }
}
@layer components {
.card { ... }
}
}내부 layer 이름은 framework.base, framework.components로 경로가 된다. 중첩된 layer끼리 비교될 때만 내부 순서가 본다.
7) import 시 layer 지정
@import url("bootstrap.css") layer(framework);
@import url("my-styles.css") layer(site);Bootstrap을 통째로 framework layer에 박을 수 있다. 외부 라이브러리를 layer로 격리하는 핵심 도구.
8) !important는 layer 순서를 역전시킨다
@layer base, theme;
@layer base {
.btn { color: red !important; }
}
@layer theme {
.btn { color: blue !important; }
}
/* → red 이김 (먼저 선언된 layer의 important가 강함) */이유: !important의 정신은 “근본적인 안전장치 보호”. reset/base가 의도적으로 박은 !important(예: box-sizing: border-box)는 후속 layer가 깨뜨릴 수 없어야 한다.
실무 권장: layer를 쓰면 !important를 안 써도 되므로, 가급적 안 쓴다.
What — 실전 layer 설계
패턴 1: 기본 5-layer 구조
@layer reset, base, tokens, components, utilities;
@import "modern-normalize.css" layer(reset);
@import "open-props" layer(tokens);
@layer base {
body { font-family: var(--font-sans); }
h1, h2, h3 { line-height: 1.2; }
}
@layer components {
.card { ... }
.btn { ... }
}
@layer utilities {
.text-red { color: red !important; } /* utilities는 강제로 이김 */
.hidden { display: none !important; }
}순서를 의미상 정렬:
reset: 0점. 모든 게 평평하게base: 요소 type 기본 (h1, p, …)tokens: 토큰만 정의 (:root { --color-... })components: 컴포넌트 스타일utilities: 유틸리티가 컴포넌트를 이김
패턴 2: 외부 라이브러리 격리
@layer external, app;
@import url("bootstrap.css") layer(external);
@import url("antd.css") layer(external);
@layer app {
.btn-primary { background: var(--brand); } /* Bootstrap 덮음 */
}app layer에 어떤 selector도 — .btn-primary 하나로 — Bootstrap의 #wrap .btn.btn-primary:hover까지 이긴다.
패턴 3: Tailwind v4의 구조
/* Tailwind v4가 자동 생성 */
@layer theme, base, components, utilities;
@layer theme {
:root { --color-red-500: oklch(...); }
}
@layer base {
/* preflight */
}
@layer components {
/* @apply로 정의된 컴포넌트 */
}
@layer utilities {
.text-red-500 { color: var(--color-red-500); }
}v3까지는 !important로 utility를 강제했지만, v4는 @layer 순서로 자연스럽게 utility가 이긴다. 베스트 프랙티스가 표준화된 사례.
패턴 4: 다크모드 + layer
@layer tokens, components;
@layer tokens {
:root { --bg: white; --fg: black; }
.dark { --bg: black; --fg: white; }
}
@layer components {
body { background: var(--bg); color: var(--fg); }
}토큰만 따로 layer로 빼두면 디자인 시스템 교체가 layer 하나 교체로 가능.
패턴 5: A/B 테스트
@layer experiment, default;
@layer default {
.btn { background: blue; }
}
@layer experiment {
.btn { background: green; }
}experiment layer를 js로 동적 추가/제거하면 A/B 토글이 된다. revert-layer로 깔끔하게 해제 가능.
What-if — layer 사고들
사고 1: 순서를 까먹어서 layer가 역순으로 정해짐
/* bad — 순서 미리 안 정함 */
/* a.css */
@layer components { ... }
/* b.css */
@layer reset { ... }a.css가 먼저 로드되면 components가 first layer가 되어 가장 약함. reset이 components를 이긴다. 의도와 반대.
해결: 진입점 CSS 최상단에서 항상
@layer reset, base, components, utilities;먼저 한 줄 박는다.
사고 2: unlayered 코드가 layer를 다 이김
@layer framework {
.btn { color: blue; }
}
.btn { color: red; } /* unlayered — 무조건 이김 */새로 layer 도입하는 레거시 프로젝트에서 자주 발생. unlayered는 “어디에도 안 속함”이 아니라 *“최강 layer”*다.
해결: 모든 author CSS를 layer 안에 넣는다.
사고 3: !important로 layer를 깨려다 역전됨
@layer base, theme;
@layer base {
.btn { color: red !important; } /* 가장 먼저 → important에선 가장 강함 */
}
@layer theme {
.btn { color: blue !important; } /* 가장 늦게 → important에선 가장 약함 */
}
/* → 의도: theme가 이김. 실제: base가 이김. red */@layer + !important는 직관과 반대로 작동한다. 헷갈리면 layer에 important 쓰지 말 것.
사고 4: shadow DOM과 layer
Shadow DOM 내부의 @layer는 외부 layer와 분리된다. host 페이지의 @layer와 섞이지 않는다 — context(라운드 2)에서 결판나기 때문.
사고 5: layer 안에서 specificity는 여전히 작동
@layer base {
.a .b .c { color: red; }
.x { color: blue; }
}layer 내부는 일반 cascade — .a .b .c (0,3,0)이 .x (0,1,0)를 이긴다. layer는 specificity를 대체하지 않고 그 앞에 추가될 뿐.
Insight — @layer는 CSS 모듈 시스템의 시작
JS는 ES modules로 import 그래프를 갖췄지만, CSS는 모든 파일이 하나의 평평한 글로벌로 합쳐졌다. 이는 1996년의 문서 스타일링 시절에는 충분했지만, 컴포넌트 라이브러리·디자인 토큰·다크모드가 동시 존재하는 현대엔 부족했다.
@layer는 CSS에 처음으로 모듈성에 가까운 개념을 줬다.
| JS의 모듈 | CSS의 layer | 공통점 |
|---|---|---|
import { x } from "lib" | @import url(lib) layer(...) | 격리 |
| tree shaking | unused layer 제거 | 무의미한 게 안 적용 |
| dependency hierarchy | layer order | 명시적 우선순위 |
다만 진짜 모듈은 아니다 — @layer는 여전히 글로벌이고, layer 안의 selector가 다른 DOM에 매칭되는 것을 막진 못한다. 그건 다음 문서의 @scope가 한다.
흥미로운 사실: @layer는 Tab Atkins(W3C CSSWG 멤버, 동시에 Google Chrome 팀)의 블로그 글에서 진지하지 않게 시작된 아이디어였다. 그가 “CSS의 cascade를 디자이너가 직접 다룰 수 있다면 어떨까?”를 농담처럼 던졌고, 1년 뒤 명세 초안이 나왔다. 2022년 4월 Chrome 99에 안착하며 모든 메이저 브라우저가 동시에 지원한 드문 사례가 되었다 (Safari/Firefox도 같은 분기).
요약 + Mermaid
@layer는 cascade의 4라운드 — specificity보다 먼저 결판.- 나중에 선언된 layer가 normal에서 이긴다,
!important에선 먼저 선언이 이긴다. - unlayered는 모든 layer보다 강함 — 새 도입 시 주의.
@import url(x) layer(name)으로 외부 라이브러리 격리.- Tailwind v4가
!important의존을 버리고@layer로 옮긴 것이 현시점 최고의 사례.
다음: 05-scope에서 @scope·:has()·:is/:where로 컴포넌트 격리를 본다.