Cascading and inheritance/Introduction

김동현·2026년 3월 18일

mdn 학습 번역 - CSS

목록 보기
50/190

CSS 캐스케이드(Cascade) 입문 (Introduction to the CSS cascade)

안녕하세요! 프론트엔드 개발의 세계에 오신 것을 환영합니다. 오늘 다룰 내용은 CSS의 영혼이자 심장과도 같은 개념, 바로 캐스케이드(Cascade, 폭포수)입니다.

캐스케이드는 브라우저(User Agent)가 여러 출처(source)에서 온 스타일 속성값들을 어떻게 결합할지 결정하는 핵심 알고리즘입니다. 하나의 요소에 대해 여러 출처(origin)캐스케이드 레이어(cascade layer), 혹은 @scope 블록에서 스타일이 동시에 선언되어 충돌할 때, 어느 것이 최종적으로 이기는지(우선순위)를 바로 이 캐스케이드가 정리해 주죠.

'Cascading Style Sheets(종속형 스타일시트)'라는 이름 자체에서 알 수 있듯, 캐스케이드는 CSS의 근간을 이룹니다. 어떤 요소에 여러 스타일 선택자(selector)가 겹칠 때, 아무리 명시도(specificity)가 높은 촘촘한 선택자라고 하더라도, 우선순위가 높은 '출처'나 '레이어'에서 선언된 속성값이 최종 승리하여 화면에 적용됩니다.

이 글에서는 캐스케이드가 도대체 무엇인지, CSS 선언(declarations)들이 어떤 순서로 흘러내리는지(cascade), 그리고 캐스케이드 레이어와 출처 유형(origin type)에 대해 자세히 알아볼 것입니다. 출처의 우선순위를 이해하는 것이야말로 캐스케이드를 마스터하는 열쇠랍니다!

💡 강사님의 꿀팁: "왜 내 CSS가 안 먹히지?"라며 모니터 앞에서 머리를 쥐어뜯어 본 적 있으신가요? 십중팔구 이 '캐스케이드'와 '명시도'의 룰을 어겼기 때문입니다. 이 개념만 확실히 잡으면 CSS 디버깅 시간이 절반으로 줄어들 거예요!


출처 유형 (Origin types)

CSS 캐스케이드 알고리즘의 주 임무는 다양한 곳에서 날아온 CSS 선언들 중 어떤 녀석의 값을 CSS 속성으로 최종 낙점할지 선택하는 것입니다. CSS 선언은 크게 세 가지 출처(origin type)에서 옵니다: 사용자 에이전트 스타일시트(User-agent stylesheets), 제작자 스타일시트(Author stylesheets), 그리고 사용자 스타일시트(User stylesheets)입니다.

이 스타일시트들은 각기 다른 출처에서 오고, 또 각 출처 안에서도 여러 레이어(layers)로 나뉠 수 있지만, 기본적으로 같은 요소(scope)를 두고 겹치기 마련입니다. 이것이 문제없이 돌아가게 하려고 캐스케이드 알고리즘이 이들의 상호작용 규칙을 정해둔 것이죠. 상호작용을 알아보기 전에, 이 핵심 용어들부터 정의하고 넘어가겠습니다.

사용자 에이전트 스타일시트 (User-agent stylesheets)

브라우저(User-agent)들은 아무런 CSS를 작성하지 않아도 HTML 문서가 보기 좋게 렌더링 될 수 있도록 기본 스타일을 가지고 있습니다. 이를 사용자 에이전트 스타일시트라고 부릅니다. 대부분의 브라우저는 실제 스타일시트 파일을 내장하고 있고, 일부는 코드로 이를 시뮬레이션하기도 하지만 결과는 똑같습니다.

일부 브라우저는 사용자가 이 기본 스타일시트를 직접 수정할 수 있게 허용하기도 하지만, 흔한 일도 아니고 개발자가 통제할 수 있는 영역도 아닙니다.

이 기본 스타일시트들은 HTML 명세에 의해 어느 정도 규격화되어 있긴 하지만, 브라우저 제조사마다 해석과 구현의 자유도가 꽤 높습니다. 즉, 브라우저마다 렌더링 결과가 미세하게 다를 수 있다는 뜻이죠! 개발자들은 이 골치 아픈 차이를 없애기 위해 웹 개발을 시작할 때 normalize.css와 같은 'CSS 리셋(reset)' 스타일시트를 사용하여 모든 브라우저의 기본값을 일관된 상태로 초기화한 뒤 본격적인 디자인을 시작합니다.

이 사용자 에이전트 스타일시트에 !important가 붙어있는 아주 예외적인 경우가 아니라면, 우리가 작성하는 제작자 스타일(리셋 스타일시트 포함)은 선택자의 구체성(specificity)과 상관없이 무조건 브라우저 기본 스타일보다 우선합니다.

제작자 스타일시트 (Author stylesheets)

제작자 스타일시트는 우리가 가장 흔하게 접하는 스타일시트입니다. 바로 웹 개발자인 여러분이 직접 작성하는 스타일이죠! 이 스타일들은 위에서 말했듯 브라우저 기본 스타일을 초기화(reset)하기도 하고, 웹페이지나 애플리케이션의 화려한 디자인을 정의합니다. 웹 개발자는 링크된 외부 CSS 파일(.css), HTML 내의 <style> 태그 블록, 그리고 태그에 직접 적는 style 속성(인라인 스타일)을 사용해 문서의 스타일을 구축합니다. 우리가 만드는 웹사이트의 테마와 룩앤필(look and feel)이 여기서 탄생하죠.

사용자 스타일시트 (User stylesheets)

대부분의 브라우저에서 웹사이트를 방문하는 '사용자(reader)'는 자신의 취향이나 필요에 맞게 디자인을 덮어쓸 수 있는 맞춤형 사용자 스타일시트를 적용할 수 있습니다. 예를 들어, 시력이 안 좋은 사용자가 모든 글씨 크기를 강제로 키우거나 고대비 색상으로 바꾸는 식이죠. 브라우저에 따라 사용자 스타일을 직접 설정하거나 브라우저 확장 프로그램(extension)을 통해 추가할 수 있습니다.

캐스케이드 레이어 (Cascade layers)

캐스케이드의 순서는 가장 먼저 '출처(origin type)'를 기준으로 정해집니다. 그리고 같은 출처 내에서는 캐스케이드 레이어(cascade layers)가 선언된 순서를 따릅니다.
모든 출처(브라우저, 개발자, 사용자)에서 스타일은 이름을 가진 레이어나 이름 없는(익명) 레이어 안에 넣을 수도 있고, 아예 레이어 밖에 둘 수도 있습니다. layer, layer() 또는 @layer를 사용하면 스타일이 지정된 이름의 레이어(이름을 안 쓰면 익명 레이어)에 쏙 들어갑니다. 레이어 밖에 선언된 스타일들은 암묵적으로 '가장 마지막에 선언된 익명 레이어'에 들어있는 것처럼 취급되어 가장 강력한 힘을 발휘합니다.

각 출처 유형 내의 레이어 규칙을 깊이 파보기 전에, 먼저 이 '출처'들이 어떤 순서로 맞붙는지(Cascading order)부터 전체적으로 조망해 볼까요?


캐스케이딩 순서 (Cascading order)

캐스케이딩 알고리즘은 문서의 각 요소가 가진 수많은 속성에 대해 최종적으로 어떤 값을 적용할지 찾아내는 심판과 같습니다. 알고리즘은 다음 단계를 순서대로 밟아갑니다:

  1. 관련성 (Relevance): 우선, 이곳저곳에서 모인 수많은 규칙 중 '현재 요소에 실제로 적용될 수 있는' 규칙들만 남기고 다 버립니다. 즉, 선택자(selector)가 해당 요소와 일치하고, 현재 기기 환경에 맞는 @media 조건 안에 있는 규칙들만 골라내는 과정입니다.

  2. 출처와 중요도 (Origin and importance): 살아남은 규칙들을 '중요도(!important가 붙었는지 여부)'와 '출처(Origin)'에 따라 정렬합니다. (지금은 일단 레이어 개념은 빼고 생각할게요.) 캐스케이드 순서는 다음과 같습니다. (번호가 클수록 우선순위가 높습니다!)

    우선순위 (낮음 -> 높음)출처 (Origin)중요도 (Importance)
    1사용자 에이전트 (브라우저 기본)일반 (normal)
    2사용자 (방문자 세팅)일반 (normal)
    3제작자 (개발자 작성)일반 (normal)
    4CSS keyframe 애니메이션
    5제작자 (개발자 작성)!important
    6사용자 (방문자 세팅)!important
    7사용자 에이전트 (브라우저 기본)!important
    8CSS transitions (트랜지션)
  3. 명시도 (Specificity): 만약 2번 단계에서 '출처와 중요도'가 완전히 똑같은 규칙들이 여럿 있다면, 이번엔 선택자의 명시도(specificity)를 비교합니다. ID가 많은지, 클래스가 많은지 등을 따져서 더 구체적인(높은 명시도를 가진) 선택자의 선언이 승리합니다.

  4. 스코프 근접성 (Scoping proximity): 출처도 같고 명시도까지 똑같은 두 선택자가 있다면, @scope 규칙 안에서 스코프 루트(scope root)와의 DOM 계층 홉(hop) 수가 더 적은(더 가까운) 속성값이 승리합니다.

  5. 작성 순서 (Order of appearance): 출처, 중요도, 명시도, 스코프 근접성까지 모든 조건이 무승부라면? 가장 마지막에 작성된(아래쪽에 있는) 코드가 최종 승리합니다.

캐스케이드 알고리즘은 오름차순(뒤로 갈수록 힘이 셈)입니다. 요약하자면:

  • CSS 애니메이션(@keyframes)은 일반적인 스타일(브라우저, 사용자, 개발자 모두 포함)보다 힘이 셉니다.
  • !important가 붙은 값은 애니메이션보다 힘이 셉니다.
  • 트랜지션(Transitions)은 !important가 붙은 값마저 무시하고 가장 강력한 힘을 발휘합니다.

💡 참고: 트랜지션과 애니메이션

  • 애니메이션(@keyframes)으로 설정된 값은 (!important가 없는) 모든 일반 스타일을 압도합니다.
  • transition을 통해 부드럽게 변하는 중인 값은 심지어 !important가 붙은 값마저 누르고 최우선으로 적용됩니다.

캐스케이드 알고리즘(2번 단계)은 명시도 알고리즘(3번 단계)보다 먼저 적용됩니다. 이게 무슨 뜻이냐면, 사용자(방문자) 스타일시트에서 명시도가 엄청나게 높은 :root p { color: red; }를 작성했더라도, 개발자가 작성한 명시도가 낮은 p { color: blue; }가 있다면(둘 다 !important가 없을 때), 개발자(Author) 출처가 사용자(User) 출처보다 우선순위가 높기 때문에 글자는 '파란색(blue)'이 된다는 것입니다. (명시도 싸움까지 가지도 않고 출처에서 끝납니다!)


기본 예제 (Basic example)

캐스케이드 레이어가 이 흐름에 어떻게 끼어드는지 자세히 보기 전에, 지금까지 배운 출처(Origin)들을 바탕으로 알고리즘이 어떻게 돌아가는지 실제 코드로 살펴볼까요?

HTML 문서를 렌더링하는데 브라우저 기본 스타일시트 1개, 개발자가 작성한 스타일시트 2개, 그리고 사용자가 적용한 커스텀 스타일시트 1개가 얽혀있다고 쳐봅시다. (HTML 내에 인라인 스타일은 없습니다.)

사용자 에이전트(브라우저) CSS:

li {
  margin-left: 10px;
}

개발자(Author) CSS 1:

li {
  margin-left: 0;
} /* 리셋 스타일 */

개발자(Author) CSS 2:

@media screen {
  li {
    margin-left: 3px;
  }
}

@media print {
  li {
    margin-left: 1px;
  }
}

@layer namedLayer {
  li {
    margin-left: 5px;
  }
}

사용자(User) CSS:

.specific {
  margin-left: 1em;
}

HTML:

<ul>
  <li class="specific">1<sup>st</sup></li>
  <li>2<sup>nd</sup></li>
</ul>

여기서 우리의 관심사인 <li class="specific"> 요소에는 과연 어떤 margin-left 값이 적용될까요?
아까 배운 알고리즘 5단계를 차례대로 밟아봅시다.

  1. 관련성 (Relevance)
    @media print 안에 있는 1px 규칙은 인쇄할 때만 적용됩니다. 화면(screen)으로 보고 있는 지금은 관련성이 없으므로 제일 먼저 탈락합니다.

  2. 출처와 중요도 (Origin and importance)
    어떤 규칙에도 !important가 붙지 않았습니다. 따라서 우선순위는 개발자 > 사용자 > 브라우저 순서가 됩니다. 이 출처 룰에 따라 브라우저의 10px과 사용자의 1em은 미련 없이 탈락합니다.
    (사용자 CSS의 .specificli보다 명시도가 높지만, 명시도는 3단계에서 따지기 때문에 출처 단계인 2단계에서 개발자 CSS에 밀려 광탈하는 것입니다!)

  3. 명시도 (Specificity) & 4. 스코프 근접성 (Scoping proximity)
    이제 개발자 CSS에서 살아남은 세 개의 규칙(0, 3px, 5px)을 비교합니다. 셋 다 선택자가 li로 동일하므로 명시도가 같습니다. @scope 블록도 없으므로 스코프 근접성도 패스합니다.

    그럼 남은 후보들을 볼까요?

    li { margin-left: 0; } /* Author CSS 1 */
    li { margin-left: 3px; } /* Author CSS 2 안의 미디어 쿼리 */
    @layer namedLayer { li { margin-left: 5px; } } /* Author CSS 2 안의 레이어 */

    여기서 주의할 점! 5px 규칙은 레이어(layer) 안에 들어있습니다. 같은 출처(개발자) 안에서는, 레이어 밖의 평범한 코드가 레이어 안의 코드보다 무조건 우선순위가 높습니다. 따라서 레이어 안의 5px도 탈락합니다.

  4. 작성 순서 (Order of appearance)
    이제 남은 건 03px입니다. 둘은 출처도 같고, 명시도도 같고, 둘 다 레이어 밖에 있습니다. 무승부 상황이므로 코드에서 가장 마지막에 읽힌(맨 아래에 있는) 녀석이 이깁니다. Author CSS 1보다 Author CSS 2가 나중에 선언되었다고 가정하면 최종 승자는 3px이 됩니다!

margin-left: 3px;

💡 정리하자면:
사용자 CSS의 1em이 클래스를 써서 명시도가 더 높았음에도(Specificity), 출처(Origin) 싸움에서 개발자 CSS에 졌습니다. 또 레이어 안에 쓴 5px이 코드상 가장 밑에 있었음에도(Order), 레이어 바깥의 코드(3px)에게 졌습니다. 명시도나 코드 순서는 '출처와 중요도(레이어 포함)'가 완벽히 동급일 때만 따진다는 사실, 잊지 마세요!


제작자 스타일 심화: 인라인 스타일, 레이어, 그리고 우선순위 (Author styles: inline styles, layers, and precedence)

지금까지는 출처(브라우저, 사용자, 개발자) 간의 굵직한 싸움을 봤다면, 이번엔 우리가 가장 많이 다루는 개발자(Author) CSS 내부의 싸움을 디테일하게 살펴보겠습니다. 개발자 스타일 안에서는 레이어가 선언된 순서, 그리고 HTML 태그에 직접 박아넣는 '인라인(inline) 스타일'이 큰 변수로 작용합니다.

레이어는 선언된 순서가 아주 중요합니다. 나중에 선언된 레이어일수록 힘이 셉니다. 하지만, 레이어 밖에서 작성된 평범한 스타일은 명시도와 상관없이 모든 레이어 스타일을 씹어먹는 최강의 힘을 가집니다.

아래의 예제를 볼까요? 개발자가 <style> 태그 안에서 @import를 써서 CSS 파일들을 잔뜩 불러오고 있습니다.

<style>
  @import "unlayeredStyles.css";
  @import "AStyles.css" layer(A); /* 레이어 A 생성 */
  @import "moreUnlayeredStyles.css";
  @import "BStyles.css" layer(B); /* 레이어 B 생성 */
  @import "CStyles.css" layer(C); /* 레이어 C 생성 */
  p {
    color: red;
    padding: 1em !important;
  }
</style>

그리고 HTML 본문에는 인라인 스타일이 선언되어 있습니다:

<p style="line-height: 1.6em; text-decoration: overline !important;">Hello</p>

이 상황을 분석해보죠.

  • "A", "B", "C" 순서대로 3개의 명명된 레이어가 만들어졌습니다.
  • 레이어에 소속되지 않은 스타일(unlayered styles)들은 파일로 불러온 두 개(unlayeredStyles.css, moreUnlayeredStyles.css)와 <style> 태그 안에 직접 쓴 p 태그 스타일이 합쳐져 암묵적으로 '가장 마지막 순서의 거대한 익명 레이어'를 형성합니다.
  • HTML에는 line-height (일반)와 text-decoration (!important)의 두 가지 인라인 스타일이 있습니다.

이 개발자(Author) 공간 내부의 우선순위를 1등부터 꼴찌까지 나열하면 이렇게 됩니다:

우선순위 (낮음 -> 높음)개발자(Author) 스타일 종류중요도 (Importance)
1 (꼴찌)A (가장 먼저 선언된 레이어)일반
2B (두 번째 레이어)일반
3C (마지막 레이어)일반
4모든 레이어 밖 스타일 (Unlayered)일반
5인라인 스타일 (style="...")일반
6애니메이션
7모든 레이어 밖 스타일 (Unlayered)!important
8C (마지막 레이어)!important
9B (두 번째 레이어)!important
10A (가장 먼저 선언된 레이어)!important
11인라인 스타일 (style="...")!important
12 (1등)트랜지션

표를 보면 아시겠지만, 일반(normal) 스타일의 세계에서는 레이어 밖의 스타일이 모든 레이어 스타일을 이깁니다. 만약 레이어 A, B, C 안에 명시도가 어마어마하게 높은 :root body p { color: black; } 코드가 잔뜩 있더라도, 레이어 밖에 있는 단순한 p { color: red; }가 이겨버립니다. 출처 단계에서 레이어 밖 코드가 우선권을 가지기 때문에, 명시도 따위는 묻지도 따지지도 않고 레이어 안의 코드는 기권 처리됩니다.

하지만 !important가 등판하면 이 모든 서열이 완벽하게 거꾸로 뒤집힙니다! (핵심)
일반 스타일에선 가장 찌질했던 가장 먼저 선언된 레이어(A)의 !important가, 레이어 밖의 !important는 물론이고 나중에 선언된 레이어(C)의 !important까지 모조리 박살내버립니다. ### 인라인 스타일 (Inline styles)

HTML 요소의 style 속성에 직접 적는 '인라인 스타일'은 개발자(Author) 스타일의 끝판왕입니다.

  • 일반 인라인 스타일은 명시도와 상관없이 개발자가 작성한 모든 일반 스타일(레이어 유무 불문)을 이깁니다.
  • !important가 붙은 인라인 스타일은 개발자가 작성한 그 어떤 !important 스타일(레이어 유무 불문)도 짓밟고 이깁니다.

단, 이런 무적의 인라인 !important 스타일조차도 질 때가 딱 세 번 있습니다:
1. 사용자(User)가 설정한 !important 스타일
2. 브라우저(User agent)의 !important 스타일
3. 진행 중인 트랜지션(Transition)

중요도와 레이어의 관계 (Importance and layers)

방금 말씀드린 내용을 코드로 증명해 볼까요? 일반 스타일과 !important 스타일에서 레이어의 서열이 어떻게 180도 뒤집히는지 확인해 보세요.

p {
  color: red;
}

@layer B {
  :root p {
    color: blue;
  }
}

위 코드에서는 red가 먼저 작성되었고 명시도도 더 낮습니다. 하지만 레이어 바깥에 있으므로(Unlayered), 레이어 B 안의 코드를 이기고 문단은 빨간색(red)이 됩니다.

그런데 여기에 !important라는 마법의 단어를 붙여보면 어떨까요?

p {
  color: red !important;
}

@layer B {
  :root p {
    color: blue !important;
  }
}

이제 문단은 파란색(blue)이 됩니다! !important의 세계에서는 가장 나중에 선언된 레이어 바깥의 코드보다, 초반에 선언된 레이어 B 안의 코드가 힘이 훨씬 세지기 때문입니다.

💡 강사님의 꿀팁: (실무 적용)
!important는 레이어의 서열을 박살 내고 생태계를 교란하는 주범입니다. 따라서 외부 라이브러리(부트스트랩 등)의 스타일을 덮어쓰려고 내 코드에 !important를 덕지덕지 바르는 짓은 제발 멈춰주세요!
대신, 외부 라이브러리를 가져올 때 @import "library.css" layer(lib); 처럼 특정 레이어에 가둬버리세요. 그러면 내가 작성하는 레이어 밖의 코드가 라이브러리 코드보다 기본적으로 힘이 세지기 때문에, !important를 한 번도 안 쓰고도 아주 우아하게 스타일을 덮어쓸 수 있답니다!


캐스케이드에 참여하는 요소들 (Which CSS entities participate in the cascade)

우리가 CSS 파일 안에 쓰는 모든 코드가 이 복잡한 캐스케이드(폭포수) 싸움에 끼어드는 건 아닙니다. 오직 CSS 속성-값 쌍(property/value pair)의 선언들만이 전쟁에 참여합니다. 다른 건 구경만 할 뿐이죠.

앳룰 (At-rules)

@font-face처럼 속성이 아니라 '디스크립터(descriptors)'를 담고 있는 앳룰들은 캐스케이드의 영향을 받지 않습니다.
예를 들어 font-family 디스크립터로 같은 이름의 폰트를 두 번 정의했다면, 명시도 같은 걸 따지는 게 아니라 브라우저가 상황에 맞는 적절한 폰트를 앳룰 전체 단위로 쿨하게 선택합니다.

마찬가지로 @keyframes 애니메이션 선언 자체도 캐스케이드에 섞이지 않습니다. 똑같은 이름의 @keyframes가 여러 개 있다면 덮어쓰거나 섞이는 게 아니라, 캐스케이드 순서(출처, 레이어 등)상 가장 우위에 있는 단 하나의 @keyframes 덩어리만 통째로 살아남아 적용됩니다.

@media@supports 같은 조건부 앳룰 안에 있는 선언들은 캐스케이드 전쟁에 참여하지만, 조건이 안 맞으면 아예 참전 자격(Relevance)을 잃고 짐을 싸게 됩니다.

프레젠테이션 속성 (Presentational attributes)

HTML을 작성하다 보면 디자인을 위해 align="center"나 SVG의 fill="red" 같은 속성을 태그에 직접 넣기도 하죠? 이를 프레젠테이션 속성이라고 부르는데, 브라우저는 이런 속성들을 보면 몰래 명시도 0짜리 CSS 규칙으로 번역해서 개발자 스타일(Author) 영역 맨 밑바닥에 끼워 넣습니다. 즉, 캐스케이드에 참여하긴 하지만 가장 힘이 약한 녀석들이라 일반 CSS를 조금만 써도 바로 덮어써 진다는 사실을 기억하세요! (당연히 여기에 !important를 붙일 수도 없습니다.)


스타일 싹 다 갈아엎기 (Resetting styles)

웹을 만들다 보면 애니메이션이 끝난 후나 다크/라이트 테마가 바뀔 때 등, 요소의 모든 스타일을 깨끗하게 처음 상태로 되돌리고 싶을 때가 있습니다. 일일이 속성을 초기화하기 귀찮으시죠? 이럴 때 쓰는 치트키가 바로 all 속성입니다!

all 속성을 사용하면 CSS의 (거의) 모든 속성을 단 한 번에 리셋할 수 있습니다. 값을 initial(브라우저 쌩초기값), inherit(부모에게서 물려받은 값), unset(알아서 초기화) 등으로 주어 요소의 상태를 완전히 새하얀 도화지로 만들어버릴 수 있답니다.


참고 자료 (See also)

캐스케이드는 CSS의 근본입니다! 조금 더 깊은 지식이 필요하시다면 아래 문서들을 천천히 탐독해 보시길 추천합니다.


MDN 개선에 도움을 주세요 (Help improve MDN)

이 문서가 학습에 도움이 되셨나요? (Was this page helpful to you?)
[Yes][No]

문서 기여 방법 알아보기: Learn how to contribute
최종 수정일: 2025년 12월 16일 (MDN contributors 작성)
이 문서를 GitHub에서 보기 | 문서의 문제점 신고하기

profile
프론트에_가까운_풀스택_개발자

0개의 댓글