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

사용자 정의 속성과 현대 CSS

docs.scrimba.com

브랜드 파란색을 선택해서 40개의 다른 규칙에 입력하고 사이트를 배포합니다. 한 달 후 마케팅에서 조금 다른 파란색을 원합니다. 이제 모든 스타일시트를 찾아다니면서 40개를 모두 놓치지 않았는지, 그리고 대문자로 표기된 2개를 빠뜨리지 않았는지 확인해야 합니다. 사용자 정의 속성은 이를 해결합니다. 색상에 한 번 이름을 지정하고 모든 곳에서 그 이름을 사용한 다음 한 곳에서 변경합니다. 이 단 하나의 습관이 테마 지정, 디자인 토큰, 그리고 재설계를 견뎌내는 스타일시트의 시작입니다.

변수 선언 및 사용

**사용자 정의 속성**은 재사용할 수 있도록 이름을 붙인 값입니다. 앞에 두 개의 대시로 이름을 작성하고 값을 설정한 다음 나중에 var()로 다시 읽습니다:

css
:root {
  --color-brand: #2563eb;   /* 한 번만 선언하고 이름 아래에 저장 */
}

.button {
  background: var(--color-brand);   /* 여기서 다시 읽기 */
}

.link {
  color: var(--color-brand);        /* 그리고 여기, 같은 값 */
}

--color-brand는 이름이고 (두 개의 대시는 필수), var(--color-brand)는 사용하는 방식입니다. :root(페이지 최상단인 <html> 요소)에 선언하면 이름이 모든 곳에서 사용 가능합니다. :root의 한 줄을 변경하면 그것을 읽는 모든 규칙이 업데이트됩니다.

**사용자 정의 속성**은 -- 접두사로 선언하고 var() 함수로 다시 읽는 이름 있는 값입니다. CSS 변수라고도 부르며, 두 이름은 같은 의미입니다:

css
:root {
  --color-brand: #2563eb;
  --color-brand-dark: #1e40af;
}

.button {
  background: var(--color-brand);
}

.button:hover {
  background: var(--color-brand-dark);
}

-- 접두사는 사용자 정의 속성을 일반 속성과 구분하는 것이며, 대소문자를 구분하므로 --color-Brand--color-brand는 두 개의 다른 이름입니다. :root에 선언하는 것이 일반적인 선택입니다. 왜냐하면 모든 요소가 볼 수 있는 트리의 최상단에 값을 놓기 때문이며, 이것이 글로벌 토큰이 있어야 할 위치입니다. 색상으로 제한되지 않습니다. 속성이 받아들이는 모든 값(길이, 섀도우, 글꼴 스택 포함)이 사용자 정의 속성에 저장될 수 있습니다.

**사용자 정의 속성**은 이름이 --로 시작하는 사용자 정의 속성이며, var() 함수를 통해 다시 읽는 값을 보유합니다. 이름은 대소문자를 구분하며 값은 거의 그대로 저장됩니다. 브라우저는 선언 시점에 거의 처리하지 않으며, 이는 실제로 해석되는 위치를 보면 중요해집니다.

css
:root {
  --color-brand: #2563eb;
  --space-md: 16px;
  --shadow-card: 0 1px 3px rgb(0 0 0 / 0.12);
}

.card {
  padding: var(--space-md);
  box-shadow: var(--shadow-card);
}

:root에 선언하는 것은 관례이지 필수는 아닙니다. :roothtml 타입 선택자보다 약간 더 높은 구체성으로 <html> 요소와 일치하며, 사용자 정의 속성이 상속되기 때문에 거기서 설정한 값은 모든 후손 요소에 표시됩니다. 보유할 정신 모델은 다음과 같습니다. 글로벌 레지스트리를 만드는 것이 아니라 루트 요소에 속성을 설정하고 있으며, 이것이 전체 문서 아래로 상속될 뿐입니다. 일반 속성이 받아들이는 모든 것을 저장할 수 있으며, 값의 조각 (색상 채널, 단위 없는 숫자)도 저장하고 나중에 조합할 수 있습니다.

Juno변수 선언 및 사용 사용자 정의 속성은 이름이 있는 값입니다. --color-brand처럼 두 개의 대시로 작성한 다음 var(--color-brand)로 읽습니다. :root에 놓으면 전체 페이지에서 사용할 수 있습니다. 한 줄을 변경하면 그 이름을 읽는 모든 규칙이 함께 업데이트됩니다.
Juno변수 선언 및 사용-- 접두사는 사용자 정의 속성을 표시하고, var()는 그것을 다시 읽습니다. :root에 선언하면 글로벌 토큰이 트리의 최상단에 위치하며 모든 요소가 도달할 수 있습니다. 이름은 대소문자를 구분하며, 속성이 받아들이는 모든 값이 하나에 저장될 수 있으며, 색상만 가능한 것이 아닙니다.
Juno변수 선언 및 사용 사용자 정의 속성은 설정한 모든 --name이며, var()를 통해 다시 읽고, 해석될 때까지 거의 그대로 저장됩니다. :root는 관례입니다. <html> 요소이며, 이러한 속성이 상속되기 때문에 거기서의 값이 모든 후손에게 도달합니다. 부분 값도 저장할 수 있으며 나중에 조합할 수 있으며, 이는 폴백 섹션이 사용하기 시작합니다.

계단식과 상속

다른 언어에서 변수를 사용했다면, 다음 부분이 놀라운 점입니다. 사용자 정의 속성은 고정되지 않습니다. 다른 CSS 속성과 같은 규칙을 따릅니다. 즉, 자식 요소로 **상속**되며, 나머지를 건드리지 않고 한 곳에서 다른 값을 줄 수 있습니다.

css
:root {
  --color-text: #1f2937;   /* 전체 페이지의 기본값 */
}

.callout {
  --color-text: #92400e;   /* .callout 내부에서만 다른 값 */
  color: var(--color-text);
}

p {
  color: var(--color-text);   /* 대부분 진회색, 콜아웃 내부에서는 황갈색 */
}

같은 var(--color-text)가 대부분의 단락에서는 진회색을 제공하지만 콜아웃 내부에서는 황갈색을 제공합니다. 왜냐하면 콜아웃이 자체 값을 설정했기 때문입니다. 이것이 테마 지정을 가능하게 하며, CSS의 작동 방식에서 그 뒤의 계단식을 보았습니다.

여기서 사용자 정의 속성은 Sass와 같은 CSS 전처리기의 변수와 다릅니다. 전처리기 변수는 브라우저가 보기 전에 컴파일되어 제거되므로, 정적 찾기 및 바꾸기입니다. 사용자 정의 속성은 브라우저에서 활성화된 실제 CSS 속성이므로 계단식을 따르고 **상속**됩니다.

사용자 정의 속성의 값은 선언된 위치가 아니라 읽는 요소에서 조회됩니다. 이 단 하나의 사실이 컴포넌트 테마 지정이 작동하는 이유입니다:

css
:root {
  --color-accent: #2563eb;   /* 기본적으로 모든 곳에 파란색 */
}

.card--warning {
  --color-accent: #d97706;   /* 이 컴포넌트 서브트리용 재정의 */
}

.card__badge {
  background: var(--color-accent);   /* 파란색, 또는 경고 카드 내부에서는 황색 */
}

배지는 --color-accent가 어디서 왔는지 알거나 신경 쓰지 않습니다. 자신에게 효과가 있는 값을 읽으며, 모든 상위 요소가 그것을 재정의할 수 있습니다. 컨테이너에 토큰을 재정의하면 단일 컴포넌트 규칙을 편집하지 않고도 모든 후손이 다시 스킨됩니다. 전체 계단식 및 상속 규칙은 CSS의 작동 방식에 있습니다.

사용자 정의 속성을 모든 전처리기 변수와 분리하는 동작은 계단식에 참여하고 기본적으로 **상속**된다는 것입니다. Sass 또는 Less 변수는 빌드 시간에 해석되며 브라우저가 파일을 로드하기 전에 사라집니다. 사용자 정의 속성은 요소의 실제 속성이며, color 또는 font-size를 관리하는 동일한 계단식으로 해석됩니다. 이는 var()읽기를 수행하는 요소의 계산된 속성 값에 대해 조회된다는 의미입니다.

css
:root {
  --color-accent: #2563eb;
}

.panel[data-tone="warning"] {
  --color-accent: #d97706;   /* 서브트리 루트에 설정 */
}

.panel__button {
  background: var(--color-accent);   /* 요소별로 해석, 패널에서 상속 */
}

두 가지 중요한 결과입니다. 첫째, 상속은 테마 지정을 수행합니다. 서브트리 루트에 --color-accent를 설정하면 그것을 읽는 모든 후손에게 전파되므로, 컨테이너 재정의는 규칙을 변경하지 않고도 전체 컴포넌트 트리를 다시 스킨합니다. 둘째, 구체성은 여전히 토큰이 설정되는 위치에 적용되며, var() 자체에는 적용되지 않습니다. 같은 요소에 --color-accent를 설정하는 두 선택자가 있다면, 일반적인 계단식이 어느 것이 이기는지 결정하며, 이는 다른 속성과 정확히 동일합니다. 상속을 중지해야 하는 속성은 initial로 재설정되거나, @property로 상속하지 않도록 선언될 수 있으며, 아래의 심화 설명에서 이를 다룹니다. 계단식 규칙은 예외 없이 여기에 적용됩니다.

Juno계단식과 상속 사용자 정의 속성은 고정되지 않습니다. 자식 요소로 상속되며, 나머지를 변경하지 않고 한 곳에서 다른 값을 설정할 수 있습니다. 그래서 같은 var(--color-text)가 대부분의 단락에서는 회색이고 콜아웃 내부에서는 황갈색일 수 있습니다. 컨테이너의 이름을 재정의하면 그 안의 모든 것이 새 값을 선택합니다.
Juno계단식과 상속 빌드 시간에 사라지는 Sass 변수와 달리, 사용자 정의 속성은 브라우저에서 활성화되고 계단식을 따르므로, 그 값은 그것을 사용하는 요소에서 읽힙니다. 컨테이너에 토큰을 재정의하면 모든 후손이 다시 스킨되며, 컴포넌트 규칙을 편집하지 않습니다. 그 조회-읽기-위치 동작이 테마 지정의 전체 기법입니다.
Juno계단식과 상속 사용자 정의 속성은 상속되고 계단식되며, 전처리기 변수는 브라우저가 로드하기 전에 사라지므로 절대 그렇지 않습니다. var()는 읽는 요소의 계산된 값에 대해 해석되므로, 서브트리 루트에 토큰을 설정하면 상속만으로 그 아래의 모든 것을 테마로 지정합니다. 구체성은 토큰이 설정되는 위치를 관리하며, var() 호출 자체는 아닙니다.

폴백과 calc()

var()는 두 번째 값을 가질 수 있으며, 사용자 정의 속성이 절대 설정되지 않은 경우에만 사용됩니다. 이것이 **폴백**이며, 토큰이 누락되어도 규칙이 작동하도록 유지합니다:

css
.card {
  gap: var(--gap, 1rem);   /* --gap이 존재하면 사용, 아니면 1rem */
}

calc()를 사용하여 사용자 정의 속성으로 수학을 할 수도 있습니다. 기본 간격 값이 있으면, 두 번째 숫자를 하드코딩하는 대신 그것으로부터 더 큰 값을 만들 수 있습니다:

css
:root {
  --space-md: 16px;
}

.section {
  padding: calc(var(--space-md) * 2);   /* 32px, 기본에서 파생 */
}

이런 식으로 값을 파생시키는 것은 --space-md에 대한 하나의 변경이 그 위에 구축된 모든 것을 통해 전파된다는 의미입니다.

var()는 **폴백**으로 작동하는 두 번째 인자를 받습니다. var(--gap, 1rem)은 설정된 경우 --gap으로 해석되고, 설정되지 않은 경우 1rem으로 해석됩니다. 그 폴백은 주어진 서브트리에서 정의되지 않을 수 있는 모든 토큰에서 사용할 가치가 있으므로, 토큰이 제공되지 않은 곳에 컴포넌트를 드롭해도 정상적으로 렌더링됩니다.

css
.card {
  gap: var(--gap, 1rem);
  padding: var(--space-md, 16px);
}

사용자 정의 속성은 calc()와 함께 할 때 그 진가를 발휘합니다. 왜냐하면 그것이 매직 숫자 더미를 저장하는 대신 한 값에서 다른 값을 파생시키는 방법이기 때문입니다:

css
:root {
  --space-md: 16px;
}

.section {
  padding: calc(var(--space-md) * 2);        /* 32px */
  margin-bottom: calc(var(--space-md) * 3);  /* 48px, 하나의 소스 오브 트루스 */
}

주의할 점은 폴백이 쉼표를 포함한 전체 값이 될 수 있다는 것입니다. 따라서 var(--shadow, 0 1px 2px black)은 유효하며, 첫 번째 쉼표 이후의 모든 것이 폴백으로 처리됩니다. 크기 단위를 참조하여 calc() 내의 단위가 어떻게 해석되는지 확인하세요.

var()는 두 번째 인자로 선택적 폴백을 가집니다. var(--gap, 1rem)은 읽는 요소에서 --gap이 설정되지 않았거나 보장된 무효 값으로 설정된 경우 1rem을 산출합니다. **폴백**은 첫 번째 쉼표 이후 모든 것을 탐욕스럽게 가져가므로, 그 자체가 쉼표를 포함하는 값이거나 중첩된 var()이며, 이를 통해 기본값을 연결할 수 있습니다: var(--gap, var(--space-md, 1rem)).

css
.card {
  gap: var(--gap, var(--space-md, 1rem));   /* 연결된 폴백 */
}

:root {
  --space-md: 16px;
}

.section {
  padding: calc(var(--space-md) * 2);   /* 32px, 하드코딩되지 않고 파생 */
}

calc()와의 짝짓기는 사용자 정의 속성이 스케일링 시스템에서 그 자리를 얻는 곳이며, 정밀도 규칙이 함께 따릅니다. 사용자 정의 속성은 단위 없는 숫자를 보유할 수 있으며, calc() 내에서 단위를 곱합니다:

css
:root {
  --scale: 1.5;   /* 단위 없음, 모든 단위로 곱할 수 있음 */
}

.title {
  font-size: calc(1rem * var(--scale));   /* 1.5rem */
  line-height: calc(var(--scale) + 0.2);  /* 순수 숫자 연산 */
}

--scale을 단위 없이 저장하는 이유는 calc(var(--scale) * 16px)이 작동하는 반면, --scale: 1.5rem을 저장하고 calc(var(--scale) * 16px)을 시도하면 두 길이를 곱하게 되어 무효하기 때문입니다. 원시 비율을 단위 없이 유지하고 사용 시점에 단위를 부착합니다. 그 단위들이 픽셀로 어떻게 해석되는지는 크기 단위에서 다룹니다.

Juno폴백과 calc()var(--gap, 1rem)은 토큰이 존재하면 사용하고 존재하지 않으면 1rem으로 폴백합니다. 따라서 값이 누락되어도 규칙이 계속 작동합니다. 그리고 calc()는 토큰에서 값을 만들 수 있게 합니다. calc(var(--space-md) * 2)처럼 기본 간격의 두 배를 만들 수 있습니다. 기본을 변경하면 그 위에 구축된 모든 것이 따라갑니다.
Juno폴백과 calc()var()의 두 번째 인자는 폴백입니다: var(--gap, 1rem)은 토큰이 절대 설정되지 않은 경우를 처리합니다. 사용자 정의 속성을 calc()와 짝지어 매직 숫자를 하드코딩하지 않고 값을 파생시키세요. 따라서 하나의 토큰이 전체 스케일을 주도합니다. 폴백은 첫 번째 쉼표 이후 모든 것을 가져가는 것을 유의하세요. 이는 여러 부분의 값에 편리합니다.
Juno폴백과 calc()var(--gap, 1rem)는 탐욕스럽게 폴백하므로 var(--gap, var(--space-md, 1rem))처럼 기본값을 중첩할 수 있습니다. 원시 비율을 단위 없이 저장하세요. calc(var(--scale) * 16px)는 작동하지만 두 길이를 곱하면 작동하지 않기 때문입니다. 사용 시점에 단위를 부착하세요. 이는 하나의 토큰이 전체 타입 또는 간격 스케일을 주도하게 합니다.

테마 지정

사용자 정의 속성이 트리 아래로 재정의될 수 있으므로, 클래스 아래에서 토큰을 변경하여 다크 테마를 만들 수 있으며, 모든 컴포넌트 규칙을 그대로 둡니다:

css
:root {
  --color-bg: #ffffff;
  --color-text: #1f2937;
}

.theme-dark {
  --color-bg: #0f172a;      /* 같은 이름, 새로운 값 */
  --color-text: #e2e8f0;
}

body {
  background: var(--color-bg);
  color: var(--color-text);
}

페이지에 class="theme-dark"를 추가하면 --color-bg 또는 --color-text를 읽는 모든 규칙이 다크 값으로 전환됩니다. 테마를 전환하기 위해 body 규칙이나 컴포넌트를 편집하지 않았습니다. 이것이 16진수 코드를 직접 작성하는 대신 색상을 토큰으로 이름 짓는 보상입니다.

테마 지정은 계단식하는 토큰의 직접적인 보상입니다. 한 번 색상을 토큰으로 정의하고, 토큰 이름에 대해 모든 컴포넌트를 작성한 다음, 테마는 클래스 또는 미디어 쿼리 아래에서 같은 이름에 대한 두 번째 값 집합일 뿐입니다:

css
:root {
  --color-bg: #ffffff;
  --color-text: #1f2937;
  --color-surface: #f8fafc;
}

.theme-dark {
  --color-bg: #0f172a;
  --color-text: #e2e8f0;
  --color-surface: #1e293b;
}

컨테이너에서 theme-dark를 전환하면 그 안의 모든 것이 다시 스킨됩니다. 왜냐하면 컴포넌트가 토큰 이름을 읽지, 고정된 색상을 읽지 않기 때문입니다. 같은 방식이 미디어 쿼리에서 작동하므로 테마가 운영 체제 설정을 따릅니다:

css
@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0f172a;
    --color-text: #e2e8f0;
    --color-surface: #1e293b;
  }
}

이를 성립시키는 규율은 컴포넌트 규칙에서 원시 16진수 코드를 완전히 제외하고, 대신 색상 토큰으로 이름 짓는 것입니다. 이는 또한 토큰 이름 지정 관례가 보상하는 곳이며, CSS 구성이 이를 다룹니다.

테마 지정은 단일 값 재사용 이상으로 사용자 정의 속성이 중요한 이유이며, 테마가 토큰 계층의 계단식 재정의 이상이 아니기 때문에 작동합니다. 컴포넌트는 토큰 이름을 읽습니다. 테마가 트리의 더 높은 곳에서 이름을 재정의합니다. 컴포넌트 계층은 테마가 존재한다는 것을 알지 못합니다.

css
:root {
  --color-bg: #ffffff;
  --color-text: #1f2937;
  --color-surface: #f8fafc;
}

.theme-dark {
  --color-bg: #0f172a;
  --color-text: #e2e8f0;
  --color-surface: #1e293b;
}

@media (prefers-color-scheme: dark) {
  :root:not(.theme-light) {
    --color-bg: #0f172a;
    --color-text: #e2e8f0;
    --color-surface: #1e293b;
  }
}

프로덕션에서 성립하는 패턴은 2층 토큰 시스템입니다. 첫 번째 계층은 원시 값(--blue-600: #2563eb)을 보유하고, 두 번째 계층은 그것을 참조하는 의미론적 토큰(--color-accent: var(--blue-600))을 보유합니다. 컴포넌트는 의미론적 계층만 읽으므로, 테마는 의미론적 토큰을 재할당하고 팔레트 계층은 그대로 유지되며, 스위치가 작고 의도가 읽을 수 있도록 합니다. 클래스 재정의를 prefers-color-scheme 미디어 쿼리와 짝짓는 것은 컴포넌트 규칙을 복제하지 않고도 시스템 기본값과 수동 토글을 제공합니다. 이를 유지 가능하게 하는 명명 규율은 나머지 파일 및 토큰 구성과 함께하며, 원시 값 자체는 색상입니다.

고급 사용자 정의 속성 보기에 속하는 세 가지가 더 있으며, 이는 광택을 추가하는 것보다 가능한 것을 변경합니다.

사용자 정의 속성은 **계산된 값 시간**에 해석됩니다. 즉, 브라우저가 파일을 구문 분석할 때가 아니라 스타일을 계산할 때 요소별로 var()를 해석합니다. 이는 정확히 테마 지정이 작동하는 이유이며 전처리기 변수가 할 수 없는 이유입니다. 값이 활성화되므로 계단식, 상속, 그리고 런타임에 만들어진 변경에 응답합니다.

런타임이 두 번째 포인트입니다. 이는 실제 속성이므로, JavaScript는 이를 읽고 쓸 수 있으며, 토큰을 읽는 모든 규칙이 즉시 업데이트됩니다:

css
:root {
  --sidebar-width: 280px;   /* JS 런타임에서 읽고 설정 */
}

스크립트는 document.documentElement.style.setProperty('--sidebar-width', '320px')를 호출할 수 있으며 레이아웃이 새 값에 대해 다시 흐릅니다. 페이지가 로드되기 훨씬 전에 컴파일되어 제거되는 Sass 변수는 절대 제공할 수 없습니다. 이 변경을 루트가 아닌 모든 요소에 속성을 설정하여 서브트리로 범위를 지정할 수도 있습니다. 이는 트리의 해당 분기에 변경을 제한합니다.

세 번째 포인트는 속성 자체에 대한 제어입니다. @property는 사용자 정의 속성을 타입, 초기 값, 그리고 상속 여부로 등록할 수 있게 하며, 이는 사용자 정의 속성의 부드러운 애니메이션을 해제합니다 (일반 사용자 정의 속성은 브라우저가 타입이 없는 값을 보간할 수 없기 때문에 켜기/끄기 점프로 애니메이션됩니다). 그 옆에, 현대 값 함수는 토큰에서 자연스럽게 읽습니다. clamp(min, preferred, max)는 선호하는 값을 선택하지만 경계를 벗어나지 않으며, min()은 가장 작은 인자를 반환하고 max()는 가장 큰 인자를 반환하므로, width: min(100%, var(--max-width))는 미디어 쿼리 없이 토큰에서 유동적인 너비를 제한합니다.

Juno테마 지정 테마는 .theme-dark와 같은 클래스 아래에서 다른 값을 가진 같은 토큰 이름입니다. class="theme-dark"를 설정하면 그 토큰을 읽는 모든 규칙이 전환되며, 단일 컴포넌트를 편집하지 않습니다. 이것이 16진수 코드를 각 규칙에 직접 입력하는 대신 색상을 토큰으로 이름 짓는 이유입니다.
Juno테마 지정 토큰이 계단식되기 때문에 테마는 클래스 또는 prefers-color-scheme 미디어 쿼리 아래의 같은 이름에 대한 두 번째 값 집합일 뿐입니다. 원시 16진수를 컴포넌트 규칙에서 제외하고 토큰 이름을 읽으세요. 그러면 전체 서브트리가 한 번의 재정의에서 다시 스킨됩니다. 클래스를 전환하거나 OS 설정과 일치하면 그 토큰을 읽는 모든 것이 따라갑니다.
Juno테마 지정 사용자 정의 속성은 계산된 값 시간에 해석되므로 활성화됩니다. JavaScript는 런타임에 --sidebar-width를 설정할 수 있으며 그것을 읽는 모든 규칙이 다시 흐릅니다. 전처리기 변수는 할 수 없습니다. 2층 설정, 의미론적 토큰 아래 원시 팔레트 토큰, 테마 스위치를 몇 가지 재할당으로 유지합니다. @propertyclamp(), min(), 그리고 max()는 그 토큰을 미디어 쿼리 없이 애니메이션하고 경계 내로 유지하게 합니다.