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

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

David Darnes
David Darnes

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

웹 구성요소 빌드 방법

웹 구성요소를 빌드하기 위해 상태, 범위가 지정된 스타일, 템플릿 등 다양한 상용구 코드를 제공하는 라이브러리인 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 프레임워크와 호환되거나 프레임워크와 전혀 호환된다는 점입니다. 기본 자바스크립트 패키지가 페이지에서 참조되면, 웹 구성 요소를 사용하는 것은 기본 HTML 요소를 사용하는 것과 매우 유사합니다. 네이티브 HTML 요소가 아니라는 것을 실제로 알 수 있는 유일한 신호는 태그 내에 일관된 하이픈이 있다는 것입니다. 이는 이것이 웹 구성요소임을 나타내는 표준입니다.


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

Shadow DOM 스타일 캡슐화

네이티브 HTML 요소에 Shadow DOM이 있는 것과 거의 마찬가지로 웹 구성요소도 있습니다. Shadow DOM은 요소 내의 숨겨진 노드 트리입니다. 이를 시각화하는 가장 좋은 방법은 Web Inspector를 열고 'Show Shadow DOMtree'(섀도우 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;
  /* ... */
}
섀도우 루트 또는 호스트 선택기에 적용되는 일부 구성요소 상용구 코드

하지만 Web Component를 사용하는 사용자가 특정 스타일을 변경해야 할 타당한 이유가 있는 경우에는 어떻게 해야 할까요? 컨텍스트로 인해 더 대비가 필요한 텍스트 줄이 있거나 테두리를 더 두꺼워야 할 수도 있습니다. 구성요소에 스타일을 입력할 수 없는 경우 이러한 스타일 지정 옵션을 잠금 해제하려면 어떻게 해야 하나요?

이때 필요한 것이 CSS 사용자설정 속성입니다.

CSS 맞춤 속성

맞춤 속성은 적절한 이름으로 지정됩니다. 이 속성은 전적으로 직접 이름을 지정하고 필요한 값을 적용할 수 있는 CSS 속성입니다. 하이픈 두 개를 접두어로 사용하기만 하면 됩니다. 맞춤 속성을 선언한 후에는 var() 함수를 사용하여 CSS에서 값을 사용할 수 있습니다.


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

.n-color-accent-text {
  color: var(--n-color-accent);
}
맞춤 속성으로서 도움말 클래스에서 사용되는 디자인 토큰을 보여주는 CSS 프레임워크의 예

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

var() 함수를 사용하여 맞춤 속성을 상속하는 이 기능은 웹 구성요소의 Shadow DOM을 관통하고 개발자가 구성요소의 스타일을 지정할 때 더 세밀하게 제어할 수 있는 방법입니다.

Nord 웹 구성요소의 맞춤 속성

디자인 시스템용 구성요소를 개발할 때마다 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를 생성하므로 해당 구성 요소의 사용자가 활용할 수 있다는 것입니다.


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

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

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

이러한 접근 방식은 디자인 시스템 구성요소를 만든 사람뿐 아니라 Google 제품에서 이러한 구성요소를 사용하는 개발팀에도 큰 효과를 발휘합니다.

맞춤 속성을 한 단계 더 발전시키기

이 글을 작성하는 시점에는 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에서 함께 만든 속성입니다. 이는 구성요소 자체의 상황별 '비공개' 맞춤 속성이지만 대체가 있는 '공개' 맞춤 속성으로 설정됩니다.



: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>
이번에도 구분선의 컨텍스트 맞춤 속성을 섹션 선택기에 추가하여 구분선의 색상을 변경할 수 있습니다. 구분선이 이를 상속하여 더 깔끔하고 유연한 코드를 생성합니다.

이 방법이 실제로 '비공개'는 아니라고 주장할 수 있지만 우리는 여전히 이 방법이 우리가 우려했던 문제를 해결하는 다소 우아한 해결책이라고 생각합니다. 기회가 되면 개발팀이 구성요소 사용을 더 잘 제어하면서 이미 마련된 안전장치의 이점을 누릴 수 있도록 구성요소에서 이 문제를 해결할 것입니다.

CSS 맞춤 속성과 함께 웹 구성요소를 사용하는 방식이 얼마나 유용한지에 대한 유용한 정보를 얻으셨기를 바랍니다. 어떻게 생각하시는지 알려주세요. 작업에 이러한 방법을 사용하려는 경우 트위터(@DavidDarnes)를 팔로우하세요. Twitter에서 Nordhealth @NordhealthHQ를 비롯해 @Viljamis, @WickyNilliams, @eric_habich에서 이 디자인 시스템을 통합하고 이 글에서 언급한 기능을 실행하기 위해 열심히 노력한 나머지 팀원도 확인하실 수 있습니다.

히어로 이미지: 댄 크리스티안 파두레트