LabHub

CSS — 나중에 쓴 규칙이 이긴다 · 캐스케이드와 레이아웃 · 이론

세 단계로 결정된다

LabHub 에서 이어서 보기

한 줄 요약

같은 속성에 규칙이 여럿 걸리면 브라우저는 중요도 → 특정성 → 순서 이 세 단계로 하나를 고른다. 앞 단계에서 갈리면 뒤는 보지 않는다.

왜 이걸 외워 두는가

CSS 에서 시간을 가장 많이 잡아먹는 순간은 문법을 몰라서가 아니라 "분명히 썼는데 안 먹는" 순간이다. 개발자 도구에 규칙은 분명히 보이는데 취소선이 그어져 있다.

이때 이유를 모른 채 선택자를 한 칸 더 길게 쓰거나 규칙을 하나 더 얹어서 넘어가면, 다음 사람은 더 긴 선택자를 써야 하고 그다음 사람은 !important 를 쓴다. 몇 달 뒤에는 지워도 되는 줄이 어디인지 아무도 모르는 파일이 남는다. CSS 가 무너지는 경로는 거의 항상 이것이다.

세 단계를 알면 그 자리에서 "졌다면 왜 졌는가" 에 답할 수 있다. 특정성 때문인지 순서 때문인지만 갈라도 고치는 방법이 완전히 달라진다.

1단계 — 중요도

사용자 !important  >  작성자 !important  >  작성자 일반  >  사용자 일반  >  브라우저 기본

여기서 !important 가 이긴다. 그래서 급할 때 손이 간다. 하지만 !important 를 이기는 방법은 또 다른 !important이라, 한 번 쓰면 그 속성은 영원히 그 방식으로만 다뤄야 한다. 그게 CSS 가 무너지는 전형적인 경로다.

!important 를 쓰고 싶어지면 대개 진짜 문제는 선택자 설계다.

2단계 — 특정성

세 자리 숫자로 센다.

| 자리 | 세는 것 | 예 |
|---|---|---|
| a | 아이디 | #nav |
| b | 클래스 · 속성 · 의사클래스 | .btn, [open], :hover |
| c | 태그 · 의사요소 | div, ::before |

a                    → 0,0,1.btn.primary         → 0,2,0#nav a:hover         → 1,1,1ul > li::marker      → 0,0,3

자리끼리 넘어가지 않는다. 클래스를 11개 붙여도 아이디 하나를 못 이긴다. 0,11,0 < 1,0,0 이다.

특별한 것 둘.

3단계 — 순서

여기가 사람을 가장 자주 잡는다.

중요도도 특정성도 같으면 나중에 쓴 것이 이긴다. 파일 안에서의 위치이지, 화면에서의 의미가 아니다.

@media (max-width: 900px) {  .side { position: fixed; }     /* 모바일 덮개 */}.side { position: sticky; }      /* ← 나중에 썼다 */

이 코드는 모바일에서도 sticky 다. 미디어 쿼리 안이라고 더 강한 것이 아니다. 특정성이 같으므로(둘 다 0,1,0) 뒤에 쓴 쪽이 이긴다.

> 기본 규칙은 항상 미디어 쿼리보다 위에 둔다.

이건 실제로 이 서비스에서 났던 버그다. 모바일 목차 덮개가 열리지 않았고, 원인은 기본 .lessonSide { position: sticky }@media 블록 아래에 있었던 것이다. 한 블록을 위로 옮겨서 고쳤다.

상속은 캐스케이드가 아니다

color, font-family, line-height 같은 것들은 자식으로 상속된다. margin, padding, display, border 는 안 된다.

상속으로 받은 값은 그 요소에 직접 걸린 어떤 규칙보다도 약하다. 특정성이 아무리 낮아도 직접 걸린 쪽이 이긴다. 상속값은 "아무 규칙도 없을 때" 만 쓰인다.

커스텀 프로퍼티는 상속된다

:root { --accent: #3b82f6; }.btn { background: var(--accent); }

--accent 는 상속되므로 :root 에 두면 어디서든 쓸 수 있고, 특정 하위 트리에서만 덮어쓸 수 있다.

.dark-panel { --accent: #93c5fd; }   /* 이 안의 .btn 들만 색이 바뀐다 */

전처리기 변수(SCSS $)와 결정적으로 다른 점이 이것이다. SCSS 변수는 컴파일 시점에 사라지지만, 커스텀 프로퍼티는 런타임에 살아 있고 캐스케이드를 탄다. 테마 전환이 자바스크립트 한 줄로 되는 이유다.

그리드에서 진짜 물리는 것

그리드는 배우기 쉬운데, 하나 모르면 크게 다친다.

display: none 인 자식은 그리드에서 아예 빠진다. 자리를 비워 두지 않는다.

.layout { grid-template-columns: 0 1fr; }   /* 접었을 때 목차를 0px 로 */.layout.hidden .side { display: none; }

의도는 "목차를 0px 로 접는다" 였다. 그런데 display: none 이라 목차가 흐름에서 빠지고, 본문이 첫 칸(0px)으로 밀려 들어간다. 글자마다 줄이 바뀌면서 페이지가 33,000px 이 됐다.

이것도 이 서비스에서 실제로 난 버그다. 고친 방법은 한 줄이다.

.layout.hidden { grid-template-columns: 1fr; }   /* 칸을 아예 하나로 */

칸을 0으로 만드는 게 아니라 칸의 개수를 줄여야 한다. 자식이 사라졌으면 칸도 사라져야 한다.

언제 그리드이고 언제 플렉스인가

헷갈리면 이렇게 묻는다. "행과 열을 둘 다 정하고 싶은가?" 그렇다면 그리드다.

플렉스에서 가장 자주 쓰는 것은 justify-content: space-between + align-items: center + gap 셋이다. margin-left: auto 로 하나만 오른쪽에 붙이는 것도 자주 쓴다.

현장에서 진단하는 순서

CSS 가 "안 먹을" 때는 규칙을 더하기 전에 순서대로 확인한다.

1. 선택자가 그 요소에 맞기는 하는가 — 개발자 도구에서 규칙이 보이는지
2. 다른 규칙에 지고 있는가 — 취소선이 그어져 있으면 진 것이다
3. 졌다면 왜인가 — 특정성인가 순서인가
4. 상속값을 기대하고 있는가 — 직접 걸린 규칙이 있으면 상속은 안 쓰인다

!important 는 이 넷을 다 확인한 뒤에도 답이 없을 때만 쓴다. 대개 3번에서 끝난다.

실무에서 이 순서가 중요한 이유는 고치는 방법이 단계마다 다르기 때문이다. 특정성에 졌으면 선택자를 손봐야 하고, 순서에 졌으면 파일 로드 순서나 규칙 위치를 옮겨야 한다. 원인을 가르지 않고 양쪽을 동시에 건드리면 고쳐졌는지 우연히 넘어갔는지도 알 수 없다.