Skip to content
This page has been auto-translated and may contain errors.View in English

CSS 구조화와 확장

docs.scrimba.com

새 프로젝트의 첫 스타일시트를 작성하는 일은 즐겁습니다. 그런데 몇백 줄쯔음 지나면 뭔가 달라집니다. 버튼 하나의 색을 바꿨는데 다른 버튼 세 개의 색까지 같이 바뀌고, 제목 하나는 !important를 붙이지 않으면 위치가 꼼짝도 하지 않으며, 새 규칙을 하나 추가할 때마다 기존 규칙이 깨질 것 같은 불안감이 듭니다. CSS 문법 자체는 여전히 틀린 곳이 없습니다. 부족해진 것은 그 CSS를 지탱하는 구조이고, 스타일시트가 천 줄이든 십만 줄이든 계속 다룰 수 있는 상태로 남는지를 결정하는 것도 문법이 아니라 바로 이 구조입니다.

CSS는 왜 규모가 커지면 어려워지는가

CSS는 기본적으로 전역으로 작동합니다. 여러분이 작성하는 모든 규칙은 페이지 어디에 있든 조건에 맞는 요소라면 다 건드릴 수 있습니다. p { color: navy; }라고 쓰면 프로젝트 전체의 모든 문단이 남색으로 바뀝니다. 의도했든 아니든 상관없습니다. 작은 페이지에서는 이게 편리하지만, 프로젝트가 커지면 대부분의 문제가 여기서 시작됩니다.

이 문제는 규칙이 서로 충돌하는 형태로 나타납니다. 두 스타일시트가 모두 p를 대상으로 하거나, 일반적인 규칙과 구체적인 규칙이 같은 요소에 동시에 적용되면, 어느 쪽이 이기는지 따져봐야 합니다. CSS는 어떻게 동작하는가에서 이를 결정하는 원리를 이미 살펴봤습니다. 브라우저는 명시도와 순서로 충돌을 해결합니다. CSS를 잘 확장한다는 것은 결국 이런 충돌 자체를 만들지 않는 것에 가깝습니다.

CSS에는 내장된 스코프 개념이 없습니다. 규칙은 **전역**입니다. 선택자와 일치하는 모든 요소에, 페이지가 불러오는 모든 파일을 통틀어 적용됩니다. 어떤 컴포넌트의 스타일이 다른 컴포넌트에 침투하지 못하게 막아주는 장치가 없는데, CSS라는 언어 자체에 "이 규칙은 저 컴포넌트에만 속한다"는 개념이 없기 때문입니다. 이 자유로움이 CSS를 처음 시작할 때는 쉽게 만들어주지만, 규모가 커질수록 다루기 어렵게 만듭니다.

코드베이스가 커질수록 두 가지 힘이 반대로 작용합니다. 선택자가 많아질수록 겹치는 범위도 늘어나 규칙이 충돌하고, 브라우저는 CSS는 어떻게 동작하는가에서 다룬 규칙, 즉 명시도를 먼저 보고 그다음 작성 순서를 보는 방식으로 각 충돌을 해결합니다. 그리고 명시도는 위로만 올라가는 경향이 있습니다. 지금 당장 충돌을 이기는 가장 빠른 방법은 조금 더 구체적인 선택자를 쓰는 것인데, 이는 내일 그 규칙을 덮어써야 할 다른 규칙의 기준선을 함께 높여버리기 때문입니다. 이를 방치하면 결국 !important 없이는 아무도 이길 수 없는 선택자들이 생겨나고, 이 지점이 바로 스타일시트가 더 이상 유지보수하기 어려워지는 순간입니다.

CSS는 하나의 평평하고 전역적인 네임스페이스를 사용합니다. 모든 선택자가 같은 공간에서 경쟁하며, 캐스케이드(하나의 요소에 여러 선언이 적용될 때 승자를 고르는 알고리즘)는 먼저 출처(origin)를, 그다음 명시도를, 마지막으로 작성 순서를 기준으로 충돌을 해결합니다. 언어 자체에는 모듈 경계가 없으므로, 한 컴포넌트를 위해 작성한 선택자가 다른 어떤 컴포넌트의 요소든 자유롭게 매칭할 수 있습니다. 이 장에서 다루는 확장 기법들은 모두 이 언어가 주지 않는 경계를 스스로 만들어내기 위한 것입니다.

이 실패 패턴은 정확히 짚어둘 필요가 있습니다. 아래 소개할 습관들이 모두 이 패턴에 대한 방어책이기 때문입니다. 급할 때 적용이 안 되는 규칙을 고치는 가장 빠른 방법은 선택자를 더 구체적으로 만드는 것입니다. 부모를 추가하거나, 클래스를 하나 더 연결하거나, 결국 id로 도피하는 식입니다. 이렇게 하면 그 순간의 충돌은 이기지만, 이후 모든 규칙의 명시도 기준선을 함께 끌어올리기 때문에 다음번 덮어쓰기는 그보다 더 구체적이어야 합니다. 이것이 바로 **명시도 전쟁**입니다. 선택자가 오로지 위로만 올라가는 일방향 톱니바퀴이며, 결국 더 이길 방법이 없어 !important로 끝나게 됩니다. 여기서 벗어나는 방법은 이 싸움을 더 영리하게 이기는 것이 아니라, 애초에 싸움이 잘 시작되지 않을 만큼 명시도를 낮고 평평하게 유지하는 것입니다. 그리고 이제 언어가 허용하는 범위 안에서, 이 장 마지막에서 다룰 캐스케이드 레이어를 이용해 순서 결정 자체를 명시도 바깥으로 옮겨놓는 것입니다.

css
/* 페이지의 모든 문단에 적용되는 전역 규칙 하나 */
p {
  color: navy;
}

/* 다른 곳에 있는 두 번째 규칙, 같은 요소를 두고 경쟁 */
.notice p {
  color: crimson;   /* .notice 안에서 승리: 더 구체적이므로 */
}
JunoCSS는 왜 규모가 커지면 어려워지는가 CSS 규칙은 전역적입니다. 하나의 규칙이 페이지 전체의 요소에 스타일을 입힐 수 있는데, 처음에는 편리하지만 나중에는 지저분해집니다. 프로젝트가 커지면 규칙이 서로 충돌하기 시작하고, 브라우저는 명시도와 순서를 이용해 승자를 골라야 합니다. CSS를 잘 조직하는 일의 대부분은 애초에 서로 싸우지 않는 규칙을 쓰는 것입니다.
JunoCSS는 왜 규모가 커지면 어려워지는가 CSS에는 스코프가 없어서 모든 규칙이 전역이며 조건에 맞는 요소는 어디든 건드릴 수 있습니다. 코드베이스가 커지면 두 가지가 문제가 됩니다. 규칙이 충돌하고, 충돌을 빠르게 해결하는 방법이 항상 더 구체적인 선택자이기 때문에 명시도가 계속 올라갑니다. 이 상승세를 억제하기만 하면 나머지 확장 작업은 훨씬 쉬워집니다.
JunoCSS는 왜 규모가 커지면 어려워지는가 CSS는 모듈 경계가 없는 하나의 평평한 전역 네임스페이스이며, 캐스케이드는 출처, 그다음 명시도, 그다음 순서로 모든 충돌을 해결합니다. 여기서 함정은 명시도 톱니바퀴입니다. 급하게 덮어쓸 때마다 기준선이 올라가고, 다음번엔 그보다 더 높이 올라가야 하며, 결국 !important까지 내려가게 됩니다. 이 장의 모든 기법은 이 톱니바퀴가 돌기 시작하지 않을 만큼 명시도를 낮게 유지하는 방법입니다.

명시도를 낮고 평평하게 유지하기

가장 유용한 습관 하나는 **클래스**로 스타일을 입히는 것입니다. 그리고 대부분 한 번에 클래스 하나만 사용하는 것입니다. .card 같은 클래스는 빠르게 적용할 수 있고, 재사용하기 쉬우며, 나중에 필요할 때 덮어쓰기도 어렵지 않습니다. 클래스 하나는 명시도가 낮고 부담이 적기 때문입니다.

문제는 두 가지에서 옵니다. id와 길게 이어진 선택자 체인입니다. #header 같은 id는 클래스보다 덮어쓰기가 훨씬 어렵고, .sidebar ul li a 같은 체인은 특정한 구조에 고정되어 버립니다. 어디에나 붙일 수 있는 평범한 클래스를 선호하세요:

CSS를 유지보수하기 쉽게 지키는 핵심 습관은 명시도를 낮고 평평하게 유지하는 것입니다. 단일 클래스로 스타일을 입히고, 명시도를 급격히 끌어올리는 두 가지, 즉 id와 깊은 자손 선택자 체인을 피하는 것입니다. 단일 클래스는 딱 적당한 지점입니다. 원하는 것을 정확히 겨냥할 만큼 구체적이면서도, 나중에 다른 단일 클래스가 싸움 없이 덮어쓸 수 있을 만큼 약합니다.

id는 클래스보다 명시도 점수가 훨씬 높아서, #id 규칙은 덮어쓰기가 괴롭고 명시도를 계속 위로 밀어올리게 만듭니다. 긴 자손 체인은 또 다른 문제를 일으킵니다. .sidebar nav ul li a는 명시도를 높이는 동시에 규칙을 특정 HTML 구조에 고정시켜서, 마크업이 바뀌는 순간 바로 깨집니다. 해당 요소에 클래스를 따로 부여하고 그것을 직접 대상으로 지정하세요.

명시도를 낮고 평평하게 유지하는 것은 명시도 전쟁을 이겨내는 게 아니라 애초에 일어나지 않게 막는 습관입니다. 낮다는 것은 각 규칙이 필요한 만큼만 정확히 선택하면서 최소한의 점수만 갖는다는 뜻이고, 평평하다는 것은 규칙들이 비슷하게 낮은 무게에 모여 있어서, 순서만으로도 어느 규칙이든 다른 규칙을 덮어쓸 수 있다는 뜻입니다. 거의 모든 것이 단일 클래스 선택자로 이루어진 스타일시트가 이 특성을 갖습니다. 아무것도 이웃보다 높게 매겨져 있지 않으니 어떤 것도 이기기 어렵지 않습니다.

이 평평함을 깨는 구성 요소 두 가지가 있고, 둘 다 기본적으로 피할 만합니다. id는 클래스보다 자릿수가 다른 명시도를 가지므로, #id 규칙 하나는 어떤 클래스 규칙 더미보다도 위에 있고 오직 다른 id나 !important만이 이길 수 있습니다. 그러니 스타일은 클래스로 입히고, id는 앵커 링크나 JavaScript 훅에만 남겨두세요. 깊은 자손 체인은 선택자별 명시도를 높이는 것과 동시에 규칙을 고정된 조상 구조에 결합시킵니다. 그래서 .sidebar nav ul li a는 덮어쓰기 어려운 동시에 취약합니다. 마크업을 바꾸면 조용히 매칭이 멈춰버립니다. 다음 절에서 다루는 방법론은 대체로 요소를 체인으로 찾아가지 않고 직접 이름 붙여 지정할 수 있게 해주기 위해 존재합니다.

css
/* 권장: 단일 클래스, 낮고 평평하게 */
.nav-link {
  color: navy;
}

/* 피하기: id, 나중에 덮어쓰기 어려움 */
#nav-link {
  color: navy;
}

/* 피하기: 깊은 체인, 취약하고 명시도가 높음 */
.sidebar nav ul li a {
  color: navy;
}
Juno명시도를 낮고 평평하게 유지하기 클래스로 스타일을 입히세요. 대부분 .card.nav-link처럼 한 번에 클래스 하나만 사용합니다. 단일 클래스는 재사용하기 빠르고 나중에 덮어쓰기도 쉬워서, 딱 원하는 성질을 갖습니다. #header 같은 id와 .sidebar ul li a 같은 긴 체인은 피하세요. 둘 다 나중에 바꾸기가 훨씬 어렵습니다.
Juno명시도를 낮고 평평하게 유지하기 단일 클래스는 딱 적당한 지점입니다. 원하는 대상을 정확히 겨냥할 만큼 구체적이면서, 다른 클래스가 싸움 없이 덮어쓸 수 있을 만큼 약합니다. id는 명시도를 급등시켜 계속 위로 밀어올리고, .sidebar nav ul li a 같은 깊은 체인은 마크업이 바뀌는 순간 깨집니다. 규칙을 어디에 걸어야 할지 애매하면, 해당 요소에 클래스를 따로 만들어 그것을 대상으로 지정하세요.
Juno명시도를 낮고 평평하게 유지하기 낮고 평평하다는 것은 모든 규칙이 거의 같은 작은 무게에 자리해서, 작성 순서만으로도 충돌을 해결할 수 있다는 뜻입니다. id는 클래스보다 자릿수가 다르게 높고 오직 !important나 다른 id만이 이길 수 있으니, 앵커 링크와 JS 훅에만 남겨두세요. 깊은 체인은 명시도를 높이는 동시에 규칙을 하나의 HTML 구조에 용접해버리는데, 이것이 바로 요소를 조상 체인으로 찾아가는 대신 직접 이름 붙이는 방식이 더 나은 이유입니다.

네이밍 컨벤션

클래스로 스타일을 입히기 시작하면, 다음 질문은 이름을 뭐라고 붙일지입니다. .blue.thing2 같은 이름은 금방 무너집니다. 그 클래스가 무엇을 위한 것인지 아무것도 말해주지 않기 때문입니다. **네이밍 컨벤션**은 이름만 보고도 클래스가 무슇을 하는지, 어디에 속하는지 알 수 있도록 클래스 이름을 짓는 합의된 방식입니다.

널리 쓰이는 것이 BEM인데, Block, Element, Modifier의 줄임말입니다. 블록은 카드 같은 컴포넌트입니다. 엘리먼트는 그 안의 부분이며 밑줄 두 개로 표기합니다. 모디파이어는 변형이며 대시 두 개로 표기합니다:

클래스를 스타일링 단위로 쓰게 되면, 이름 짓기가 이를 정리하는 핵심 요소가 됩니다. **네이밍 컨벤션**은 클래스 이름에 대한 공유된 패턴이고, 그 역할은 이름을 예측 가능하고 충돌 없게 만드는 것입니다. 클래스 이름만 보고도 어떤 컴포넌트에 속하는지, 어떤 부분에 스타일을 입히는지 알 수 있고, 두 컴포넌트가 우연히 같은 이름을 쓰는 일도 없어집니다.

가장 널리 쓰이는 컨벤션은 BEM(Block, Element, Modifier)입니다. 블록은 컴포넌트(.card)이고, 엘리먼트는 밑줄 두 개로 이어붙인 그 일부(.card__title)이며, 모디파이어는 대시 두 개로 이어붙인 변형(.card--featured)입니다. 이 방식의 진짜 이득은 명시도가 평평해진다는 것입니다. 모든 부분이 자신만의 단일 클래스를 갖기 때문에 .card__title에 도달하기 위해 자손 체인이 필요 없고, 그래서 BEM과 낮고 평평한 습관은 서로를 강화합니다.

**네이밍 컨벤션**은 이 언어에 없는 스코프를 대신하는 장치입니다. 컴포넌트 경계를 클래스 이름 자체에 인코딩해서, 언어의 어떤 기능에도 의존하지 않고 충돌 없고 이름만으로 이해되는 클래스명을 만들어줍니다. 클래스를 읽으면 그것이 어떤 컴포넌트인지, 어떤 부분인지, 어떤 변형인지 알 수 있습니다. 일관된 컨벤션이라면 어떤 것이든 이 효과를 냅니다. 가치는 정확한 표기법이 아니라 일관성 자체에 있습니다.

BEM(Block, Element, Modifier)이 가장 널리 쓰입니다. 블록은 컴포넌트 이름(.card)이고, 엘리먼트는 밑줄 두 개로 부분의 이름을 붙이며(.card__title), 모디파이어는 대시 두 개로 변형의 이름을 붙입니다(.card--featured). BEM이 단순한 네이밍을 넘어 제 역할을 하는 이유는, 구조적으로 명시도를 평평하게 유지해준다는 데 있습니다. 모든 엘리먼트가 자기만의 단일 클래스 선택자를 갖기 때문에 .card .title처럼 쓰는 대신 .card__title을 직접 지정할 수 있고, 그 컴포넌트 안의 모든 규칙이 같은 무게에 자리하게 됩니다. 이것이 바로 앞 절에서 본 낮고 평평한 성질인데, 이번에는 네이밍 방식 덕분에 자연스럽게 따라옵니다. 대가는 HTML 안의 클래스 목록이 길어진다는 것이며, 이는 대부분의 팀이 예측 가능성을 얻기 위해 받아들이는 비용입니다.

css
/* 블록: 컴포넌트 그 자체 */
.card { }

/* 엘리먼트: 블록의 일부, 밑줄 두 개 */
.card__title { }
.card__body { }

/* 모디파이어: 블록의 변형, 대시 두 개 */
.card--featured { }
html
<article class="card card--featured">
  <h2 class="card__title">주말 워크숍</h2>
  <p class="card__body">레이아웃에 대한 짧은 소개.</p>
</article>
Juno네이밍 컨벤션 네이밍 컨벤션은 이름만 보고도 무엇을 위한 클래스인지 알 수 있도록 클래스 이름을 짓는 합의된 방식입니다. BEM이 흔히 쓰이는데, .card 같은 블록, 밑줄 두 개로 표기하는 .card__title 같은 엘리먼트, 대시 두 개로 표기하는 .card--featured 같은 변형이 있습니다. BEM을 꼭 써야 하는 건 아니지만, 일관된 방식을 하나 정해서 계속 지키세요.
Juno네이밍 컨벤션 컨벤션은 클래스 이름을 예측 가능하고 충돌 없게 만들어서, 이름만 보고도 어떤 컴포넌트인지, 어떤 부분인지 알 수 있게 해줍니다. BEM이 널리 쓰이는 컨벤션으로, 블록 .card, 엘리먼트 .card__title, 모디파이어 .card--featured로 구성됩니다. 자손 체인 대신 모든 부분이 자기만의 단일 클래스를 갖기 때문에 명시도도 평평하게 유지됩니다.
Juno네이밍 컨벤션 컨벤션은 CSS가 원래 주지 않는 스코프를 대신합니다. 컴포넌트 경계가 클래스 이름 안에 담기기 때문에 이름이 충돌 없고 스스로 설명이 됩니다. BEM은 블록, 엘리먼트, 모디파이어를 .card, .card__title, .card--featured로 인코딩하며, 진짜 이득은 구조적으로 명시도가 평평해진다는 것입니다. 모든 부분이 단일 클래스이기 때문입니다. 대가는 마크업 안 클래스 목록이 길어지는 것인데, 대체로 예측 가능성을 얻는 대가로 감수할 만합니다.

파일 구조 잡기

스타일시트가 커지면 하나의 긴 파일은 다루기 어려워집니다. 흔한 해법은 CSS를 규칙이 하는 역할별로 몇 개의 폴더로 나누는 것입니다. base 스타일(body나 제목 같은 평범한 요소의 기본값), 컴포넌트(카드, 버튼, 그 외 조각들), 유틸리티(간격이나 텍스트 정렬 클래스처럼 단일 목적의 작은 헬퍼)입니다.

파일을 나눌 때 중요한 것이 하나 있습니다. 바로 불러오는 순서입니다. 두 규칙의 명시도가 같으면 나중에 나온 규칙이 이깁니다. 그래서 나중에 불러온 스타일시트가 앞선 것을 덮어쓸 수 있습니다. 파일을 가장 일반적인 것부터 가장 구체적인 것 순으로 불러오세요:

CSS를 역할별로 폴더에 나눠 담으면 커지는 코드베이스도 다루기 쉬워집니다. 흔한 구조는 세 그룹입니다. base(리셋과 body, 제목, 링크 같은 요소 기본값), 컴포넌트(.card, .btn 같은 독립된 조각), 유틸리티(.text-center.mt-4 같은 단일 목적 헬퍼)입니다.

이 파일들을 합치거나 임포트하는 순서는 단순한 미적 요소가 아닙니다. 작성 순서가 캐스케이드의 동점자 판정 기준이기 때문입니다. 두 규칙의 명시도가 같으면 나중 것이 이깁니다. 그러니 덜 구체적인 것부터 더 구체적인 순으로 불러오세요. base를 먼저, 그다음 컴포넌트, 마지막으로 유틸리티. 그래야 유틸리티가 컴포넌트를 덮어쓰고, 컴포넌트가 base 기본값을 덮어쓸 수 있으며, 아무도 명시도를 억지로 끌어올리지 않아도 됩니다. 순서를 제대로 잡는 것이야말로 모든 것을 단일 클래스 명시도로 유지하면서도 덮어쓰기가 원하는 대로 적용되게 해주는 핵심입니다.

파일을 역할별로 구조화하는 것은 낮고 평평한 CSS를 규모가 커져도 다루기 쉽게 유지하는 방법이며, 여기서 로딩 순서는 핵심 뼈대입니다. 흔한 구분은 base(리셋과 순수 요소의 기본값), 컴포넌트(캡슐화된 조각, 파일당 하나씩), 유틸리티(원자적인 단일 속성 헬퍼)입니다. 이 순서 규칙은 캐스케이드에서 직접 따라온 것입니다. 명시도를 의도적으로 평평하게 유지했으므로, 작성 순서가 주된 동점자 판정 기준이 되고, 파일이 로딩되는 순서가 같은 무게의 규칙 중 어느 것이 이길지를 결정합니다.

그래서 순서가 의도상 덜 구체적인 것에서 더 구체적인 것 순으로 흐릅니다. base, 그다음 컴포넌트, 그다음 유틸리티입니다. 이렇게 하면 명시도를 조금도 올리지 않고도 나중 그룹이 앞선 그룹을 덮어쓸 수 있습니다. .text-center 유틸리티는 컴포넌트 자체의 텍스트 정렬을 이겨야 하는데, 순전히 같은 무게로 마지막에 로딩된다는 이유만으로 이길 수 있습니다. 이 방식은 잘 작동하지만, 언어가 강제하는 것이 아니라 규율로 지켜지는 컨벤션일 뿐입니다. 누군가가 유틸리티를 컴포넌트보다 먼저 임포트해버리면 전체 체계가 조용히 뒤집혀도 언어 차원에서 막을 방법이 없습니다. 대규모 팀에서 작성 순서에만 의존하는 것이 바로 이런 이유로 취약하며, 이것이 마지막 절에서 다룰, 순서를 위치가 아니라 명시적으로 선언하게 만드는 방식의 동기가 됩니다.

css
/* main.css: 덜 구체적인 것부터 더 구체적인 것 순으로 */
@import "base/reset.css";        /* 요소 기본값 */
@import "base/typography.css";

@import "components/card.css";    /* 독립된 조각들 */
@import "components/button.css";

@import "utilities/spacing.css";  /* 마지막에 로딩되어 덮어쓸 수 있음 */
@import "utilities/text.css";
Juno파일 구조 잡기 CSS를 규칙이 하는 역할별로 폴더에 나누세요. base는 요소 기본값, 컴포넌트는 카드나 버튼 같은 조각들, 유틸리티는 작은 헬퍼들입니다. 그리고 불러오는 순서를 잘 보세요. 명시도가 같으면 나중 규칙이 이깁니다. 일반적인 것을 먼저, 구체적인 것을 나중에 불러와서 헬퍼가 조각들을 덮어쓸 수 있게 하세요.
Juno파일 구조 잡기 파일을 역할별로 묶으세요. base, 컴포넌트, 그다음 유틸리티입니다. 덜 구체적인 것부터 더 구체적인 것 순으로 불러오세요. 명시도가 같을 때는 작성 순서가 동점자 판정 기준이 되기 때문에, 마지막에 로딩된 유틸리티가 명시도를 올리지 않고도 컴포넌트를 덮어쓸 수 있습니다. 순서를 제대로 잡는 것이야말로 모든 것을 단일 클래스 무게로 유지하면서도 덮어쓰기가 잘 적용되게 해주는 핵심입니다.
Juno파일 구조 잡기 명시도를 의도적으로 평평하게 만들면, 작성 순서가 주된 동점자 판정 기준이 되어 파일 로딩 순서가 같은 무게의 규칙 중 어느 것이 이길지를 결정합니다. base, 그다음 컴포넌트, 그다음 유틸리티라는 순서면 명시도를 조금도 올리지 않고도 나중 그룹이 앞선 그룹을 자연스럽게 덮어씁니다. 함정은 이것이 규율로 지켜지는 컨벤션이라는 점입니다. 유틸리티를 너무 일찍 임포트하면 전체 체계가 뒤집혀버리는데, 바로 이 문제 때문에 다음 단계에서는 순서를 명시적으로 선언하게 됩니다.

캐스케이드를 명시적으로 만들기

지금까지 살펴본 모든 방법은 컨벤션을 통해 캐스케이드를 다룰 만하게 만드는 것이었습니다. 낮은 명시도, 좋은 이름, 신중한 파일 순서가 그것입니다. 더 깊이 들어가면 이 이상의 방법도 있으니, 앞으로 나아갈 방향을 알아두면 좋습니다. 최신 CSS는 어떤 파일이 우연히 어디에 있는지에 의존하지 않고 순서를 직접 제어할 수 있게 해주며, 더 많은 사람이 같은 스타일시트에서 함께 작업할 때도 캐스케이드를 예측 가능하게 유지하는 방법을 제공합니다. 프로젝트가 커지면서 이런 도구를 만나게 될 텐데, 지금까지 익힌 습관들이 바로 그것을 준비시켜주는 밑바탕입니다.

지금까지 다룬 기법들은 명시도와 작성 순서를 통해 캐스케이드를 간접적으로 관리합니다. 더 최신 CSS는 캐스케이드를 직접 관리하게 해줍니다. **캐스케이드 레이어**는 @layer 규칙으로 작성하며, 이름이 붙은 순서 버킷입니다. 레이어를 원하는 승리 순서대로 미리 선언해두면, 한 레이어 안의 모든 규칙은 명시도와 무관하게 앞선 레이어의 모든 규칙을 이깁니다.

css
/* 승리 순서를 한 번에 선언, 가장 지는 것부터 가장 이기는 것까지 */
@layer base, components, utilities;

@layer components {
  .card { padding: 16px; }
}

@layer utilities {
  .p-0 { padding: 0; }   /* 명시도가 같아도 .card를 이김 */
}

나중 레이어가 항상 이기기 때문에, 파일 순서가 완벽할 필요가 없어지고, 억지로 덮어쓰기 위해 !important를 쓸 일도 거의 없어집니다. 이것이 실질적인 이점입니다. 레이어는 "어느 파일이 먼저 로딩됐는가"라는 취약한 영역에서 순서 결정을 꺼내와서, 누구나 읽을 수 있는 명시적인 선언으로 바꿔줍니다.

작성 순서는 위치에 의존하기 때문에 취약합니다. 누군가가 임포트 순서를 바꾸는 순간 깨져버립니다. 캐스케이드 레이어, 즉 @layer 규칙은 이를 캐스케이드에서 명시도보다 위에 자리하는 명시적인 순서로 대체합니다. 레이어 순서를 한 번 선언해두면, 캐스케이드는 명시도를 보기 전에 먼저 레이어 순서를 확인하므로, 앞선 레이어의 규칙이 더 구체적이더라도 나중 레이어의 규칙이 이깁니다. 순서 결정이 파일 위치의 부수 효과가 아니라 이름 붙은, 읽을 수 있는 결정이 됩니다.

css
/* 이 한 줄이 코드베이스 전체의 승리 순서를 정함 */
@layer reset, base, components, utilities;

@layer components {
  .card__title { font-size: 1.25rem; }
}

@layer utilities {
  .text-lg { font-size: 1.5rem; }   /* 승리: utilities가 더 나중 레이어이므로 */
}

이는 두 가지 습관을 다시 만듭니다. 첫째, 명시도 전쟁을 무장 해제시킵니다. 레이어 순서가 명시도보다 우선하기 때문에, 충돌을 이기기 위해 더 구체적인 선택자를 찾을 필요가 없어지고, !important도 거의 필요 없어집니다. !important의 존재 이유는 다른 방법으로는 이길 수 없는 명시도를 회피하는 것이었으니까요. (레이어는 !important의 우선순위조차 뒤집어서, 앞선 레이어의 important 선언이 이기게 만들지만, 목표는 이 사실이 그저 잡학 상식으로 남을 만큼 !important를 쓸 일 자체를 줄이는 것입니다.) 둘째, 오래된 두 조직화 방식 사이의 선택을 명확하게 해줍니다. 여러 개의 원자적인 단일 속성 클래스로 페이지를 구성하는 유틸리티 우선 접근법과, 조각마다 하나의 시맨틱 클래스 뒤에 스타일을 묶어두는 컴포넌트 접근법입니다. 대부분의 프로덕션 코드베이스는 둘 다 사용하는데, 레이어는 이 둘이 설계상 공존할 수 있게 해줍니다. 컴포넌트를 components 레이어에, 유틸리티를 그보다 나중의 utilities 레이어에 두면, 유틸리티는 어느 쪽도 명시도를 끌어올리지 않고도 안정적으로 컴포넌트를 덮어씁니다. 결정은 더 이상 "캐스케이드 싸움에서 누가 이기는가"가 아니라 "이 UI 조각에는 어느 방식이 더 잘 읽히는가"가 됩니다. 캐스케이드는 누가 더 밀어붙이는 선택자를 썼는지가 아니라 레이어 순서로 결정되기 때문입니다. 팀이 커질수록 이 예측 가능성이 진짜 보상입니다. 승리 순서가 임포트 순서에 대한 부족 지식이 아니라 누구나 읽을 수 있는 하나의 선언이 되기 때문입니다. 이 레이어들 사이에서 색상이나 간격 같은 값을 공유해야 할 때는, 커스텀 프로퍼티를 이용해 값을 한 번만 정의하고 중복을 피할 수 있습니다.

Juno캐스케이드를 명시적으로 만들기 지금까지는 낮은 명시도, 명확한 이름, 신중한 로딩 순서 같은 습관으로 캐스케이드를 다스렸습니다. 더 나아가면 CSS는 어느 파일이 먼저 로딩됐는지에 의존하지 않고 승리 순서를 직접 정할 수 있는 도구를 제공합니다. 지금 당장 필요하지는 않지만, 이 장에서 익힌 습관들이 바로 그 도구를 쓸 준비를 시켜줍니다.
Juno캐스케이드를 명시적으로 만들기@layer로 만드는 캐스케이드 레이어는 이름 붙은 순서 버킷입니다. 레이어 순서를 미리 선언해두면 나중 레이어가 명시도와 무관하게 앞선 레이어를 이깁니다. 그러면 파일 순서가 완벽할 필요가 없어지고, 승리를 억지로 만들기 위해 !important를 쓸 일도 거의 없어집니다. 순서 결정을 취약한 파일 위치에서 꺼내와서 누구나 읽을 수 있는 한 줄로 옮겨줍니다.
Juno캐스케이드를 명시적으로 만들기 캐스케이드 레이어는 명시도보다 위에 있어서, 나중 레이어가 더 구체적인 규칙조차 이기며, 이는 명시도 톱니바퀴를 풀어주고 대부분의 !important 사용을 사라지게 합니다. 또한 유틸리티 우선 방식과 컴포넌트 방식이 공존할 수 있게 해줍니다. 컴포넌트를 한 레이어에, 유틸리티를 그보다 나중 레이어에 두면 어느 쪽도 명시도를 끌어올리지 않고도 유틸리티가 컴포넌트를 덮어씁니다. 팀이 커질수록 얻는 이득은, 순서가 임포트 순서에 대한 부족 지식이 아니라 하나의 읽을 수 있는 선언이 된다는 점입니다.