Skip to main content

CSS를 배우다 보면 꼭 한 번은 이런 순간이 옵니다.

"분명히 color: red;를 썼는데 왜 파란색이지?"

이때 초보자는 보통 CSS를 더 많이 덧붙입니다. div .card p span a처럼 선택자를 길게 만들거나, 급한 마음에 !important를 붙입니다. 그런데 이상하게도 문제는 잠깐만 조용해질 뿐, 나중에 더 크게 돌아옵니다.

CSS 실력은 속성을 많이 외우는 것보다, 왜 안 되는지 빠르게 찾는 능력에서 크게 차이 납니다. 이번 장에서는 그 핵심인 캐스케이드, 상속, 특이성, 그리고 브라우저 개발자 도구로 문제를 찾는 법을 차근차근 익혀보겠습니다.

왜 스타일이 안 먹을까: 캐스케이드, 상속, 특이성, 디버깅

브라우저는 어떤 규칙을 먼저 고를까

같은 요소에 여러 CSS 규칙이 동시에 걸리면, 브라우저는 아무거나 고르지 않습니다. 순서가 있습니다. 아주 단순하게 말하면 다음 흐름으로 생각하면 됩니다.

  • 먼저 중요도와 출처를 봅니다.

  • 그다음 특이성을 봅니다.

  • 그래도 같으면 나중에 작성된 규칙이 이깁니다.1

예제를 먼저 보겠습니다.

<h1>안녕하세요</h1>
h1 {
  color: red;
}

h1 {
  color: blue;
}

두 선택자는 완전히 같습니다. 그러면 마지막 규칙이 이겨서 제목은 파란색이 됩니다. 이것이 캐스케이드의 가장 기본적인 모습입니다.1

여기서 중요한 포인트는 하나입니다. CSS는 "마지막에 쓴 것만 이긴다"가 아니라, "조건이 같을 때 마지막 것이 이긴다"는 점입니다. 조건이 다르면, 뒤에 썼어도 질 수 있습니다.

상속: 부모에게서 물려받는 스타일

어떤 스타일은 부모 요소에서 자식 요소로 자연스럽게 내려갑니다. 이것을 상속이라고 합니다.

예를 들어 color, font-family, line-height 같은 글자 관련 속성은 자주 상속됩니다.1

<div class="post">
  <h2>제목</h2>
  <p>문단 <span>강조</span></p>
</div>
.post {
  color: #333;
  font-family: sans-serif;
}

위 코드는 h2, p, span에 각각 색과 글꼴을 따로 쓰지 않아도, 부모인 .post의 값을 물려받습니다. 아주 편리하죠.

하지만 모든 속성이 상속되는 것은 아닙니다. margin, padding, border, background, width 같은 박스나 배치 관련 속성은 보통 상속되지 않습니다.1

.post {
  background: beige;
  border: 2px solid orange;
}

이렇게 해도 자식 요소마다 베이지 배경과 주황 테두리가 자동으로 생기지는 않습니다.

초보자가 자주 헷갈리는 부분도 여기 있습니다. 부모에 color를 줬는데 자식에 다른 규칙을 직접 쓰면, 그 직접 쓴 규칙이 이깁니다. 상속받은 값은 직접 선택된 규칙보다 약합니다. 부모의 선택자가 아주 복잡하더라도, 자식을 직접 겨냥한 규칙이 있으면 그쪽이 우선합니다.1

body {
  color: steelblue;
}

p {
  color: tomato;
}

이 경우 p는 부모의 파란색을 물려받지 않고, 직접 지정된 tomato를 사용합니다.

필요하면 상속을 직접 제어할 수도 있습니다.

a {
  color: inherit;
}

이렇게 하면 링크가 기본 파란색 대신, 부모의 글자색을 따르게 만들 수 있습니다.1

특이성: 누가 더 구체적으로 찍었는가

캐스케이드에서 가장 자주 싸움을 일으키는 것이 특이성입니다. 특이성은 선택자의 "무게"라고 생각하면 이해가 쉽습니다.

브라우저는 보통 다음 기준으로 선택자의 무게를 계산합니다.

  • ID 선택자: 가장 강함

  • 클래스, 속성 선택자, 가상 클래스: 그다음

  • 요소 선택자, 가상 요소: 비교적 약함2

실무에서는 보통 ID - CLASS - TYPE 세 칸으로 생각하면 충분합니다.2

선택자특이성
p0-0-1
.note0-1-0
article p0-0-2
.card p0-1-1
#app .card p1-1-1

비교는 합산이 아니라 왼쪽부터 합니다. 그래서 1-0-0은 0-99-99보다 강합니다.2

예제를 보겠습니다.

<p class="special">중요한 문장</p>
p {
  color: blue;
}

.special {
  color: red;
}

p는 0-0-1, .special은 0-1-0입니다. 클래스가 요소보다 강하므로 글자는 빨간색이 됩니다.1

이제 조금 더 가보겠습니다.

.special {
  color: red;
}

article .special {
  color: green;
}

같은 요소를 겨냥한다면 article .special은 0-1-1이라서 .special의 0-1-0보다 강합니다. 그래서 초록색이 됩니다.

그런데 아래처럼 생기면 더 놀랍습니다.

#sidebar {
  color: purple;
}

.content .title {
  color: orange;
}

많은 초보자가 클래스가 두 개니까 더 강할 것 같다고 느끼지만, 실제로는 ID 하나가 훨씬 강합니다. 특이성 비교는 "총 개수"가 아니라 "칸별 비교"이기 때문입니다.2

특이성이 같다면 마지막 규칙이 이긴다

특이성이 완전히 같으면 그때는 선언 순서가 승부를 가릅니다.

input.my-input {
  border-color: gray;
}

:root input {
  border-color: royalblue;
}

이 둘은 같은 수준의 특이성을 가질 수 있고, 그때는 뒤에 있는 규칙이 적용됩니다.2

그래서 어떤 스타일이 안 먹는다고 무조건 파일 아래로 내리는 것은 위험합니다. 특이성이 다르면 순서를 바꿔도 소용이 없기 때문입니다.

인라인 스타일과 !important: 마지막 카드 두 장

HTML에 직접 쓰는 인라인 스타일은 아주 강합니다.

<p style="color: purple;">보라색 문장</p>

이런 인라인 스타일은 일반적인 외부 CSS보다 우선하는 경우가 많습니다.2

또 하나는 !important입니다.

p {
  color: blue !important;
}

.special {
  color: red;
}

이 경우 보통은 !important가 붙은 파란색이 이깁니다.2

하지만 여기서 중요한 것은, !important가 만능 해결책이 아니라는 점입니다. 한 번 붙이기 시작하면 나중에는 더 강한 !important로 맞붙는 "중요도 전쟁"이 벌어집니다. CSS가 점점 말을 안 듣는 이유가 바로 이것입니다.

초보자라면 이렇게 기억하면 좋습니다.

  • 평소 스타일링에는 !important를 거의 쓰지 않는다.

  • 외부 라이브러리 스타일을 잠깐 덮어써야 할 때만 아주 조심해서 쓴다.

  • 붙였다면 왜 붙였는지 주석으로 이유를 남긴다.2

실전 예제 1: 분명 버튼을 빨갛게 했는데 왜 회색일까

아래 코드를 보겠습니다.

<button class="btn primary">가입하기</button>
button {
  background: gray;
}

.btn {
  background: steelblue;
}

button.primary {
  background: crimson;
}

정답은 crimson입니다.

이유를 하나씩 풀면 이렇습니다.

button은 0-0-1, .btn는 0-1-0, button.primary는 0-1-1입니다. 가장 강한 것은 button.primary입니다. 그래서 버튼은 진홍색이 됩니다.

이 예제에서 중요한 점은, 클래스 하나보다 클래스+요소가 더 강하다는 것입니다. 아주 작은 차이 같지만, 실제 프로젝트에서는 이런 작은 차이가 스타일 충돌의 시작점이 됩니다.

실전 예제 2: 부모에 배경색을 줬는데 자식은 왜 그대로일까

<div class="card">
  <p>카드 설명</p>
</div>
.card {
  color: #222;
  background: #f5f5f5;
  padding: 16px;
}

p는 글자색을 물려받아 어두운 회색이 될 수 있습니다. 하지만 배경색과 패딩은 상속되지 않으므로 p 자체가 따로 회색 배경을 갖지는 않습니다.1

이걸 이해하면 "왜 배경은 안 내려오지?"라는 오래된 궁금증이 풀립니다. CSS는 문서의 글자 스타일은 잘 물려주지만, 박스 모양까지 몽땅 물려주지는 않습니다. 안 그랬다가는 모든 자식 요소가 부모의 테두리와 패딩을 복사해 버려서 화면이 꽤 혼란스러워질 테니까요.

실전 예제 3: :hover 스타일이 안 보일 때

.button {
  background: #333;
}

.button:hover {
  background: orange;
}

이 코드가 맞는데도 hover 효과가 안 보인다면 몇 가지를 의심해야 합니다.

  • 실제 요소에 .button 클래스가 있는지

  • 마우스를 올릴 수 없는 상태인지

  • 더 강한 선택자가 이미 배경색을 덮고 있는지

  • !important가 끼어 있는지

이때 가장 좋은 도구가 브라우저 개발자 도구의 강제 상태 기능입니다. Chrome, Edge 같은 브라우저에서는 :hov 또는 비슷한 메뉴에서 :hover, :focus 상태를 강제로 켤 수 있습니다. 그래서 마우스를 계속 올리고 있지 않아도 hover 스타일이 적용되는지 확인할 수 있습니다.34

개발자 도구로 CSS 문제 찾기

이 장의 진짜 핵심은 여기입니다. CSS는 추측으로 고치면 오래 걸리고, 개발자 도구로 보면 빨라집니다.

브라우저에서 요소를 검사하면 보통 Elements 탭과 Styles, Computed 패널을 볼 수 있습니다. 이곳에서 브라우저가 어떤 규칙을 적용했고, 어떤 규칙을 버렸는지 거의 다 확인할 수 있습니다.34

1단계: 요소를 정확히 선택하기

문제가 생긴 요소를 오른쪽 클릭해서 검사를 누릅니다. 그러면 실제 렌더링된 DOM에서 해당 요소를 바로 볼 수 있습니다. 여기서 중요한 점은, 브라우저가 실제로 해석한 DOM을 본다는 것입니다. 원본 HTML과 화면에 그려진 DOM이 다를 수도 있습니다.3

2단계: Styles 패널에서 취소선부터 보기

Styles 패널에는 선택한 요소에 걸린 규칙들이 나옵니다. 여기서 취소선이 그어진 속성은 적용에 실패한 것입니다. 이미 더 강한 규칙에게 졌다는 뜻입니다.34

예를 들어 아래처럼 보인다면,

color: red;

이 속성에 취소선이 있다면, 위쪽 어딘가에 color를 이긴 규칙이 있다는 뜻입니다. 이때 해야 할 일은 감으로 고치는 것이 아니라, 누가 이겼는지 확인하는 것입니다.

3단계: Computed 패널에서 최종 결과 확인하기

Computed 패널은 최종 승자만 보여줍니다. 예를 들어 color의 최종 값이 무엇인지, margin-left가 실제로 얼마인지, 어떤 파일의 어떤 규칙에서 왔는지를 더 쉽게 확인할 수 있습니다.34

Styles 패널이 "경기 과정"이라면, Computed 패널은 "최종 스코어보드"에 가깝습니다.

4단계: 규칙을 끄고 켜면서 원인 좁히기

대부분의 개발자 도구에서는 속성 옆 체크박스를 끄고 켤 수 있습니다. 이 기능이 정말 강력합니다.

  • margin을 꺼 보니 레이아웃이 정상이다.

  • position을 끄니 겹침이 사라진다.

  • display를 바꾸니 의도한 모양이 나온다.

이렇게 하면 범인을 빠르게 찾을 수 있습니다.3

5단계: 박스 모델 보기

요소 크기나 여백이 이상하면 Box Model 보기를 확인하세요. 내용 영역, 패딩, 테두리, 마진이 색으로 구분되어 보여서 "왜 이렇게 커 보이지?"를 금방 이해할 수 있습니다.35

특히 다음 상황에서 매우 유용합니다.

  • 예상보다 요소가 넓거나 좁을 때

  • 마진이 겹치거나 과하게 보일 때

  • 패딩 때문에 박스가 커졌을 때

6단계: 반응형과 상태를 강제로 테스트하기

개발자 도구에서는 단순히 CSS를 읽는 것만이 아니라, 상태를 흉내 내는 것도 가능합니다.

  • :hover, :focus 같은 상태 강제 적용

  • 모바일 화면 크기 에뮬레이션

  • 미디어 쿼리 구간 확인45

그래서 "모바일에서만 깨져요", "포커스 때만 이상해요" 같은 문제도 재현하기 쉬워집니다.

7단계: 겹침 문제라면 z-index 조건도 함께 보기

겹침이 이상할 때는 z-index 숫자만 보는 것으로 끝나지 않습니다. 보통은 position이 있는 요소에서 먼저 확인하면 되지만, flex item이나 grid item은 position: static이어도 z-index가 적용될 수 있습니다.2

즉, 이런 순서로 보면 좋습니다.

  • 이 요소가 positioned 요소인가?

  • 아니면 Flexbox나 Grid의 직계 자식인가?

  • 더 바깥쪽 부모가 stacking context를 만들고 있지는 않은가?

이 세 가지를 같이 보면 "왜 z-index가 안 먹지?"라는 질문을 훨씬 빨리 풀 수 있습니다.

스타일이 안 먹을 때 확인할 순서

문제가 생기면 아래 순서대로 보면 대부분 해결됩니다.

  • 선택자가 맞는지 확인한다. 클래스명 오타가 없는지 본다.

  • CSS 문법이 맞는지 확인한다. 세미콜론, 중괄호, 속성 이름을 본다.

  • 해당 속성이 상속되는 속성인지 확인한다.

  • 더 강한 선택자, 인라인 스타일, !important가 있는지 본다.

  • 특이성이 같다면 선언 순서를 본다.

  • 개발자 도구 Computed에서 최종 값을 확인한다.13

이 순서를 몸에 익히면 CSS 디버깅 속도가 눈에 띄게 빨라집니다.

유지보수 잘 되는 CSS 습관

스타일 충돌은 한 번 생기면 고치는 데 시간이 듭니다. 그래서 애초에 덜 충돌하게 쓰는 습관이 중요합니다.

클래스 중심으로 작성하기

스타일링에는 보통 ID보다 클래스를 더 많이 씁니다. ID는 너무 강해서 나중에 덮어쓰기 어렵기 때문입니다.2

/* 덜 유연함 */
#header {
  color: navy;
}

/* 더 유연함 */
.site-header {
  color: navy;
}

선택자를 괜히 길게 늘이지 않기

/* 피하고 싶은 방식 */
main .content .card .title span {
  color: tomato;
}

/* 더 단순한 방식 */
.card-title {
  color: tomato;
}

선택자가 길어질수록 특이성이 쌓이고, 나중에 다른 규칙으로 덮기 어려워집니다.

!important를 습관처럼 쓰지 않기

당장은 편하지만, 나중에 더 큰 혼란을 만듭니다. 진짜 마지막 수단으로만 사용하세요.2

한 걸음 더: @layer는 왜 주목받을까

큰 프로젝트에서는 특이성 싸움이 끝없이 커지기 쉽습니다. 이를 줄이기 위해 나온 기능이 캐스케이드 레이어입니다. @layer를 사용하면 "기본 스타일", "컴포넌트", "유틸리티"처럼 우선순위를 명시적으로 나눌 수 있습니다.6

@layer base, components;

@layer base {
  p {
    color: black;
  }
}

@layer components {
  .notice {
    color: crimson;
  }
}

이 기능은 초보 단계에서 꼭 바로 써야 하는 것은 아닙니다. 하지만 나중에 규모가 커지면, 특이성을 억지로 키우지 않고도 우선순위를 다루는 좋은 방법이 됩니다.6

마무리: CSS는 기억력보다 추적력이 중요하다

이 장에서 꼭 가져가야 할 것은 하나입니다.

CSS가 안 먹을 때는 브라우저가 고집을 부리는 것이 아니라, 브라우저가 아주 성실하게 규칙을 따르고 있는 것입니다. 우리가 그 규칙을 아직 다 읽지 못했을 뿐입니다.

이제부터는 스타일이 안 보일 때 이렇게 생각해 보세요.

"이 규칙이 틀린 게 아니라, 누가 이기고 있는지 확인하자."

그 순간부터 CSS는 덜 답답해지고, 훨씬 재밌어집니다.

참고

1Handling conflicts - Learn web development | MDN

2Specificity - CSS | MDN

3Debugging CSS - Learn web development | MDN

4CSS 보기 및 변경

5Get started viewing and changing CSS

6Cascade Layers Guide | CSS-Tricks

책 목록

처음부터 끝까지 배우는 CSS 기초

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기