🎨 Frontend CSS1. Cascade & Specificity04 — Cascade Layers (@layer)

04 — Cascade Layers

한 줄 답: @layerspecificity보다 먼저 보는 추가 우선순위 축이다. 같은 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가 있다. 🔥

!importantnon-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 — @layerCSS 모듈 시스템의 시작

JS는 ES modules로 import 그래프를 갖췄지만, CSS는 모든 파일이 하나의 평평한 글로벌로 합쳐졌다. 이는 1996년의 문서 스타일링 시절에는 충분했지만, 컴포넌트 라이브러리·디자인 토큰·다크모드가 동시 존재하는 현대엔 부족했다.

@layer는 CSS에 처음으로 모듈성에 가까운 개념을 줬다.

JS의 모듈CSS의 layer공통점
import { x } from "lib"@import url(lib) layer(...)격리
tree shakingunused layer 제거무의미한 게 안 적용
dependency hierarchylayer order명시적 우선순위

다만 진짜 모듈은 아니다 — @layer는 여전히 글로벌이고, layer 안의 selector가 다른 DOM에 매칭되는 것을 막진 못한다. 그건 다음 문서의 @scope가 한다.

흥미로운 사실: @layer는 Tab Atkins(W3C CSSWG 멤버, 동시에 Google Chrome 팀)의 블로그 글에서 진지하지 않게 시작된 아이디어였다. 그가 “CSS의 cascade를 디자이너가 직접 다룰 수 있다면 어떨까?”를 농담처럼 던졌고, 1년 뒤 명세 초안이 나왔다. 2022년 4월 Chrome 99에 안착하며 모든 메이저 브라우저가 동시에 지원한 드문 사례가 되었다 (Safari/Firefox도 같은 분기).


요약 + Mermaid

  • @layercascade의 4라운드 — specificity보다 먼저 결판.
  • 나중에 선언된 layer가 normal에서 이긴다, !important에선 먼저 선언이 이긴다.
  • unlayered는 모든 layer보다 강함 — 새 도입 시 주의.
  • @import url(x) layer(name)으로 외부 라이브러리 격리.
  • Tailwind v4가 !important 의존을 버리고 @layer로 옮긴 것이 현시점 최고의 사례.

다음: 05-scope에서 @scope·:has()·:is/:where로 컴포넌트 격리를 본다.