CSS 완전 정복 — 특이성·캐스케이드·박스모델·스택킹·실무 패턴
이 글의 핵심
브라우저가 CSS를 적용하는 순서(캐스케이드), 규칙이 충돌할 때 이기는 조건(특이성), 레이아웃이 깨질 때 자주 등장하는 마진 병합·스택킹까지 한 번에 정리합니다. 라이브러리 없이도 스타일 버그를 예측·추적할 수 있게 하는 것이 목표입니다.
들어가며
CSS는 단순히 “선언을 모아 스타일을 입히는 언어”가 아니라, 캐스케이드(cascade)와 박스 모델, 스택킹(stacking)이라는 세 축 위에서 브라우저가 규칙을 순서대로 해석·충돌 해결·그리기까지 수행하는 시스템입니다. 프레임워크나 유틸리티 클래스를 쓰더라도, 디버깅 시점에는 결국 이 내부 동작을 알아야 “왜 이 색이 이겼는지”, “왜 z-index가 먹히지 않는지”를 설명할 수 있습니다.
이 글에서는 스펙 관점의 핵심만 추려 특이성 계산, 캐스케이드 알고리즘, 박스 모델과 마진 병합, 스택킹 컨텍스트와 z-index, 그리고 프로덕션에서 통하는 패턴을 연결해 설명합니다.
1. 선택자 특이성(Specificity) 계산
특이성은 “동일한 속성에 서로 다른 선언이 있을 때, 어떤 선언이 이기는지”를 수치화한 것입니다. 최신 CSS에서는 특이성이 3개의 컴포넌트(A, B, C)와 0과 같이 취급되는 행(row)로 표현됩니다.
1.1 A·B·C의 의미
| 구분 | 의미 | 예시 |
|---|---|---|
| A | ID 선택자의 개수 | #app → A=1 |
| B | 클래스, 속성, 의사 클래스 선택자 | .btn, [type="submit"], :hover |
| C | 타입(태그)·의사 요소 선택자 | div, ::before |
:is()와 :not() 등은 인자 안의 선택자를 풀어 위 규칙에 포함합니다. :is()는 인자 목록 중 가장 특이성이 높은 선택자와 동일한 특이성을 갖습니다. :where()는 인자에 ID·클래스가 있어도 항상 특이성 0으로 취급되어, 베이스 스타일을 덮어쓰기 쉽게 만듭니다.
1.2 계산 예시
/* A=0, B=1, C=1 → (0,1,1) */
article.card p { color: navy; }
/* A=1, B=0, C=1 → (1,0,1) — 위보다 ID가 있어 더 강함 */
#intro p { color: crimson; }
/* :is(.a, #b) → 인자 중 가장 특이성 높은 쪽과 동일 (#b가 있으면 ID 반영) */
:is(.foo, #bar) span { } /* #bar 때문에 A가 올라갈 수 있음 */
1.3 !important와 인라인 스타일
- 인라인 스타일(
style="")은 일반적으로 ID 하나보다도 강한 별도 층으로 취급됩니다(스펙에서는 특이성 튜플에 추가 행으로 표현). !important는 “중요도(importance)” 축에서 일반 선언보다 우선합니다. 같은!important끼리는 다시 특이성·소스 순서로 결정됩니다.
실무에서는 유지보수를 위해 !important를 남발하지 않으며, 충돌의 근본(선택자 설계·레이어 @layer)을 고치는 편이 안전합니다.
2. 박스 모델과 마진 병합(Margin Collapse)
2.1 표준 박스 모델과 box-sizing
기본 content-box에서는 width/height가 콘텐츠 박스만 가리킵니다. padding과 border는 그 바깥에 더해져 실제 차지 너비가 커집니다.
*,
*::before,
*::after {
box-sizing: border-box; /* width에 padding+border 포함 — 레이아웃 예측에 유리 */
}
border-box를 전역으로 두는 패턴은 “디자인 시스템에서 정한 너비”와 실제 렌더링 너비를 맞추기 쉽다는 이유로 널리 사용됩니다.
2.2 마진 병합이 일어나는 대표 경우
수직 방향으로 인접한 블록 레벨 박스의 마진은, 둘 중 더 큰 값 하나로 합쳐져 보입니다(둘 다 양수일 때).
- 형제 간: 위 요소의
margin-bottom과 아래 요소의margin-top이 병합될 수 있습니다. - 부모와 첫/마지막 자식: 부모의 상단 마진과 첫 자식의 상단 마진, 부모의 하단과 마지막 자식의 하단이 병합될 수 있습니다(부모에
padding/border/인플로우 콘텐츠/overflow가 다른 값이면 등 조건에 따라 달라짐). - 빈 블록: 상하 마진이 서로 병합되어 높이 0인 블록처럼 보일 수 있습니다.
2.3 병합을 끊는 실무적 방법
- padding 또는 border를 부모에 최소 1px이라도 준다(투명 border 등).
overflow를visible이 아닌 값으로 설정(컨텍스트에 따라).- Flex/Grid 컨테이너의 자식은 일반적인 블록 병합 규칙에서 벗어나는 경우가 많아, 레이아웃 루트를 플렉스로 두면 “의도치 않은 간격 붕괴”를 막기 쉽습니다.
/* 예: 세로 스택에서 마진 병합 대신 간격을 부모가 관리 */
.stack {
display: flex;
flex-direction: column;
gap: 1rem; /* gap은 margin collapse와 다른 메커니즘 */
}
3. 스택킹 컨텍스트(Stacking Context)와 z-index
3.1 그리기 순서의 직관
화면 위에 겹친 요소는 “나중에 그린 것이 위”처럼 보이지만, 실제로는 스택킹 컨텍스트 단위로 레이어가 나뉩니다. z-index는 같은 컨텍스트 안에서만 의미 있는 상대 순서입니다.
3.2 새 스택킹 컨텍스트를 만드는 흔한 조건
position이static이 아니고z-index가auto가 아닌 경우opacity가 1 미만transform,filter,perspective등이none이 아닌 경우(브라우저·속성별 세부는 스펙 참고)isolation: isolate- Flex/Grid 자식이며
z-index가auto가 아닌 경우 등
따라서 “모달에 z-index: 9999를 줬는데 헤더 아래에 있다”는 현상은, 모달의 조상 중 하나가 더 낮은 스택에 갇혀 있기 때문인 경우가 많습니다.
3.3 디버깅 체크리스트
- 문제 요소에서 위로 올라가며
position/opacity/transform/filter/isolation을 확인합니다. - 가장 가까운 스택킹 컨텍스트 루트를 찾으며, 그 안에서
z-index를 비교합니다. - 필요하면
isolation: isolate로 의도적으로 자식만의 스택을 만들거나, 모달을position: fixed+body직계 자식(포털)으로 옮깁니다.
.modal-root {
position: fixed;
inset: 0;
z-index: 50;
isolation: isolate; /* 내부 z-index 레이어를 이 루트 안으로 한정 */
}
4. CSS 캐스케이드 알고리즘
“캐스케이드”는 후보 선언들 중 한 속성에 대해 최종값을 하나 고르는 알고리즘입니다. 큰 흐름은 다음과 같습니다.
4.1 출처(Origin)와 중요도
- 사용자 에이전트(UA) 스타일
- 사용자(user) 스타일
- 작성자(author) 스타일 — 우리가 쓰는 스타일시트
각 출처는 !important가 뒤집는 순서가 다릅니다. 일반 선언은 author가 강하지만, !important는 사용자·UA 쪽이 우선순위 규칙이 다르게 설계되어 접근성(강제 대비 등)을 보호합니다.
4.2 한 속성에 대한 대략적 결정 순서
후보가 여러 개일 때:
- 선언 레이어(@layer) — 레이어 간 우선순위(나중에 선언된 레이어가 이전 레이어를 덮음 등, 스펙 규칙 따름)
- 중요도 —
!importantvs 일반 - 출처 — UA / user / author 규칙
- 특이성
- 소스 순서 — 같으면 문서상 뒤에 온 선언
4.3 @layer가 중요한 이유
프로젝트가 커질수록 “유틸리티가 컴포넌트를 덮어써야 한다” 같은 의도적 우선순위를 특이성 전쟁으로 풀면 유지보수가 어려워집니다. @layer로 토큰 → 컴포넌트 → 유틸 순서를 고정하면, 특이성을 키우지 않고도 설계 의도를 코드로 표현할 수 있습니다.
@layer reset, tokens, components, utilities;
@import "reset.css" layer(reset);
@import "tokens.css" layer(tokens);
@layer components {
.btn { /* … */ }
}
@layer utilities {
.text-center { text-align: center; }
}
5. 프로덕션 CSS 패턴
5.1 특이성을 낮추는 선택자
:where()로 감싸면:where()자체의 특이성은 0에 가깝게 유지되어, 리셋·베이스 스타일에 쓰기 좋습니다.:is()는 인자 중 가장 특이성 높은 선택자를 따릅니다.
5.2 컴포넌트 스코프와 데이터 속성
BEM처럼 단일 클래스 네임스페이스를 쓰거나, [data-variant="primary"]처럼 상태를 속성으로 분리하면 충돌을 줄이면서도 특이성을 (0,1,0) 수준으로 통일하기 쉽습니다.
5.3 논리 속성(Logical properties)
다국어·RTL 대응을 위해 margin-left 대신 margin-inline-start, width 대신 inline-size 등 논리 속성을 쓰면 방향 전환 시 수정 범위가 줄어듭니다.
5.4 clamp()로 반응형 타이포·간격
h1 {
font-size: clamp(1.5rem, 1rem + 2vw, 2.5rem);
}
뷰포트에 따라 최소·선호·최대를 한 줄로 묶어 미디어 쿼리 분기 수를 줄입니다.
5.5 컨테이너 쿼리(@container)
부모 컨테이너 너비에 따라 자식 레이아웃을 바꾸면, 재사용 카드 UI가 뷰포트가 아닌 배치 맥락에 맞게 반응합니다.
.card {
container-type: inline-size;
}
@container (min-width: 400px) {
.card__body { display: flex; }
}
6. 정리
- 특이성은 ID·클래스·타입의 개수로 비교하며,
:where()·:is()는 각각 특이성을 낮추거나 인자 기준으로 계산합니다. - 마진 병합은 블록 흐름의 수직 마진에서 자주 발생하며, flex/grid·gap·padding 등으로 설계하면 예측 가능한 간격을 얻기 쉽습니다.
z-index는 스택킹 컨텍스트 안에서만 유효하므로, “안 올라간다”면 조상의transform/opacity/필터부터 의심합니다.- 캐스케이드는 레이어·중요도·출처·특이성·순서의 합이며,
@layer로 팀 단위 우선순위를 표현하는 것이 실무에 잘 맞습니다. - 프로덕션에서는 논리 속성,
clamp, 컨테이너 쿼리, 낮은 특이성 베이스 등으로 확장 가능한 스타일 계층을 만드는 것이 좋습니다.
브라우저 개발자 도구의 “Computed” 탭에서 최종값과 승리한 규칙을 함께 보면, 이 글의 개념이 그대로 디버깅 UI로 연결됩니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. CSS 특이성 계산, 캐스케이드 알고리즘, 박스모델·마진 병합, 스택킹 컨텍스트·z-index, @layer·:where 등 프로덕션 패턴을 엔진 관점에서 정리한 고급 가이드입니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- CSS 기초 | 선택자, 속성, 색상, 폰트
- [2026] CSS Grid 심화 — 트랙 크기·자동 배치·minmax·fr 내부 원리와 실무 패턴
- CSS 박스 모델 | Margin, Padding, Border 완벽 정리
이 글에서 다루는 키워드 (관련 검색어)
CSS, 특이성, 캐스케이드, 스택킹 컨텍스트, 박스모델, 프론트엔드 등으로 검색하시면 이 글이 도움이 됩니다.