css · high priority
CSS 동작 원리 — Cascade · Specificity · BFC · Stacking Context
*"왜 이 스타일이 안 먹히는가"* 의 실제 원인을 설명하는 4 가지 모델
학습 개요
탄생 배경
쉬운 설명
복잡한 개념을 실생활 비유로 설명합니다.
“법원의 위계 + 건물 층수 + 독립 부지”
*Cascade* 는 법원의 위계입니다. 헌법(`!important`) → 대법원(`@layer` 순서) → 고등법원(ID) → 지방법원(class) → 1심(요소) 순으로 판단이 내려가고, *위 단계 한 표* 가 *아래 단계 천 표* 를 이깁니다. *Stacking Context* 는 건물의 층수와 같습니다 — 같은 건물 안에서는 *층 (z-index)* 으로 위아래를 정하지만 *건물 자체* 가 다르면 건물 위치(부모 Context 의 z-index) 로 우열이 정해집니다. 9999층짜리 건물도 뒷줄에 있으면 앞 건물의 1층보다 뒤에 있죠. *BFC* 는 *독립된 부지* 입니다 — 그 안의 건축 규제(margin·float) 가 부지 밖으로 새지 않고, 밖의 흐름도 부지를 침범하지 못합니다.
핵심 개념
두 규칙이 충돌했을 때 *위에서부터* 비교
Origin & Importance
*작성자(author) · 사용자(user) · 브라우저 기본(UA)* 의 origin 별 우선순위. 동일 origin 내에서 !important 가 먼저, 일반 선언이 다음.
Cascade Layers (`@layer`)
*나중에 선언된 레이어가 더 강함*. 레이어 밖(unlayered) 규칙이 *어떤 레이어보다도 강함*. !important 의 경우 *순서가 역전* 되어 *먼저 선언된 레이어가 더 강함*.
Specificity
(inline, ID, class/attr/pseudo-class, element/pseudo-element) 4 자리 수로 비교. *왼쪽 자리* 가 더 큰 쪽이 이긴다.
Source Order
*같은 specificity* 라면 *나중에 선언된* 규칙이 이긴다. 같은 파일이라면 아래쪽, import 순서라면 더 늦게 import 된 것.
`!important` 의 *진짜 위험*
!important 는 *Specificity 위* 의 단계다. 한 번 도입되면 *그 위를 덮으려면 또 다른 !important* 가 필요하다. 디자인 시스템 스타일을 한 줄 덮으려고 시작한 !important 가 1년 뒤 *프로덕션 전체에 퍼져* 누가 누구를 덮는지 추적 불가능해진다. 정답은 !important 가 아니라 *@layer 로 우선순위를 명시* 하거나 *Specificity 를 의도적으로 동등하게* 맞추는 것.
1/* 우선순위는 선언 순서로 결정 — 나중이 더 강함 */2@layer reset, base, components, utilities;34@layer reset {5 * { margin: 0; padding: 0; box-sizing: border-box; }6}78@layer base {9 body { font-family: system-ui; color: #111; }10}1112@layer components {13 /* 여기 들어간 .button 은 utilities 보다 약함 */14 .button { padding: 12px 16px; border-radius: 8px; }15}1617@layer utilities {18 /* Tailwind 스타일 유틸리티 — 컴포넌트보다 강함 */19 .p-4 { padding: 16px; }20 .text-center { text-align: center; }21}2223/* 레이어 밖 (unlayered) — 어떤 레이어보다도 강함 */24.urgent-override { color: red; }
실무 적용
어떤 상황에서 사용하는가
디자인 시스템과 Tailwind 유틸리티가 혼용된 모노레포에서 *모달 위에 떠야 할 토스트가 모달 뒤에 깔리고*, *컴포넌트 기본값이 유틸리티 클래스로도 안 덮이는* 두 종류 버그가 함께 들어왔다.
어떻게 적용하는가
(1) z-index 문제는 *Stacking Context 추적* — DevTools 의 *Layers* 패널이나 3D View 로 시각화하고, 모달의 조상 중 `transform`, `filter`, `opacity<1`, `isolation` 을 찾는다. 보통 모달 자체를 *React Portal 로 body 직속* 으로 옮기거나, 토스트 컨테이너에 `isolation: isolate` 를 붙여 *별도 우주* 로 분리한다. (2) 유틸리티가 컴포넌트를 못 덮는 문제는 specificity 싸움이 *결정적 원인* — `@layer reset, base, components, utilities;` 를 파일 가장 위에 선언하고, 컴포넌트 라이브러리의 모든 규칙을 `@layer components` 에 감싸 *유틸리티가 무조건 더 강한 레이어* 가 되도록 한다. (3) 기본값을 *언제나 약하게* 두고 싶으면 `:where(...)` 로 감싸 specificity 0 으로 만든다. (4) 새 마이그레이션이 어려우면 *부분 도입* — 가장 충돌이 잦은 폴더부터 `@layer` 적용. (5) `!important` 는 *디자인 시스템 정책* 으로 *금지* 한다 — 한 번 도입되면 전염된다.
흔한 실수와 안티패턴
- z-index 가 작동 안 할 때 *그 요소* 만 보고 *조상 Stacking Context* 를 안 본다.
- `opacity: 0.99` 같은 핵으로 새 Context 를 만든다 — `isolation: isolate` 를 모름.
- `!important` 한 번 도입 후 그것을 덮으려 *또 다른 `!important`* — 사슬화.
- `overflow: hidden` 으로 BFC 를 만들고는 *내부 콘텐츠가 잘리는* 부작용을 발견.
- Tailwind 와 컴포넌트 라이브러리의 *자연스러운 우선순위 싸움* 을 *@layer 없이* 해결하려 함.
- `:where()` 와 `:is()` 의 *Specificity 차이* 를 모르고 같다고 가정.
흔한 오해
*"Cascade 는 '나중에 쓴 게 이긴다' 가 전부"*
교정*마지막 단계만* 그렇다. 그 위에 Origin → Importance → Layer → Specificity 가 있고, 이 4 단계가 *모두 동일* 할 때만 source order 가 결정한다.
왜 중요CSS 표준은 *결정적* 으로 평가하기 위해 다단계 알고리즘을 둔다. 한 단계만 알면 *왜 안 먹히는지* 가 영영 미궁.
*"!important 는 '정말 중요한' 스타일에 쓰는 것"*
교정`!important` 는 *Specificity 위* 의 평면이라 *전염* 된다. 한 번 쓰면 그 위를 덮으려 또 `!important` 가 필요해지고, 이 사슬은 *모든 페이지* 로 퍼진다. 정답은 *@layer* 또는 *Specificity 동등 맞추기*.
왜 중요대형 코드베이스의 정책으로 `!important` 금지가 흔하다 — 명시 도구 (`@layer`) 가 더 안전한 대체재.
*"z-index 만 충분히 크면 항상 위에 뜬다"*
교정*같은 Stacking Context* 안에서만 z-index 가 의미 있다. 다른 Context 끼리는 *Context 자체의 순서* 가 우선. 9999 가 1 을 못 이기는 일이 일상이다.
왜 중요브라우저는 *Context 단위로 합성* 한다. 자식의 z-index 는 *Context 내부* 에서만 평가되고, *Context 끼리는 부모 레벨에서 비교* 된다.
*"BFC = overflow: hidden 으로 만든다"*
교정*결과적으로* BFC 가 만들어지는 *부작용* 일 뿐, *의도형 도구* 는 `display: flow-root` 다. `overflow: hidden` 은 *콘텐츠가 잘리는* 위험을 동반.
왜 중요`flow-root` 는 *오직 BFC 만 만든다* 는 한 가지 일을 한다 — 코드의 의도가 명확.
면접 질문
답변 방향 힌트
`(0, ID, class+attr+pseudo, element)` 자릿수.
반드시 언급할 키워드
- `#link` = 0,1,0,0
- `a.external` = 0,0,1,1
- `a[href]:hover` = 0,0,2,1
- `.external` = 0,0,1,0
- `a` = 0,0,0,1
- ID 자릿수 한 자리가 다른 자릿수의 합을 *모두 이김*
예상 꼬리 질문
- `:where()` 와 `:is()` 의 *Specificity 차이* 는?
- `@layer` 가 도입되기 전에는 *유틸리티 vs 컴포넌트 우선순위* 를 어떻게 잡았나요?
자기 점검
Cascade 평가 단계를 *위에서부터* 4 단계로 답하라.
기대 키워드
자주 하는 오해
*"나중에 선언한 게 이긴다"* — 마지막 단계에 불과. 그 위에 3 단계가 있다.
`(0,1,0,0)` 과 `(0,0,5,3)` 중 *어느 쪽이 이기는가* 와 그 이유.
기대 키워드
자주 하는 오해
*"오른쪽에 숫자가 많으니 (0,0,5,3) 이 이김"* — Specificity 는 *합* 이 아니라 *왼쪽 자리부터* 의 비교.
BFC 를 *부작용 없이* 만드는 가장 깔끔한 한 줄.
기대 키워드
자주 하는 오해
*"`overflow: hidden` 이 표준"* — 콘텐츠가 잘리는 부작용 위험.
*시각적 부작용 없이* 새 Stacking Context 를 만드는 한 줄.
기대 키워드
자주 하는 오해
*"`opacity: 0.99` 핵을 쓴다"* — 색이 흐려지는 부작용. 이젠 `isolation` 한 줄.
학습 자료
- MDN — CSS Cascade & InheritanceCascade 평가 단계와 Specificity 계산의 *정본* 문서.DocMDN
- MDN — SpecificitySpecificity 4 자리수 모델, `:where()`/`:is()` 차이.DocMDN
- MDN — Stacking ContextStacking Context 생성 트리거와 z-index 평가 규칙.DocMDN
- What The Heck, z-index?? Stacking Contexts*시각적 예시* 로 Stacking Context 와 z-index 버그 디버깅 정석.BlogJosh W Comeau
- MDN — `@layer`Cascade Layers 의 표준 사용법과 우선순위 규칙.DocMDN
- Cascade Layers Guide디자인 시스템·라이브러리 통합 시 `@layer` 실전 패턴.BlogCSS-Tricks
- MDN — `isolation``isolation: isolate` 로 부작용 없이 새 Stacking Context 만들기.DocMDN