Nordhealth가 웹 구성요소에서 커스텀 속성을 사용하는 방법

디자인 시스템 및 구성요소 라이브러리에서 맞춤 속성을 사용하는 이점

David Darnes
David Darnes

저는 Nordhealth의 선임 프런트엔드 개발자인 데이브입니다. 저는 디자인 시스템 Nord의 디자인과 개발을 담당하고 있으며, 여기에는 구성요소 라이브러리의 웹 구성요소 빌드가 포함됩니다. CSS 맞춤 속성을 사용하여 Web Components 스타일 지정과 관련된 문제를 해결한 방법과 디자인 시스템 및 구성요소 라이브러리에서 맞춤 속성을 사용할 때의 기타 이점을 공유하고 싶습니다.

웹 구성요소를 빌드하는 방법

웹 구성요소를 빌드하기 위해 상태, 범위가 지정된 스타일, 템플릿 등 많은 상용구 코드를 제공하는 라이브러리인 Lit를 사용합니다. Lit는 경량일 뿐만 아니라 네이티브 JavaScript API를 기반으로 빌드되므로 브라우저에 이미 있는 기능을 활용하는 간결한 코드 번들을 제공할 수 있습니다.


import {html, css, LitElement} from 'lit';

export class SimpleGreeting extends LitElement {
  static styles = css`:host { color: blue; font-family: sans-serif; }`;

  static properties = {
    name: {type: String},
  };

  constructor() {
    super();
    this.name = 'there';
  }

  render() {
    return html`

Hey ${this.name}, welcome to Web Components!

`
; } } customElements.define('simple-greeting', SimpleGreeting);
Lit로 작성된 웹 구성요소입니다.

하지만 웹 구성요소의 가장 매력적인 점은 기존 JavaScript 프레임워크와 거의 모두 호환된다는 점입니다. 프레임워크가 없어도 작동합니다. 기본 JavaScript 패키지가 페이지에서 참조되면 웹 구성요소를 사용하는 것은 네이티브 HTML 요소를 사용하는 것과 매우 유사합니다. 기본 HTML 요소가 아님을 나타내는 유일한 실제 징후는 태그 내에 일관되게 사용되는 하이픈입니다. 이는 브라우저에 웹 구성요소임을 알리는 표준입니다.

섀도우 DOM 스타일 캡슐화

네이티브 HTML 요소에 Shadow DOM이 있는 것과 마찬가지로 Web Components에도 있습니다. 섀도 DOM은 요소 내의 숨겨진 노드 트리입니다. 이를 시각화하는 가장 좋은 방법은 웹 인스펙터를 열고 '섀도우 DOM 트리 표시' 옵션을 사용 설정하는 것입니다. 이렇게 한 후 인스펙터에서 기본 입력 요소를 살펴보세요. 이제 해당 입력을 열고 그 안에 있는 모든 요소를 볼 수 있습니다. Google의 웹 구성요소 중 하나를 사용해 볼 수도 있습니다. 맞춤 입력 구성요소를 검사하여 Shadow DOM을 확인해 보세요.

DevTools에서 검사된 섀도우 DOM
일반 텍스트 입력 요소와 Nord 입력 웹 구성요소의 Shadow DOM 예시

Shadow DOM의 장점 (또는 관점에 따라 단점) 중 하나는 스타일 캡슐화입니다. 웹 구성요소 내에서 CSS를 작성하면 스타일이 누출되어 기본 페이지나 다른 요소에 영향을 미칠 수 없습니다. 스타일은 구성요소 내에 완전히 포함됩니다. 또한 기본 페이지 또는 상위 웹 구성요소용으로 작성된 CSS는 웹 구성요소로 유출될 수 없습니다.

스타일의 이러한 캡슐화는 구성요소 라이브러리에서 유용합니다. 이를 통해 사용자가 구성요소를 사용할 때 상위 페이지에 적용된 스타일과 관계없이 의도한 대로 표시될 수 있습니다. 또한 확실하게 하기 위해 모든 웹 구성요소의 루트 또는 '호스트'에 all: unset;를 추가합니다.


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
일부 구성요소 상용구 코드가 섀도우 루트 또는 호스트 선택기에 적용됩니다.

하지만 웹 구성요소를 사용하는 사람이 특정 스타일을 변경해야 하는 정당한 이유가 있다면 어떻게 해야 할까요? 맥락상 대비가 더 필요한 텍스트 줄이 있거나 테두리가 더 두꺼워야 할 수도 있습니다. 스타일이 구성요소에 적용되지 않는 경우 스타일 옵션을 잠금 해제하려면 어떻게 해야 할까요?

이때 CSS 맞춤 속성이 필요합니다.

CSS 맞춤 속성

맞춤 속성은 이름이 매우 적절합니다. 완전히 직접 이름을 지정하고 필요한 값을 적용할 수 있는 CSS 속성이기 때문입니다. 두 개의 하이픈을 앞에 붙이기만 하면 됩니다. 커스텀 속성을 선언하면 var() 함수를 사용하여 CSS에서 값을 사용할 수 있습니다.


:root {
  --n-color-accent: rgb(53, 89, 199);
  /* ... */
}

.n-color-accent-text {
  color: var(--n-color-accent);
}
CSS 프레임워크에서 디자인 토큰이 맞춤 속성으로 사용되고 도우미 클래스에서 사용되는 예

상속의 경우 모든 맞춤 속성이 상속되며 이는 일반 CSS 속성 및 값의 일반적인 동작을 따릅니다. 상위 요소 또는 요소 자체에 적용된 맞춤 속성은 다른 속성의 값으로 사용할 수 있습니다. Google은 CSS 프레임워크를 통해 루트 요소에 적용하여 디자인 토큰에 맞춤 속성을 많이 사용합니다. 즉, 웹 구성요소, CSS 도우미 클래스 또는 토큰 목록에서 값을 가져오려는 개발자 등 페이지의 모든 요소가 이러한 토큰 값을 사용할 수 있습니다.

var() 함수를 사용하여 맞춤 속성을 상속하는 기능은 Web Components의 Shadow DOM을 관통하고 개발자가 구성요소를 스타일링할 때 더 세부적으로 제어할 수 있도록 하는 방법입니다.

Nord 웹 구성요소의 맞춤 속성

Google은 디자인 시스템의 구성요소를 개발할 때마다 CSS에 신중하게 접근합니다. 간결하면서도 유지관리성이 뛰어난 코드를 지향합니다. Google이 보유한 디자인 토큰은 루트 요소의 기본 CSS 프레임워크 내에서 맞춤 속성으로 정의됩니다.


:root {
  --n-space-m: 16px;
  --n-space-l: 24px;
  /* ... */
  --n-color-background: rgb(255, 255, 255);
  --n-color-border: rgb(216, 222, 228);
  /* ... */
}
루트 선택기에 정의된 CSS 맞춤 속성

그런 다음 이러한 토큰 값이 구성요소 내에서 참조됩니다. 일부 경우에는 CSS 속성에 값을 직접 적용하지만, 다른 경우에는 실제로 새로운 컨텍스트 맞춤 속성을 정의하고 그 속성에 값을 적용합니다.


:host {
  --n-tab-group-padding: 0;
  --n-tab-list-background: var(--n-color-background);
  --n-tab-list-border: inset 0 -1px 0 0 var(--n-color-border);
  /* ... */
}

.n-tab-group-list {
  box-shadow: var(--n-tab-list-border);
  background-color: var(--n-tab-list-background);
  gap: var(--n-space-s);
  /* ... */
}
구성요소의 섀도우 루트에 정의된 후 구성요소 스타일에서 사용되는 맞춤 속성 디자인 토큰 목록의 맞춤 속성도 사용되고 있습니다.

또한 구성요소에만 해당하지만 토큰에는 없는 일부 값을 추상화하여 컨텍스트 기반 맞춤 속성으로 변환합니다. 구성요소와 관련된 맞춤 속성은 두 가지 주요 이점을 제공합니다. 첫째, 이 값을 구성요소 내의 여러 속성에 적용할 수 있으므로 CSS를 더 'dry'하게 사용할 수 있습니다.


.n-tab-group-list::before {
  /* ... */
  padding-inline-start: var(--n-tab-group-padding);
}

.n-tab-group-list::after {
  /* ... */
  padding-inline-end: var(--n-tab-group-padding);
}
탭 그룹 패딩 컨텍스트 맞춤 속성이 구성요소 코드 내 여러 위치에서 사용되고 있습니다.

두 번째로, 구성요소 상태와 변형 변경이 매우 깔끔해집니다. 예를 들어 호버 또는 활성 상태나 이 경우 변형을 스타일링할 때 이러한 속성을 모두 업데이트하려면 맞춤 속성만 변경하면 됩니다.


:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
여러 업데이트가 아닌 단일 맞춤 속성 업데이트를 사용하여 패딩이 변경되는 탭 구성요소의 변형입니다.

하지만 가장 강력한 이점은 이러한 컨텍스트 기반 맞춤 속성을 구성요소에 정의할 때 각 구성요소에 일종의 맞춤 CSS API를 만들어 해당 구성요소의 사용자가 활용할 수 있다는 것입니다.


<nord-tab-group label="T>itl<e"
  >!<-- ... --
/nord>-t<ab-gr>oup

style
  nord-tab-group {
    --n-tab-group-padding: var(--n-space<-xl);
>  }
/style
페이지에서 탭 그룹 구성요소를 사용하고 패딩 맞춤 속성을 더 큰 크기로 업데이트합니다.

위의 예에서는 선택기를 통해 변경된 컨텍스트 기반 맞춤 속성이 있는 웹 구성요소를 보여줍니다. 이 전체 접근 방식의 결과는 사용자가 스타일을 유연하게 지정할 수 있으면서도 대부분의 실제 스타일을 관리할 수 있는 구성요소입니다. 또한 구성요소 개발자는 사용자가 적용한 스타일을 가로챌 수 있습니다. 이러한 속성 중 하나를 조정하거나 확장하려는 경우 사용자가 코드를 변경하지 않아도 됩니다.

이 접근 방식은 디자인 시스템 구성요소의 크리에이터인 Google뿐만 아니라 개발팀이 제품에서 이러한 구성요소를 사용할 때도 매우 강력합니다.

맞춤 속성 확장

작성 시점에는 Google 문서에 이러한 컨텍스트 맞춤 속성이 실제로 표시되지 않지만, 더 많은 개발팀이 이러한 속성을 이해하고 활용할 수 있도록 할 계획입니다. 구성요소는 npm에 매니페스트 파일과 함께 패키징됩니다. 매니페스트 파일에는 구성요소에 관한 모든 정보가 포함되어 있습니다. 그런 다음 문서 사이트가 배포될 때 매니페스트 파일을 데이터로 사용합니다. 이는 Eleventy전역 데이터 기능을 사용하여 실행됩니다. 이 매니페스트 데이터 파일에 이러한 컨텍스트 맞춤 속성을 포함할 계획입니다.

개선하고자 하는 또 다른 영역은 이러한 컨텍스트 맞춤 속성이 값을 상속하는 방식입니다. 현재 예를 들어 두 개의 구분선 구성요소의 색상을 조정하려면 선택기로 두 구성요소를 모두 구체적으로 타겟팅하거나 스타일 속성이 있는 요소에 맞춤 속성을 직접 적용해야 합니다. 이 방법도 괜찮아 보이지만 개발자가 포함 요소나 루트 수준에서 이러한 스타일을 정의할 수 있다면 더 유용할 것입니다.


<nord-divider></nord-divider>

<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>

<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }

  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
  }
  
  section nord-divider {
    --n-divider-color: var(--n-color-status-success);
  }
</style>
두 가지 색상 처리가 필요한 구분선 구성요소의 두 인스턴스 하나는 더 구체적인 선택기에 사용할 수 있는 섹션 내에 중첩되어 있지만 구분선을 구체적으로 타겟팅해야 합니다.

구성요소에 직접 맞춤 속성 값을 설정해야 하는 이유는 구성요소 호스트 선택기를 통해 동일한 요소에 정의하기 때문입니다. 구성요소에서 직접 사용하는 전역 디자인 토큰은 이 문제의 영향을 받지 않고 바로 전달되며 상위 요소에서 가로챌 수도 있습니다. 두 가지의 장점을 모두 누릴 수 있는 방법이 있을까요?

비공개 및 공개 맞춤 속성

비공개 맞춤 속성은 Lea Verou가 만든 것으로, 컴포넌트 자체의 컨텍스트가 지정된 '비공개' 맞춤 속성이지만 대체가 있는 '공개' 맞춤 속성으로 설정됩니다.



:host {
  --_n-divider-color: var(--n-divider-color, var(--n-color-border));
  --_n-divider-size: var(--n-divider-size, 1px);
}

.n-divider {
  border-block-start: solid var(--_n-divider-size) var(--_n-divider-color);
  /* ... */
}
내부 CSS가 대체가 있는 공개 맞춤 속성으로 설정된 비공개 맞춤 속성에 의존하도록 컨텍스트 맞춤 속성이 조정된 구분선 웹 구성요소 CSS

이런 방식으로 컨텍스트 맞춤 속성을 정의하면 전역 토큰 값을 상속하고 구성요소 코드 전체에서 값을 재사용하는 등 이전과 동일한 작업을 모두 수행할 수 있습니다. 하지만 구성요소는 자체 또는 상위 요소에서 해당 속성의 새로운 정의도 정상적으로 상속합니다.


<nord-divider></nord-divider>

<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>

<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }

  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
    --n-divider-color: var(--n-color-status-success);
  }
</style>
두 개의 구분선이 다시 표시되지만 이번에는 섹션 선택기에 구분선의 컨텍스트 맞춤 속성을 추가하여 구분선의 색상을 변경할 수 있습니다. 구분선이 이를 상속하여 더 깔끔하고 유연한 코드를 생성합니다.

이 방법이 진정한 '비공개'는 아니라고 주장할 수도 있지만, Google에서는 이 방법이 우려했던 문제를 해결하는 상당히 우아한 방법이라고 생각합니다. 기회가 있을 때 구성요소에서 이 문제를 해결하여 개발팀이 구성요소 사용을 더 효과적으로 제어하면서도 마련된 가이드라인의 이점을 누릴 수 있도록 할 예정입니다.

CSS 맞춤 속성과 함께 Web Components를 사용하는 방법에 관한 이 통계가 유용했기를 바랍니다. 어떤 방법이 가장 마음에 드는지 알려주세요. 이러한 방법을 업무에 사용하기로 결정했다면 Twitter @DavidDarnes에서 저를 찾으실 수 있습니다. Twitter에서 Nordhealth(@NordhealthHQ)를 팔로우할 수도 있습니다. 이 디자인 시스템을 통합하고 이 도움말에 언급된 기능을 실행하기 위해 열심히 노력한 나머지 팀원(@Viljamis, @WickyNilliams, @eric_habich)도 팔로우하세요.

Dan Cristian Pădureț의 히어로 이미지