FEInterview Prep

css · high priority

CSS 동작 원리 — Cascade · Specificity · BFC · Stacking Context

*"왜 이 스타일이 안 먹히는가"* 의 실제 원인을 설명하는 4 가지 모델

intermediate 난이도5시간토스카카오네이버당근배민라인쿠팡
시작 전
이해도
매우 낮음

학습 개요

탄생 배경

쉬운 설명

복잡한 개념을 실생활 비유로 설명합니다.

법원의 위계 + 건물 층수 + 독립 부지

*Cascade* 는 법원의 위계입니다. 헌법(`!important`) → 대법원(`@layer` 순서) → 고등법원(ID) → 지방법원(class) → 1심(요소) 순으로 판단이 내려가고, *위 단계 한 표* 가 *아래 단계 천 표* 를 이깁니다. *Stacking Context* 는 건물의 층수와 같습니다 — 같은 건물 안에서는 *층 (z-index)* 으로 위아래를 정하지만 *건물 자체* 가 다르면 건물 위치(부모 Context 의 z-index) 로 우열이 정해집니다. 9999층짜리 건물도 뒷줄에 있으면 앞 건물의 1층보다 뒤에 있죠. *BFC* 는 *독립된 부지* 입니다 — 그 안의 건축 규제(margin·float) 가 부지 밖으로 새지 않고, 밖의 흐름도 부지를 침범하지 못합니다.

핵심 개념

두 규칙이 충돌했을 때 *위에서부터* 비교

1
Origin & Importance

*작성자(author) · 사용자(user) · 브라우저 기본(UA)* 의 origin 별 우선순위. 동일 origin 내에서 !important 가 먼저, 일반 선언이 다음.

2
Cascade Layers (`@layer`)

*나중에 선언된 레이어가 더 강함*. 레이어 밖(unlayered) 규칙이 *어떤 레이어보다도 강함*. !important 의 경우 *순서가 역전* 되어 *먼저 선언된 레이어가 더 강함*.

3
Specificity

(inline, ID, class/attr/pseudo-class, element/pseudo-element) 4 자리 수로 비교. *왼쪽 자리* 가 더 큰 쪽이 이긴다.

4
Source Order

*같은 specificity* 라면 *나중에 선언된* 규칙이 이긴다. 같은 파일이라면 아래쪽, import 순서라면 더 늦게 import 된 것.

`!important` 의 *진짜 위험*

!important 는 *Specificity 위* 의 단계다. 한 번 도입되면 *그 위를 덮으려면 또 다른 !important* 가 필요하다. 디자인 시스템 스타일을 한 줄 덮으려고 시작한 !important 가 1년 뒤 *프로덕션 전체에 퍼져* 누가 누구를 덮는지 추적 불가능해진다. 정답은 !important 가 아니라 *@layer 로 우선순위를 명시* 하거나 *Specificity 를 의도적으로 동등하게* 맞추는 것.

`@layer` 로 *팀 단위 우선순위* 를 잡는 패턴css
1/* 우선순위는 선언 순서로 결정 — 나중이 더 강함 */
2@layer reset, base, components, utilities;
3
4@layer reset {
5 * { margin: 0; padding: 0; box-sizing: border-box; }
6}
7
8@layer base {
9 body { font-family: system-ui; color: #111; }
10}
11
12@layer components {
13 /* 여기 들어간 .button 은 utilities 보다 약함 */
14 .button { padding: 12px 16px; border-radius: 8px; }
15}
16
17@layer utilities {
18 /* Tailwind 스타일 유틸리티 — 컴포넌트보다 강함 */
19 .p-4 { padding: 16px; }
20 .text-center { text-align: center; }
21}
22
23/* 레이어 밖 (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 단계로 답하라.

기대 키워드

Origin`!important``@layer`SpecificitySource order

자주 하는 오해

*"나중에 선언한 게 이긴다"* — 마지막 단계에 불과. 그 위에 3 단계가 있다.

`(0,1,0,0)` 과 `(0,0,5,3)` 중 *어느 쪽이 이기는가* 와 그 이유.

기대 키워드

ID 자릿수왼쪽 자리 우선자릿수 가중치

자주 하는 오해

*"오른쪽에 숫자가 많으니 (0,0,5,3) 이 이김"* — Specificity 는 *합* 이 아니라 *왼쪽 자리부터* 의 비교.

BFC 를 *부작용 없이* 만드는 가장 깔끔한 한 줄.

기대 키워드

`display: flow-root`

자주 하는 오해

*"`overflow: hidden` 이 표준"* — 콘텐츠가 잘리는 부작용 위험.

*시각적 부작용 없이* 새 Stacking Context 를 만드는 한 줄.

기대 키워드

`isolation: isolate`

자주 하는 오해

*"`opacity: 0.99` 핵을 쓴다"* — 색이 흐려지는 부작용. 이젠 `isolation` 한 줄.

학습 자료