Compute Which Rule Wins
한국어 원문으로 표시합니다.
목표
CSS 가 예측 불가능해 보이는 것은 규칙이 없어서가 아니라 규칙이 세 단계라서입니다. 중요도 → 특정성 → 순서.
브라우저가 없으니 "보기에 맞는가" 는 채점하지 않습니다. 대신 어떤 규칙이 이기는가 를 실제로 계산합니다 — 그건 브라우저 없이도 답이 하나입니다.
만들 것
/root/work/css/styles.css 하나입니다. HTML 은 필요 없습니다.
채점기가 아래 요소들을 가정하고 캐스케이드를 풉니다.
div#app > div.card
main.lessonLayout > aside.lessonSide
main.lessonLayout.sideHidden > aside.lessonSide
div.toolbar
계산기
cd /root/work/css
python3 /opt/lab/checks/css-lab/cascade.py styles.css
규칙마다 순서 · 미디어 조건 · 특정성을 보여 줍니다. 다루는 범위는
태그·클래스·아이디·의사클래스·자손/자식 결합자·콤마 그룹, 그리고
@media (max-width|min-width: Npx) 입니다. 그 밖의 선택자를 만나면
조용히 넘기지 않고 "해석할 수 없다" 고 말합니다.
단계
:root토큰 3개 +var()3곳- 특정성 예측 →
02-spec.txt !important없이 이기기- 순서 함정 (기본 규칙을 미디어 쿼리 위로)
- 3칸 그리드
- 접으면 1칸
- 플렉스 툴바
- 계산기 →
08-cascade.txt - 정리 →
09-notes.md
참고
4단계와 6단계는 이 서비스에서 실제로 났던 버그입니다. 모바일 목차 덮개가 안 열리던 것과, 목차를 접으면 페이지가 33,000px 로 늘어나던 것.
토큰을 :root 에 둔다
/root/work/css/styles.css 를 만들고 :root 에 커스텀 프로퍼티를 세 개 이상 정의한 뒤, var() 로 세 곳 이상에서 쓰세요.
mkdir -p /root/work/css. 이름은 --accent, --gap, --fg 처럼 자유입니다. SCSS 변수와 달리 커스텀 프로퍼티는 런타임에 살아 있고 상속됩니다 — 그래서 하위 트리에서만 덮어쓸 수 있습니다.
특정성을 계산해 본다
아래 다섯 선택자의 특정성을 02-spec.txt 에 번호=a,b,c 형식으로 한 줄씩 적으세요.
1) a
2) .btn.primary
3) #nav a:hover
4) ul > li::marker
5) :where(.card) .title
a=아이디, b=클래스·속성·의사클래스, c=태그·의사요소입니다. 예: 1=0,0,1. 5번이 함정입니다 — :where() 는 항상 0으로 셉니다. 자리끼리 넘어가지 않는다는 것도 기억하세요: 클래스 11개도 아이디 하나를 못 이깁니다.
!important 없이 이긴다
.card { color: ... } 를 먼저 쓰고, #app 안의 .card 만 다른 색이 되게 하세요. !important 를 쓰면 안 됩니다.
채점기가 div#app > div.card 요소에 대해 어떤 규칙이 이기는지 실제로 계산합니다. #app .card 는 1,1,0 이라 .card(0,1,0)를 이깁니다. !important 는 또 다른 !important 로만 이길 수 있어서, 한 번 쓰면 그 속성을 영원히 그렇게 다뤄야 합니다.
순서 함정을 피한다
.lessonSide 의 position 을 기본은 sticky, max-width: 900px 에서는 fixed 가 되게 하세요. 두 규칙의 특정성은 같으니 순서가 답을 정합니다.
기본 규칙을 @media 블록 위에 두세요. 아래에 두면 특정성이 같으므로(둘 다 0,1,0) 나중에 쓴 기본 규칙이 모바일에서도 이깁니다. 채점기가 1280px 와 500px 두 폭에서 각각 계산합니다. 이 서비스에서 모바일 덮개가 안 열리던 버그가 정확히 이것이었습니다.
골격을 그리드로
.lessonLayout 을 display: grid 로 만들고 세 칸짜리 grid-template-columns 와 gap 을 주세요.
예: grid-template-columns: 240px 1fr 200px. 그리드는 행과 열을 둘 다 정하고 싶을 때 씁니다. 한 줄 안에서 나누는 것뿐이라면 플렉스가 맞습니다.
접으면 칸도 없어져야 한다
.lessonLayout.sideHidden 일 때 칸이 하나가 되게 하세요. 0 1fr 200px 처럼 0짜리 칸을 남기면 안 됩니다.
자식이 display: none 이면 그리드에서 아예 빠집니다. 칸을 0으로 만들어 두면 본문이 그 0px 칸으로 밀려 들어가 글자마다 줄이 바뀌고, 실제로 페이지가 33,000px 이 됐습니다. 칸의 개수를 줄이세요 — grid-template-columns: 1fr.
툴바는 플렉스로
.toolbar 를 display: flex 로 만들고 justify-content, align-items, gap 을 모두 지정하세요.
가장 자주 쓰는 조합은 space-between + center + gap 셋입니다. 하나만 오른쪽으로 밀고 싶으면 그 요소에 margin-left: auto 를 주는 방법도 있습니다.
계산기로 확인한다
/opt/lab/checks/css-lab/cascade.py 로 여러분의 스타일시트를 풀어 결과를 08-cascade.txt 로 남기세요.
python3 /opt/lab/checks/css-lab/cascade.py styles.css | tee 08-cascade.txt. 규칙마다 순서·미디어 조건·특정성이 나옵니다. 개발자 도구에서 취소선이 그어진 규칙을 볼 때 머릿속에서 하는 계산이 이것입니다.
왜 졌는지 설명한다
09-notes.md 에 세 줄 이상. 4단계에서 순서를 뒤집으면 무슨 일이 생기는지, 6단계에서 0짜리 칸을 남기면 무슨 일이 생기는지, 그리고 !important 를 쓰고 싶어질 때 먼저 볼 것.
본문에 특정성, 순서, 그리드 가 들어가야 합니다. 진단은 늘 네 단계입니다 — 선택자가 맞는가, 지고 있는가, 특정성인가 순서인가, 상속을 기대하고 있는가.