정의
사이트의 색·간격·모서리 반경 같은 디자인 값을 한 곳에서 관리하고, 라이트/다크 모드나 브랜드별 테마를 "값만 바꿔서" 전환하고 싶을 때 쓰는 방법이다. 이를 위해 값을 CSS 커스텀 프로퍼티(--token 형태로 선언해 재사용하는 변수)로 빼두고, 모던 CSS의 세 도구로 적용 규칙을 나눈다: 어떤 규칙이 이기는가는 @layer(캐스케이드 레이어 — CSS 규칙들을 순서 있는 묶음으로 나눠 우선순위를 정하는 기능), 어디에 적용되는가는 @scope(스타일이 먹히는 DOM 범위를 한정하는 기능), 모드에 따라 어떤 값인가는 light-dark()(라이트/다크에 따라 두 값 중 하나를 고르는 CSS 함수). 그러면 테마 전환은 토큰 값만 바꾸는 일로 줄어든다.
왜 필요한가
테마 색을 여기저기 클래스·인라인 스타일에 직접 박아두면, "이 색이 왜 안 바뀌지?" 같은 문제가 생긴다. 같은 요소에 규칙이 여러 개 걸려 서로 이기려고 싸우고(우선순위 충돌), 같은 값이 여러 곳에 중복되며, 다크모드용 분기가 곳곳에 늘어난다. 원인은 대개 명시도(specificity — .a .b처럼 선택자가 구체적일수록 이기는 CSS의 우선순위 규칙) 싸움에 묻혀 찾기 어렵다. "이긴 규칙은 무엇인가", "어디까지 적용되나", "이 모드 값은 무엇인가" 세 질문을 각각 @layer·@scope·light-dark()로 분리하면 하나씩 따로 답할 수 있어 디버깅과 확장이 쉬워진다.
동작 원리
| 도구 | 역할 | 주의점 |
|---|---|---|
--token: value | 상속되는 토큰. 값 교체로 전파 | 상속·캐스케이드를 따르므로 적용 경계가 중요 |
@layer reset, base, theme | 레이어 순서로 우선순위 결정 | 뒤 레이어가 명시도와 무관하게 이김 |
@scope (root) to (limit) | 토큰을 서브트리로 한정 | to ([data-theme]) 하한으로 중첩 테마 격리 |
light-dark(l, d) | color-scheme에 따라 두 값 중 선택 | [light, dark] 쌍을 한 프로퍼티로 인코딩 |
@layer로 선언한 레이어들은 선언 순서대로 우선순위가 정해진다. 늦게 온 레이어가 항상 이기므로, reset → base → theme 순서면 테마 레이어가 베이스를 항상 덮는다. 이때 개별 규칙의 명시도는 레이어 간 비교에 영향을 주지 않는다.
실무 적용
런타임에 주입하는 CSS와 빌드 시 생성하는 CSS가 따로 만들어지면 두 결과가 어긋나 테마가 깨진다. 두 소비 경로가 하나의 생성기를 단일 진실 원천으로 공유하게 하고(single-source-of-truth-content-metadata), 산출물은 임시 파일로 쓴 뒤 이름을 바꾸는 원자적 쓰기로 절반만 생성된 상태를 막는다.
트레이드오프
- 장점: 우선순위·범위·모드가 분리돼 디버깅·확장이 쉽고, 명시도 전쟁이 사라진다. 토큰 값 한 곳만 바꿔 전체가 전환된다.
- 비용:
@layer/@scope/light-dark()는 비교적 최신 기능이라 구형 브라우저 지원이 필요하면 폴백이 든다. 레이어 순서 설계를 처음에 잘 잡아야 한다.
사용하면 안 되는 경우
- 테마가 하나뿐이고 다크모드도 없는 단순 사이트(레이어·스코프 도입 이득이 적음).
- 레거시 브라우저 비중이 높아 모던 CSS 기능에 폴백 비용이 과한 경우.
흔한 실수
light-dark()를 쓰면서color-scheme을 설정하지 않아 항상 첫 값만 나온다.- 명시도를 높여 테마를 강제하려다 레이어 모델과 충돌한다(레이어 간 우선순위는 명시도로 못 뒤집는다).
- 런타임·빌드 CSS 생성기를 이원화해 두 결과가 미묘하게 달라진다.
@scope하한(to (...))을 빠뜨려 의도보다 넓은 범위에 토큰이 샌다.
관련 개념
- single-source-of-truth-content-metadata — 동일 산출물을 여러 경로가 소비할 때 생성 로직을 단일 원천으로 두는 원칙.