Nordhealth가 웹 구성요소에서 맞춤 속성을 사용하는 방법

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

David Darnes
David Darnes

저는 Nordhealth의 수석 프런트엔드 개발자인 데이브입니다. 저는 디자인 시스템 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 프레임워크와 호환되며 프레임워크가 없어도 작동한다는 것입니다. 페이지에서 기본 JavaScript 패키지가 참조되면 웹 구성요소를 사용하는 것은 네이티브 HTML 요소를 사용하는 것과 매우 유사합니다. 이것이 네이티브 HTML 요소가 아니라는 것을 알려주는 유일한 실제 징후는 태그 내의 일관된 하이픈입니다. 이는 브라우저에 이것이 웹 구성요소임을 나타내는 표준입니다.

섀도우 DOM 스타일 캡슐화

네이티브 HTML 요소에 Shadow DOM이 있는 것과 마찬가지로 웹 구성요소에도 Shadow DOM이 있습니다. Shadow DOM은 요소 내에 있는 숨겨진 노드 트리입니다. 이를 시각화하는 가장 좋은 방법은 웹 검사기를 열고 'Shadow DOM 트리 표시' 옵션을 사용 설정하는 것입니다. 이 작업을 완료한 후 검사기에서 네이티브 입력 요소를 살펴보세요. 이제 해당 입력을 열고 그 안에 있는 모든 요소를 볼 수 있습니다. Google의 웹 구성요소 중 하나를 사용하여 이를 시도해 볼 수도 있습니다. Google의 맞춤 입력 구성요소를 검사하여 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 웹 구성요소의 맞춤 속성

디자인 시스템의 구성요소를 개발할 때마다 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);
  /* ... */
}
구성요소의 shadow root에 정의된 후 구성요소 스타일에 사용되는 맞춤 속성입니다. 디자인 토큰 목록의 맞춤 속성도 사용됩니다.

또한 구성요소에만 적용되고 토큰에는 없는 일부 값을 추상화하여 문맥 맞춤 속성으로 변환합니다. 구성요소의 컨텍스트에 맞는 맞춤 속성은 두 가지 주요 이점을 제공합니다. 첫째, 이 값을 구성요소 내의 여러 속성에 적용할 수 있으므로 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를 만들 수 있으며, 이 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에 패키징됩니다. 그런 다음 문서 사이트가 배포될 때 매니페스트 파일을 데이터로 사용합니다. 이는 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>
두 개의 구분선이 다시 표시되지만 이번에는 구분선의 문맥 맞춤 속성을 섹션 선택기에 추가하여 구분선의 색상을 변경할 수 있습니다. 구분선이 이를 상속하여 더 깔끔하고 유연한 코드를 생성합니다.

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

CSS 맞춤 속성과 함께 웹 구성요소를 사용하는 방법에 관한 이 유용한 정보가 도움이 되었기를 바랍니다. 의견을 알려주세요. 이러한 방법 중 하나를 사용하기로 결정했다면 Twitter(@DavidDarnes)에서 저를 찾으실 수 있습니다. 트위터에서 Nordhealth(@NordhealthHQ)를 찾을 수 있으며, 이 디자인 시스템을 함께 만들고 이 문서에 언급된 기능을 구현하기 위해 열심히 노력한 나머지 팀원들도 찾을 수 있습니다. @Viljamis, @WickyNilliams, @eric_habich.