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 요소가 아님을 알 수 있는 유일한 신호는 태그 내에 있는 일관된 하이픈으로, 브라우저에 이 요소가 웹 구성 요소임을 나타내는 표준입니다.


// TODO: DevSite - Code sample removed as it used inline event handlers
페이지에서 위에서 만든 웹 구성요소 사용

Shadow DOM 스타일 캡슐화

기본 HTML 요소에 Shadow DOM이 있는 것과 동일한 방식으로 웹 구성요소도 마찬가지입니다. Shadow DOM은 요소 내에 있는 노드의 숨겨진 트리입니다. 이를 시각화하는 가장 좋은 방법은 웹 검사기를 열고 'Show Shadow DOM 트리' 옵션을 설정하는 것입니다. 그런 다음 검사기에서 네이티브 입력 요소를 살펴봅니다. 이제 해당 입력을 열고 그 안에 있는 모든 요소를 볼 수 있습니다. 웹 구성요소 중 하나를 사용하여 이 작업을 시도해 볼 수도 있습니다. 맞춤 입력 구성요소를 검사하여 Shadow DOM을 확인해 보세요.

DevTools에서 검사된 Shadow 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() 함수를 사용하여 맞춤 속성을 상속하는 이 기능은 웹 구성요소의 Shadow DOM을 관통하고 개발자가 구성요소의 스타일을 지정할 때 더 세밀하게 제어할 수 있는 방법입니다.

Nord Web 구성요소의 맞춤 속성

디자인 시스템을 위한 구성요소를 개발할 때마다 Google은 CSS에 대해 신중하게 접근합니다. 즉, 가볍지만 유지 관리가 용이한 코드를 목표로 합니다. 우리가 지정한 디자인 토큰은 기본 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를 더 '건조하게' 사용할 수 있습니다.


.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가 생성되며 이 API는 해당 컴포넌트의 사용자가 활용할 수 있다는 점입니다.


<nord-tab-group label="Title">
  <!-- ... -->
</nord-tab-group>

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

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

이러한 접근 방식은 디자인 시스템 구성요소의 제작자뿐만 아니라 개발팀도 제품에서 이러한 구성요소를 사용할 때 매우 큰 힘을 발휘합니다.

맞춤 속성의 추가 기능

이 글을 작성하는 시점에서는 문서에 이러한 문맥 기반 맞춤 속성이 실제로 표시되지 않습니다. 하지만 더 많은 개발팀에서 이러한 속성을 이해하고 활용할 수 있도록 공개할 계획입니다. Google의 구성요소는 매니페스트 파일과 함께 npm으로 패키징되며, 여기에 구성요소에 관해 알아야 할 모든 내용이 포함되어 있습니다. 그런 다음 문서 사이트가 배포될 때 매니페스트 파일을 데이터로 사용합니다. 이때 EleventyGlobal Data 기능을 사용합니다. 이 매니페스트 데이터 파일에 상황별 맞춤 속성을 포함할 계획입니다.

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


<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가 조합한 속성입니다. 비공개 커스텀 속성은 구성요소 자체에서 상황별 '비공개' 커스텀 속성이지만 대체가 있는 'public' 커스텀 속성으로 설정됩니다.



: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>
다시 2개의 구분선. 단, 이번에는 섹션 선택기에 구분선의 상황별 맞춤 속성을 추가하여 구분선 색상을 다시 지정할 수 있습니다. 구분선이 이를 상속하여 더 명확하고 유연한 코드를 생성합니다.

이 방법이 실제로는 '비공개'가 아니라고 주장할 수도 있지만, 우려했던 문제에 대한 매우 우아한 해결 방법이라고 생각합니다. 기회가 되면 개발팀이 구성요소 사용을 더 세밀하게 제어하면서도 기존에 마련한 안전장치의 이점을 계속 누릴 수 있도록 구성요소에서 이 문제를 해결할 예정입니다.

웹 구성요소를 CSS 맞춤 속성과 함께 사용하는 방법에 대한 정보가 도움이 되었기를 바랍니다. 의견이 있으시면 알려주세요. 작업에 이러한 방법을 사용하고 싶으시다면 Twitter @DavidDarnes에서 저를 찾아주시기 바랍니다. Twitter에서도 Nordhealth @NordhealthHQ를 확인하실 수 있으며, 저희 팀원들은 이 디자인 시스템을 한데 모으고 @Viljamis, @WickyNilliams, @eric_habich 도움말에서 언급된 기능을 실행하기 위해 열심히 노력해 왔습니다.

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