CSS 커스텀 프로퍼티(CSS 변수, --variable 형태로 선언하는 속성)는 CSS 상속 메커니즘에 완전히 참여한다는 점에서 일반 CSS 속성과 근본적으로 다른 특성을 가집니다. 대부분의 CSS 속성은 속성마다 상속 여부가 고정되어 있지만, 커스텀 프로퍼티는 기본적으로 항상 상속되며 DOM 트리를 따라 자식 요소로 전파됩니다. 이 특성 덕분에 부모 요소에 한 번 정의한 변수를 모든 자식 요소에서 자유롭게 참조할 수 있고, 특정 하위 트리에서만 값을 재정의하여 컴포넌트 단위의 테마를 손쉽게 구현할 수 있습니다. Sass나 Less 같은 전처리기 변수가 컴파일 시점에 값으로 치환되어 버리는 것과 달리, CSS 커스텀 프로퍼티는 런타임에 상속과 캐스케이드에 따라 동적으로 결정되기 때문에 훨씬 유연한 패턴을 가능하게 합니다.
핵심 특징
- 기본 상속 동작: 커스텀 프로퍼티는 별도의 설정 없이 DOM 트리를 따라 부모에서 자식으로 자동 상속됩니다
- 캐스케이드 참여: 일반 CSS 속성과 마찬가지로 명시도와 선언 순서에 따라 값이 결정되며, DOM 트리 어느 레벨에서든 재정의할 수 있습니다
- 스코프 기반 재정의: 특정 선택자(컴포넌트 루트 요소 등)에서 변수를 다시 선언하면, 해당 요소와 그 하위 트리에만 새 값이 적용됩니다
- 전처리기 변수와의 근본적 차이: Sass/Less 변수는 빌드 타임에 정적으로 치환되지만, 커스텀 프로퍼티는 런타임에 상속과 캐스케이드를 통해 동적으로 값이 결정됩니다
- 상속 차단 가능:
initial키워드를 사용하면 특정 요소에서 상속을 명시적으로 끊을 수 있어, 상속 흐름을 세밀하게 제어할 수 있습니다
실무에서의 영향
CSS 커스텀 프로퍼티의 상속 특성은 컴포넌트 기반 설계에서 테마 시스템을 구축하는 방식을 완전히 바꿔놓습니다. 예를 들어 :root에 전역 색상 팔레트를 선언하고, 특정 섹션이나 컴포넌트 루트에서 몇 가지 변수만 재정의하면 다크 모드, 브랜드 테마, 컨텍스트별 색상 변경을 CSS만으로 구현할 수 있습니다. 이 패턴은 JavaScript로 DOM을 직접 조작하거나 수십 개의 클래스를 관리할 필요 없이, 단 하나의 클래스 전환으로 하위 트리 전체의 시각적 표현을 바꿀 수 있게 해줍니다. 디자인 토큰을 커스텀 프로퍼티로 관리하면 디자인 시스템과 컴포넌트 라이브러리 사이의 계약을 CSS 레벨에서 명확하게 정의할 수 있어, 유지보수성과 일관성이 크게 향상됩니다. 또한 상속이 런타임에 동작하기 때문에 미디어 쿼리나 JavaScript와 조합하면 사용자 설정이나 시스템 환경에 반응하는 동적 테마도 손쉽게 구현할 수 있어, 현대 웹 UI 개발의 핵심 기법으로 자리잡고 있습니다.
핵심 개념
커스텀 프로퍼티의 기본 상속 동작
입문
CSS 변수(커스텀 프로퍼티)는 부모 요소에서 선언하면 자식 요소들이 자동으로 물려받아요. 마치 가족 규칙처럼, 부모가 정한 것을 자식들이 따르는 거예요!
📋 가족 규칙판 비유 집에 큰 규칙판이 있다고 생각해요. 부모님이 “우리 집 주색은 파란색”이라고 써두면, 방에 있는 모든 물건(자식 요소)들이 자동으로 파란색을 쓰게 돼요. 방마다 별도로 알려주지 않아도요. CSS 변수도 똑같이 부모에서 자식으로 자동으로 전달돼요.
🌳 나무처럼 퍼져나가요
DOM 트리는 나무와 같아요. 뿌리(root)에 색을 칠하면 줄기를 타고 모든 가지(자식 요소)로 색이 퍼져나가요. :root에 CSS 변수를 선언하면 페이지 전체에서 그 변수를 사용할 수 있어요.
🎨 일반 CSS 속성과 다른 점 보통 CSS 속성은 종류에 따라 상속이 되는 것도 있고 안 되는 것도 있어요. 예를 들어 글자 색은 상속되지만 테두리는 상속이 안 돼요. 그런데 CSS 변수는 항상 상속돼요. 예외 없이요!
📦 변수는 값이 아니라 ‘이름표’예요 CSS 변수는 실제 값(파란색, 16px 등)이 아니라 이름표를 전달해요. 자식 요소가 그 이름표를 보고 현재 값을 찾는 거예요. 그래서 나중에 값이 바뀌어도 이름표만 그대로면 자동으로 업데이트돼요.
중급
CSS 커스텀 프로퍼티(--변수명 형식)는 CSS 명세에 따라 기본적으로 상속(inherited) 속성으로 동작합니다. 이는 일반 CSS 속성의 상속 여부가 속성마다 고정된 것과 달리, 모든 커스텀 프로퍼티가 예외 없이 DOM 트리를 따라 부모에서 자식으로 전파된다는 의미입니다.
상속 전파 범위
커스텀 프로퍼티는 선언된 요소와 그 모든 자손 요소에서 var() 함수를 통해 참조 가능합니다. 선언된 선택자의 스코프(범위) 내에서만 유효하며, 형제 요소나 조상 요소로는 전파되지 않습니다.
/* :root에 선언하면 전체 문서에서 사용 가능 */
:root {
--brand-color: #0066cc;
--font-size-base: 16px;
}
/* 어느 자식 요소에서든 var()로 참조 가능 */
h1 { color: var(--brand-color); }
p { font-size: var(--font-size-base); }
.button { background-color: var(--brand-color); }
/* .card 안에서만 유효한 변수 */
.card {
--card-padding: 1.5rem;
}
.card-body {
padding: var(--card-padding); /* 정상 작동 */
}
.sidebar {
padding: var(--card-padding); /* undefined - .card 밖이라 무효 */
}
상속 vs 캐스케이드 커스텀 프로퍼티 값 결정에는 캐스케이드(명시도, 순서)와 상속이 모두 관여합니다. 같은 요소에 여러 선택자가 동일한 커스텀 프로퍼티를 선언하면 캐스케이드 규칙에 따라 하나가 결정되고, 그 값이 자식으로 상속됩니다.
심화
CSS 커스텀 프로퍼티의 상속 동작은 W3C CSS Custom Properties for Cascading Variables Module Level 1 명세(CSS Variables, 이하 CSS Variables spec)에서 정의된 것으로, 일반 CSS 속성과 동일한 상속 메커니즘에 완전히 참여합니다.
CSS Variables 명세의 상속 정의 CSS Custom Properties Module Level 1 §3에 따르면, 커스텀 프로퍼티의 initial value는 “guaranteed-invalid value”이며 상속 플래그는 항상 “yes”로 고정됩니다. 이는 속성 정의(property definition)에서 inherited 플래그를 “yes”로 명시한 것으로, color나 font-size처럼 상속이 기본 동작인 속성과 동일한 상속 경로를 따릅니다.
브라우저 엔진 구현과 값 전파 메커니즘
Blink 엔진(Chromium)에서 커스텀 프로퍼티는 ComputedStyle 객체 내부의 CSSVariableData 맵에 저장됩니다. 스타일 상속 시, 엔진은 부모의 CSSVariableData 맵 전체를 자식에게 전달하고 자식이 재선언한 프로퍼티만 덮어씁니다. 이 때문에 커스텀 프로퍼티 상속의 메모리 비용은 재선언되지 않은 변수 수에 비례하지 않고, 포인터 공유(copy-on-write)로 최적화됩니다. var() 함수는 파싱 시점이 아닌 스타일 계산(computed value) 시점에 해석되며, 현재 요소의 ComputedStyle에서 변수 값을 조회합니다.
guaranteed-invalid value와 폴백(fallback) 처리
변수가 현재 스코프에서 정의되지 않은 경우(부모에게도 없는 경우), var(--undefined, fallback) 구문의 두 번째 인자가 사용됩니다. 폴백이 없으면 해당 선언이 invalid at computed-value time으로 처리되어, 해당 속성의 inherited value(상속 가능 속성) 또는 initial value(비상속 속성)로 대체됩니다. 이 동작은 CSS Variables spec §3.1에 명시되어 있으며, 브라우저가 일반 속성의 cascaded value 결정 실패 시와 동일한 폴백 경로를 따릅니다.
스코프와 재정의 메커니즘
입문
CSS 변수는 선언한 곳 안에서만 유효해요. 그리고 자식 요소에서 같은 이름의 변수를 다시 선언하면, 그 영역 안에서는 새 값이 덮어써요. 마치 반 규칙이 나라 규칙을 바꾸는 것처럼요!
🏫 학교 규칙 vs 반 규칙
나라 전체의 규칙(:root 변수)이 있고, 각 학교의 규칙, 각 반의 규칙이 있어요. 반에서 “우리 반만의 규칙”을 정하면, 그 반 안에서는 반 규칙이 우선이에요. 그렇다고 다른 반이나 학교 전체가 영향받지는 않아요.
🎭 무대 뒤쪽에서만 바뀌는 조명 연극 무대에서 한 장면만 조명 색을 바꾸고 싶다면, 그 장면(특정 영역)에만 다른 색을 지정하면 돼요. 다른 장면들은 원래 전체 조명 색을 그대로 써요. CSS 변수도 이렇게 특정 영역에서만 값을 바꿀 수 있어요.
🔄 자식은 가장 가까운 부모 값을 써요 만약 할아버지, 아버지, 자식이 모두 같은 이름의 변수를 갖고 있다면, 자식은 가장 가까운 아버지의 값을 사용해요. 먼 조상의 값이 아니라 가장 최근에 선언된 값이 이겨요.
💡 이게 왜 유용한가요? 버튼 컴포넌트가 페이지 어디에 있든 그 버튼만의 색을 지정하고 싶을 때, 버튼의 부모 요소에서 변수를 재선언하면 돼요. 다른 곳의 버튼들은 전혀 영향받지 않아요!
중급
커스텀 프로퍼티의 스코프는 선언된 선택자가 매칭하는 요소를 루트로 하는 서브트리입니다. 동일한 변수 이름을 하위 요소에서 재선언하면 해당 요소와 그 자손 범위에서만 새 값이 적용됩니다. 이를 통해 동일한 변수 이름으로 컨텍스트별 다른 값을 제공할 수 있습니다.
:root {
--color-primary: #0066cc; /* 전역 기본값 */
}
/* 이 섹션 안에서만 primary 색이 바뀜 */
.promo-section {
--color-primary: #ff4400;
}
/* 두 버튼 모두 var(--color-primary)를 쓰지만 결과가 다름 */
.button { background-color: var(--color-primary); }
/* .promo-section 밖의 버튼: #0066cc */
/* .promo-section 안의 버튼: #ff4400 */
:root {
--bg: #ffffff;
--text: #111111;
}
[data-theme="dark"] {
--bg: #111111;
--text: #ffffff;
}
body {
background-color: var(--bg);
color: var(--text);
}
상속 차단: initial 키워드
특정 요소에서 커스텀 프로퍼티의 상속을 끊으려면 initial 키워드를 사용합니다. initial은 커스텀 프로퍼티의 초기값인 “guaranteed-invalid value”로 설정하여, var() 사용 시 폴백 값이 적용되거나 해당 선언이 무효화됩니다.
:root { --accent: #0066cc; }
.isolated-widget {
--accent: initial; /* 상속 차단 */
}
/* .isolated-widget 안에서 var(--accent)는 폴백 값 사용 */
.isolated-widget .icon {
color: var(--accent, gray); /* gray (폴백) 적용 */
}
심화
커스텀 프로퍼티의 스코프 재정의는 CSS 캐스케이드의 일반 규칙을 따르며, 값 결정은 inherited value 체인과 캐스케이드 레이어를 모두 거쳐 이루어집니다.
캐스케이드에서의 커스텀 프로퍼티 값 결정 CSS Variables spec §3에서 커스텀 프로퍼티는 캐스케이드(명시도, 선언 순서, 출처, @layer)에 완전히 참여합니다. 즉, 동일 요소에 여러 규칙이 같은 커스텀 프로퍼티를 선언하면 일반 CSS 속성과 동일한 우선순위 알고리즘으로 하나가 결정됩니다. 그 캐스케이드 결과값이 상속 체인을 통해 자손으로 전파됩니다.
스코프 재정의의 메모리 및 성능 특성
Blink 엔진에서 커스텀 프로퍼티 재선언은 해당 요소의 ComputedStyle에 새 CSSVariableData 항목을 생성합니다. Copy-on-write 방식으로 부모의 맵을 기반으로 재선언된 변수만 교체하므로, 재선언하지 않은 변수는 메모리 복사 없이 부모 포인터를 공유합니다. initial 키워드를 커스텀 프로퍼티에 적용하면, 해당 변수는 맵에서 “guaranteed-invalid” 마커로 저장됩니다. var(--prop) 조회 시 invalid 마커를 발견하면, var() 함수는 폴백 인자를 평가하거나 폴백이 없으면 해당 선언 전체를 invalid로 처리합니다. 이 과정은 CSS Variables spec §3.1의 “invalid at computed-value time” 개념에 해당합니다.
@layer와의 상호작용 CSS Cascading and Inheritance Level 5의 @layer 내에서 커스텀 프로퍼티를 선언하면, 레이어 우선순위 규칙이 일반 속성과 동일하게 적용됩니다. 레이어 외부 선언이 레이어 내 선언을 이기므로, 레이어 내부에서 정의한 커스텀 프로퍼티는 레이어 외부에서 쉽게 재정의할 수 있습니다. 이를 통해 테마 기반 설계에서 @layer base에 커스텀 프로퍼티 기본값을 정의하고, 컴포넌트나 유틸리티 레이어에서 재정의하는 아키텍처를 명확하게 표현할 수 있습니다.
전처리기 변수와의 핵심 차이
입문
Sass나 Less 같은 도구에서 쓰는 변수($color: blue)는 CSS 변수(--color: blue)와 전혀 달라요. 겉보기엔 비슷하지만 동작 방식이 완전히 달라요. 사진과 살아있는 사람의 차이라고 생각하면 돼요!
📸 사진 vs 살아있는 사람 Sass 변수는 사진처럼 한 번 찍히면 영원히 그 순간만 담아요. 웹사이트가 만들어지는 순간(빌드 타임) 값이 결정되고, 그 이후에는 절대 변하지 않아요. 반면 CSS 변수는 살아있어서 페이지가 보여지는 동안에도 언제든 바뀔 수 있어요.
🌙 다크 모드를 예로 들면 Sass 변수로 다크 모드를 만들려면, 밝은 버전과 어두운 버전 두 벌의 CSS 파일을 만들어야 해요. 하지만 CSS 변수를 쓰면 파일 하나만으로 버튼 클릭에 즉각적으로 색상이 바뀌어요. 마치 방에 스위치를 달아두는 것처럼요!
🧬 상속의 차이
Sass 변수는 쓰는 순간 값으로 바뀌어버려요. $color: blue라고 쓰면 코드 안에서 blue로 그냥 교체돼요. 하지만 CSS 변수는 이름 그대로 전달되고, 부모에서 자식으로 상속되면서 그때그때 적절한 값으로 읽혀요.
🤔 그럼 어느 게 더 좋은가요? 상황에 따라 달라요! 값이 변할 필요가 없고 코드 정리용으로만 쓴다면 Sass 변수도 충분해요. 하지만 테마 전환, 사용자 설정, 다크 모드처럼 실시간으로 바뀌어야 하는 건 CSS 변수만 가능해요.
중급
전처리기 변수(Sass $var, Less @var)와 CSS 커스텀 프로퍼티는 이름만 유사할 뿐 처리 시점, 스코프 모델, 상속 여부가 근본적으로 다릅니다.
처리 시점 차이
- 전처리기 변수: 빌드(컴파일) 타임에 해석되어 최종 CSS에는 리터럴 값만 남습니다. 브라우저는 변수를 볼 수 없습니다.
- CSS 커스텀 프로퍼티: 브라우저가 런타임에 해석합니다. DevTools에서 변수 이름이 그대로 보이고, JavaScript로 읽고 쓸 수 있습니다.
/* Sass - 빌드 타임에 값으로 치환됨 */
$primary: #0066cc;
.button { color: $primary; } /* → .button { color: #0066cc; } */
/* 출력된 CSS에는 변수가 존재하지 않음 */
/* CSS 커스텀 프로퍼티 - 런타임에 상속/해석 */
:root { --primary: #0066cc; }
.button { color: var(--primary); } /* 브라우저가 직접 해석 */
/* JavaScript로 동적 변경 가능 */
/* document.documentElement.style.setProperty('--primary', '#ff4400'); */
:root { --bg: #fff; --text: #000; }
[data-theme="dark"] {
--bg: #000;
--text: #fff;
}
/* JavaScript: element.dataset.theme = 'dark' 한 줄로 전체 테마 전환 */
body { background: var(--bg); color: var(--text); }
스코프 모델 차이 Sass 변수는 선언 순서 기반의 렉시컬 스코프(lexical scope)를 가지며, 규칙 블록 안팎에 따라 전역/지역 변수로 구분됩니다. CSS 커스텀 프로퍼티는 DOM 트리 기반의 상속 스코프를 가지며, 선택자가 매칭하는 요소를 루트로 하위 트리 전체로 상속됩니다.
심화
전처리기 변수와 CSS 커스텀 프로퍼티의 차이는 단순한 문법 차이가 아니라, 컴파일 타임 정적 치환(static substitution)과 런타임 상속 기반 동적 해석(runtime inheritance-based resolution)이라는 근본적으로 다른 두 패러다임의 차이입니다.
컴파일 타임 vs 런타임 처리 모델
Sass $var는 Sass 컴파일러의 AST(Abstract Syntax Tree) 순회 과정에서 변수 참조를 해당 값으로 텍스트 치환합니다. 출력된 CSS는 변수 흔적이 전혀 없으며, 브라우저의 CSS 파서나 스타일 엔진은 변수의 존재를 인식하지 못합니다. 반면 CSS 커스텀 프로퍼티는 CSS Variables spec에 따라 브라우저의 스타일 계산 파이프라인(style calculation pipeline) 내부에서 처리됩니다. var() 함수는 computed value 단계에서 해석되며, 이 시점에 DOM 구조와 상속 체인을 실시간으로 참조합니다.
DOM 기반 스코프 vs 렉시컬 스코프
Sass의 변수 스코프는 렉시컬(lexical) 스코프로, CSS 파일의 텍스트 구조에 따라 결정됩니다. 규칙 블록 밖의 $var는 전역, 블록 안의 $var는 지역으로 처리되며 이 관계는 DOM과 무관합니다. CSS 커스텀 프로퍼티의 스코프는 DOM 트리 기반입니다. 동일한 CSS 파일에 동일한 변수 재선언이 있더라도, 어떤 DOM 요소가 어떤 선택자에 매칭되는지에 따라 최종 값이 결정됩니다. 이 차이는 미디어 쿼리, 의사 클래스(:hover, :focus 등), JavaScript의 DOM 조작과 결합할 때 CSS 커스텀 프로퍼티만이 제공할 수 있는 동적 반응성을 만들어냅니다.
JavaScript 상호운용성
CSS 커스텀 프로퍼티는 CSSOM(CSS Object Model)에 노출됩니다. getComputedStyle(element).getPropertyValue('--var') 및 element.style.setProperty('--var', value) API를 통해 JavaScript에서 읽기/쓰기가 가능합니다. 전처리기 변수는 빌드 산출물에 존재하지 않으므로 JavaScript에서 접근할 방법이 없습니다. 이 상호운용성은 컴포넌트 라이브러리, 디자인 토큰 시스템, 사용자 설정 테마 등 JavaScript와 CSS의 상태를 동기화해야 하는 패턴에서 CSS 커스텀 프로퍼티의 근본적 우위를 만듭니다.
테마 설계 패턴
입문
CSS 변수의 상속 특성을 이용하면 웹사이트 테마(전체 모습)를 아주 쉽게 바꿀 수 있어요. 다크 모드처럼 버튼 하나로 전체 색상이 바뀌는 마법도 CSS 변수 덕분이에요!
🎨 팔레트를 한 번에 바꾸는 마법
화가가 물감 팔레트를 통째로 바꾸면 그림 전체의 분위기가 바뀌는 것처럼, :root의 CSS 변수 몇 가지만 바꾸면 웹사이트 전체 색상이 한 번에 바뀌어요. 각 요소를 하나씩 고칠 필요가 없어요!
🌙 다크 모드는 이렇게 만들어요 “낮 모드”에서는 배경이 흰색, 글씨가 검정이에요. “밤 모드”에서는 배경이 어두운 색, 글씨가 밝은 색이고요. CSS 변수를 쓰면, 단 하나의 클래스나 속성을 추가하는 것만으로 이 모든 색상이 자동으로 바뀌어요. 마치 스위치 하나로 방 조명 전체를 바꾸는 것처럼요!
🧩 컴포넌트마다 자기 색을 가질 수 있어요 버튼 컴포넌트가 있을 때, 기본 버튼, 위험 버튼, 성공 버튼처럼 여러 종류가 있잖아요. CSS 변수를 쓰면 각 종류의 버튼이 자기만의 색 변수를 갖고, 나머지 구조(크기, 모양)는 공유할 수 있어요. 옷은 같은 디자인인데 색만 다른 것처럼요!
🔧 나중에 고치기도 쉬워요
CSS 변수를 쓰지 않으면 색상 하나를 바꾸려고 파일 전체를 뒤져야 해요. 하지만 CSS 변수를 쓰면 :root에서 변수 값 하나만 고치면 사이트 전체에 반영돼요. 유지보수가 훨씬 쉬워져요.
중급
CSS 커스텀 프로퍼티의 상속 특성을 이용한 테마 설계는 크게 세 가지 패턴으로 나뉩니다: 전역 토큰 선언, 컨텍스트 재정의(다크 모드 등), 컴포넌트 스코프 테마입니다.
패턴 1: 전역 디자인 토큰
:root에 색상, 폰트, 간격 등의 디자인 토큰을 선언하고 전체 컴포넌트가 상속받습니다.
:root {
/* 색상 팔레트 */
--color-primary: #0066cc;
--color-danger: #cc0000;
--color-success: #00aa44;
/* 타이포그래피 */
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.25rem;
/* 간격 */
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 2rem;
}
패턴 2: 다크 모드 컨텍스트 재정의 최상위 요소에서 변수를 재선언하면 하위 전체가 자동으로 바뀝니다. JavaScript로 클래스나 속성 하나만 토글합니다.
:root {
--surface: #ffffff;
--on-surface: #111111;
--primary: #0066cc;
}
[data-theme="dark"] {
--surface: #1a1a1a;
--on-surface: #f0f0f0;
--primary: #66aaff;
}
/* 모든 컴포넌트가 동일한 var()를 쓰면 자동 전환 */
.card { background: var(--surface); color: var(--on-surface); }
.button { background: var(--primary); }
/* 버튼 컴포넌트: 내부 변수로 variant 구현 */
.button {
--btn-bg: var(--color-primary);
--btn-text: white;
background: var(--btn-bg);
color: var(--btn-text);
}
.button--danger { --btn-bg: var(--color-danger); }
.button--success { --btn-bg: var(--color-success); }
패턴 3: 시스템 미디어 쿼리 연동
prefers-color-scheme 미디어 쿼리로 OS 다크 모드 설정에 자동 반응하도록 구성할 수 있습니다.
:root { --bg: #fff; --text: #000; }
@media (prefers-color-scheme: dark) {
:root { --bg: #111; --text: #fff; }
}
심화
커스텀 프로퍼티 기반 테마 설계는 디자인 토큰(Design Token) 아키텍처와 결합할 때 가장 강력한 유지보수성을 발휘합니다. W3C Design Tokens Community Group의 디자인 토큰 형식과 CSS 커스텀 프로퍼티를 연결하는 것이 현대 디자인 시스템의 표준 패턴으로 자리잡고 있습니다.
디자인 토큰과 커스텀 프로퍼티의 계층 구조
효과적인 테마 시스템은 토큰을 세 계층으로 분리합니다. 첫째, Primitive Token(원시 토큰)은 실제 값을 담는 최하위 계층으로 --color-blue-500: #0066cc처럼 값 자체를 표현합니다. 둘째, Semantic Token(의미 토큰)은 사용 맥락을 표현하며 --color-primary: var(--color-blue-500)처럼 원시 토큰을 참조합니다. 셋째, Component Token(컴포넌트 토큰)은 특정 컴포넌트에 한정되며 --button-bg: var(--color-primary)처럼 시맨틱 토큰을 참조합니다. 이 계층화는 CSS 커스텀 프로퍼티의 var() 중첩 참조로 표현됩니다. 브라우저는 var() 참조를 computed value 단계에서 재귀적으로 해석하므로, 이론상 무제한 중첩이 가능하나 실무에서는 2-3 계층이 권장됩니다.
런타임 테마 전환과 스타일 재계산 비용
다크 모드 전환을 위해 [data-theme="dark"] 속성을 최상위 요소에 추가하면, Blink 엔진은 해당 요소의 커스텀 프로퍼티 값 변경을 감지하고 상속 트리를 통해 연쇄적 스타일 재계산(style recalculation)을 트리거합니다. 이 재계산 범위는 변경된 커스텀 프로퍼티를 var()로 직접 또는 간접 참조하는 모든 요소입니다. Blink의 스타일 무효화(style invalidation) 메커니즘은 커스텀 프로퍼티 변경에 대해 “custom property subtree invalidation”을 수행하며, --var를 사용하는 요소를 선택적으로 무효화합니다. 대규모 DOM에서 전역 커스텀 프로퍼티 변경은 수천 개 요소의 재계산을 유발할 수 있으므로, will-change 힌트나 CSS Containment(contain: style)를 활용하여 재계산 범위를 제한하는 최적화가 필요한 경우가 있습니다.
Shadow DOM 환경에서의 커스텀 프로퍼티 테마
Shadow DOM은 스타일을 캡슐화하지만, 커스텀 프로퍼티는 Shadow Boundary를 관통하여 상속됩니다. 이는 CSS Variables spec에서 명시적으로 정의된 동작으로, Web Components 기반 디자인 시스템에서 커스텀 프로퍼티를 “공개 API(public theming API)“로 활용하는 근거가 됩니다. 컴포넌트 내부에서 var(--comp-bg, white)처럼 폴백과 함께 커스텀 프로퍼티를 참조하면, 외부에서 --comp-bg를 선언한 경우 테마가 적용되고 선언하지 않은 경우 기본값(white)이 유지됩니다. 이 패턴은 ::part()와 ::slotted()의 한계를 보완하여 더 유연한 외부 커스터마이징 인터페이스를 제공합니다.