본문 바로가기
css

Vanilla Extract CSS를 사용하여 설계 시스템 생성

by code-box 2022. 2. 23.
반응형

나는 최근에 Vanilla Extract CSS를 확인하는 시간을 가졌습니다. 스타일링 라이브러리는 Styled Components 또는 Imotion과 같은 CSS-in-JS 프로젝트와 유사하지만 Stylex(Meta) 또는 Stitchs와 같은 라이브러리처럼 생산을 위해 번들 CSS를 생성하는 스타일링 라이브러리입니다. 그리고 물론 원자 CSS를 생성할 수 있기 때문에 본질적으로 자신만의 테일윈드 라이브러리를 만들 수 있습니다.

이 문서는 토큰, 테마, 변형과 같은 디자인 시스템의 기본 주제에 대해 자세히 설명합니다. 이 중 몇 가지가 무엇인지 잘 모르신다면, 이 내용을 다루는 가이드나 튜토리얼을 확인해보겠습니다.

그것은 어떻게 작동하나요?

Style API(Style(CSObject))를 사용하여 스타일을 작성합니다. 이 API는 Style Components, Imotion, JSS 등과 같은 라이브러리와 유사합니다.

 

이러한 스타일은 변수(예: buttonClass)에 저장됩니다. 스타일 메서드는 HTML 요소 또는 React 구성요소에 적용할 수 있는 CSS 클래스 이름을 반환합니다.

const buttonClass = style({ display: flex })

return <Button className={buttonClass} />

스타일 변수를 결합하여 스타일을 구성할 수도 있습니다.

const combinedStyle = style({ buttonClass, backgroundColor: 'red' })

과정

 

내가 가장 먼저 받아들여야 했던 것은 Vanilla Extract는 빌드 파이프라인이 필요하다는 것이다. 웹 팩, 구획 또는 esbuild에 관계없이 프로젝트에서 지원되는 빌드 라이브러리 설정이 필요합니다.

React 앱 및 라이브러리 부트스트랩과 함께 Nx 모노레포를 사용하겠지만 빌드 구성 확장을 지원하는 모든 프로젝트 상용구(예: NextJS, GatsbyJS 등)를 사용할 수 있으며 매우 유사한 프로세스입니다.

세우다

프로젝트의 기반으로 Nx 모노레포를 사용하기 때문에 주 라이브러리 @vanilla-extract/css와 함께 웹 팩 플러그인을 사용했습니다.

yarn add @vanilla-extract/css @vanilla-extract/webpack-plugin
 

빌드 구성 배선

디자인 시스템을 완전히 번들로 배송할 것인지(Webpack을 사용하여) 최종 사용자가 번들로 제공하기를 원하는지 선택해야 했습니다.

나는 도서관 소비자가 바닐라 엑스트랙트 웹팩 플러그인을 설치하도록 하는 후자를 선택했다. 이렇게 하면 내 라이브러리에 추가 설정 단계가 추가되지만 코드를 더 쉽게 전송할 수 있습니다. 또한 사용자가 자신의 빌드 스크립트에 연결할 수 있으므로 성능을 더욱 최적화할 수 있습니다. 단 한가지 중요한 주의사항은 CDN에서 CodePen과 같은 것을 통해 라이브러리를 사용할 수 없다는 것입니다.

Nx 모노레포를 사용하면 라이브러리는 즉시 빌드 시스템을 설정하는 것이 아니라 Typescript를 사용하여 코드를 컴파일합니다. 그러나 당신이 "앱"을 만들거나 스토리북과 같은 서비스를 사용하면 기본적으로 웹팩이 설치된다.

우리는 이러한 앱에 Vanilla Extract 플러그인을 추가하여 디자인 시스템을 가져오고 앱을 만들 수 있습니다. 이 과정은 우리 도서관 사용자와 동일하므로, 구현을 테스트하는 좋은 방법입니다.

 

Nx를 사용하면 스토리북 구성을 쉽게 확장할 수 있습니다.

// .storybook/main.js
module.exports = {
    stories: [],
    addons: ['@storybook/addon-essentials'],
    webpackFinal: async (config, { configType }) => {
          // apply any global webpack configs that might have been specified in .storybook/main.js
          if (rootMain.webpackFinal) {
                  config = await rootMain.webpackFinal(config, { configType })
          }

          // Add Vanilla Extract here
          config.plugins = [...config.plugins, new VanillaExtractPlugin()]

          return config
    },
}

라이브러리 사용

스타일 API

스타일 방법을 사용하고 CSS 특성의 객체를 전달합니다. 스타일링을 위한 객체 구문과 함께 이모션(또는 스타일링된 구성 요소)과 유사합니다.

 
// button.css.ts
import { style } from '@vanilla-extract/css'

export const buttonStyles = style({
    display: 'flex',
    backgroundColor: 'red',
})
// button.tsx
import { buttonStyles } from './button.css'

export interface ButtonProps {}

export function Button(props: ButtonProps) {
    return (
          <button className={buttonStyles}>
            <h1>Welcome to Button!</h1>
    </button>
  )
}

export default Button

Button in StorybookJS

이 스타일 방법은 다음을 지원합니다.

  • 현재 구성요소의 스타일
  • CSS 유사 선택기(:hover)
  • CSS Selector(현재 객체에만 해당)
  • CSS 변수(새 토큰의 범위 변경 또는 생성용)
  • 미디어 쿼리(중단점에 대한 @미디어)
  • 브라우저 지원(@지원)
 

또한 다른 Vanilla Extract 기능(다른 구성 요소 스타일 또는 CSS 변수 이름 및 값을 사용하여 클래스 이름을 참조하는 등)과의 상호 운용성이 우수합니다.

테마

테마는 Vanilla Extract의 퍼스트 클래스 시민으로, 필요에 따라 몇 가지 다양한 방법으로 만들 수 있습니다. 테마는 스타일에서 변수로 사용되는 속성의 CSS 객체입니다. 예를 들어, 설계 시스템에서 글꼴 크기가 각각 다른 테마 특성(테마)을 가질 수 있습니다.글꼴.작음). 이 변수들은 빌드 시 CSS 변수로 변환되고 다른 구성 요소들에 의해 CSS 변수(var(-fonts-small-12883)로 사용됩니다.

첫 번째 테마 방법은 만들기입니다.Theme. 테마 속성 및 값을 수락하고 CSS 클래스 이름(테마의 앱 전체 또는 일부를 줄바꿈하는 데 사용됨 - <div className={themeClass}))과 테마 속성을 포함하는 변수(다른 구성 요소 내부에서 사용됨)를 반환합니다.글꼴.작음).

                                                                                    ```js
                                                                                    // theme.css.ts
                                                                                    import { createTheme } from '@vanilla-extract/css'

                                                                                    export const [themeClass, vars] = createTheme({
                                                                                      color: {
                                                                                        brand: 'blue',
                                                                                      },
                                                                                      font: {
                                                                                        body: 'arial',
                                                                                      },
                                                                                    })

                                                                                    // In your component:
                                                                                    import { style } from '@vanilla-extract/css'
                                                                                    import { vars } from '../theme'

                                                                                    export const buttonStyles = style({
                                                                                      display: 'flex',
                                                                                      backgroundColor: vars.color.brand,
                                                                                    })

                                                                                    // Make sure to wrap your app in the `themeClass`
                                                                                    import { themeClass } from '../theme'

                                                                                    export const App = ({ children }) => (
{children}

) ```

 

두 번째 테마 방법은 createGlobalTheme입니다. 이 방법은 테마 변수를 컨테이너에 부착합니다(<div id="app"처럼). 테마를 파일로 가져오면 CSS 변수가 문서 범위에 삽입됩니다.

                                                              ```js
                                                              import { createGlobalTheme } from '@vanilla-extract/css'

                                                              // In this case, we attach variables to global `:root`
                                                              // so you don't need an ID or Class in your DOM
export const vars = createGlobalTheme(':root', {
  color: {
    brand: 'blue',
  },
  font: {
    body: 'arial',
  },
})
```

세 번째 테마 방법은 만들기입니다.ThemeContract - 기본적으로 테마(속성만 해당)의 "도형"을 수락하고 사용자가 문자 그대로 값을 무효화합니다. 그런 다음 이 테마를 기본으로 사용하여 다른 테마를 만듭니다.

```js
import {
  createThemeContract,
  createTheme
} from '@vanilla-extract/css';

// The contract - or "shape" of theme
export const vars = createThemeContract({
  color: {
    brand: null
  },
  font: {
    body: null
  }
});

// "Fills in" the contract with values
export const themeA = createTheme(vars, {
  color: {
    brand: 'blue'
  },
  font: {
    body: 'arial'
  }
});

// Secondary theme
export const themeB = createTheme(vars, {
  color: {
    brand: 'pink'
  },
  font: {
    body: 'comic sans ms'
  }
});

// In your component
<div id="app" className={mode === 'a' ? themeA : themeB}>
```

### 밝음/어둠 모드

<div class="content-ad"></div>

밝음/어둠 모드 토글을 처리하는 데는 몇 가지 방법이 있습니다. 두 가지 테마만 만들면 밝은 테마를 "기본" 테마로 정의하고 어두운 테마의 기준으로 사용할 수 있습니다.

```js
//  light.css.ts
import { createTheme } from '@vanilla-extract/css'

export const colors = {
  text: '#111212',
  background: '#fff',
  primary: '#005CDD',
  secondary: '#6D59F0',
  muted: '#f6f6f9',
  gray: '#D3D7DA',
  highlight: 'hsla(205, 100%, 40%, 0.125)',
  white: '#FFF',
  black: '#111212',

  success: 'green',
  message: 'blue',
  warning: 'yellow',
  danger: 'red',
}

export const gradients = {
  subtle: `linear-gradient(180deg, ${colors.primary} 0%, ${colors.secondary} 100%)`,
  purple: `linear-gradient(180deg, ${colors.primary} 0%, #A000C4 100%)`,
  blue: `linear-gradient(180deg, #00D2FF 0%, ${colors.secondary} 100%)`,
}

export const [themeClass, vars] = createTheme({
  colors,
  gradients,
})

//  dark.css.ts
import { createTheme } from '@vanilla-extract/css'
import { vars } from './light.css'

// Redefine all the colors here (except, you know, dark)
// const colors = { /* ... */ };
// const gradients = { /* ... */ };

// We use the vars from the light theme here as basis for this theme
// It ensures our dark theme has same properties (like color.brand)
export const [darkTheme, darkVars] = createTheme(vars, {
  colors,
  gradients,
})
```

그런 다음 앱에서 밝은 모드와 어두운 모드를 전환할 때(보통 응답 상태 또는 사용자의 시스템 기본 설정에 따라 콜백 사용) 앱을 밝은 색에서 감싼 클래스 이름을 변경합니다.테마를 어둡게주제

```js
import { ThemeContext, ThemeNames } from '../context/theme'
import { useState } from 'react'
import { light, dark, base } from '../theme'

/* eslint-disable-next-line */
export interface ThemeProviderProps {}

export function ThemeProvider({
  children,
}: React.PropsWithChildren<ThemeProviderProps>) {
  const [selectedTheme, setSelectedTheme] = useState<ThemeNames>('light')

  const toggleTheme = () => {
    setSelectedTheme((prevValue) => (prevValue === 'light' ? 'dark' : 'light'))
  }

  const colorMode = selectedTheme === 'light' ? light.class : dark.class
  return (
    <ThemeContext.Provider
      value={
        theme: selectedTheme,
        toggleTheme,
      }
    >
<div className={`${base.class} ${colorMode}`}>{children}</div>
    </ThemeContext.Provider>
  )
}

export default ThemeProvider
```

### "기본 테마"

<div class="content-ad"></div>

하지만 테마 모드 간에 변하지 않는 속성은 어떨까요? 글꼴 크기나 간격 같은 거요? 여기서 테마의 구성성이 발휘됩니다. 모든 공유 속성을 포함하는 "기본" 테마로 작동하는 테마를 만들 수 있습니다.

```js
//  base.css.ts
import { createTheme } from '@vanilla-extract/css';

export const [baseThemeClass, vars] = createTheme({
  fontSizes: {
        small: '12px',
        medium: '16px',
    },
    spacing: {
        0: '2px',
        1: '8px',
    }
});

// Then in the app you can combine the two themes
// Base and Light/Dark
export const App = ({ children }) => <div className={`${baseThemeClass} ${lightThemeClass}`}
```

이렇게 하면 어두운 버전과 같은 다른 테마를 정의할 때 글꼴 크기와 같이 동일하게 유지되는 속성을 재정의할 필요가 없습니다.

Vanilla Extract의 테마는 기본 테마에 의해 정의된 모든 속성을 사용해야 합니다. 예를 들어 밝은 테마와 동일하거나 CSS가 구축되지 않고 오류가 발생하더라도 색상을 건너뛸 수 없습니다. 이상적으로 밝은 테마에서 테마 속성을 전달하고 필요한 부분을 재지정하여 "확장"할 수 있습니다(계속 어둡음).테마 = 만들기테마(lightVars, {...lightTheme, 색상: { red: `dark-red` } } }) - 하지만 저는 모든 것을 정리하고 좀 더 모듈식으로 유지하는 것이 더 좋다고 생각합니다.

### 테마 API 표준화

<div class="content-ad"></div>

각각 자신만의 토큰 세트를 가진 두 개의 테마가 있었으므로 최종 사용자에게 적합한 API가 있는지 확인하고 싶었습니다.

처음에는 각 테마 클래스 이름뿐만 아니라 토큰 속성을 가진 테마 객체를 속성으로 내보내기로 결정했습니다. 이것은 주제를 빨리 할 수 있게 해줍니다.토큰 및 grab 토큰 - 또는 temes.light를 사용하여 테마 중 하나를 사용합니다.

```js
export {
  // The tokens from one theme (or a theme contract)
  tokens,
  // Each theme class
  light,
  dark,
}
```

이것은 한 가지 유형의 테마에만 적용되지만, 2가지("기본" 테마와 "색" 테마)가 있기 때문에 토큰(또는 변수)을 함께 결합하는 다른 구조가 필요했습니다.

```js
//  theme/light.css.ts
const [themeClass, vars] = createTheme({
  colors,
  gradients,
});

// We export an object
// So we don't have to destructure each time
                                                              const light = {
                                                                class: themeClass,
                                                                tokens: vars,
                                                              };

                                                              //  theme/index.ts
                                                              export {
                                                                  // Each theme class
                                                                  themes: {
                                                                      base,
                                                                      light,
                                                                      dark,
                                                                  },

                                                                  // Tokens
                                                                  tokens: {
                                                                      ...baseVars,
                                                                      ...colorVars,
                                                                  }
                                                              }

                                                              //  In a component:
                                                              import { style } from '@vanilla-extract/css';
                                                              import { tokens } from '../theme';

                                                              export const buttonStyles = style({
                                                                display: 'flex',
                                                                backgroundColor: tokens.colors.primary,
                                                              });
                                                              ```

                                                              <div class="content-ad"></div>

                                                              비록 최종 사용자는 어떤 테마가 (빛/어둠과 같은) 상호 교환이 가능하고 어떤 토큰이 어느 것에 속하는지 이해하기 어렵기 때문에 혼란스러울 수 있다.

                                                              # 생각들

                                                              몇 가지 구성 요소를 만들고 여러 상황에서 최종적으로 어떻게 사용될 것인가에 대해 많은 시간을 보낸 후, 다양한 주제에 대한 몇 가지 생각이 들었습니다.

                                                              ## 사용자 정의

                                                              제가 많은 라이브러리에서 겪고 있는 큰 문제 중 하나는 구성 요소를 사용자 지정하거나(단추에서 테두리 반지름을 제거하는 것과 같은) 전체 시스템의 미관을 극적으로 바꾸는 것입니다.

                                                              <div class="content-ad"></div>

                                                              이상적으로는 Vanilla Extract를 사용하여 설계 시스템 소비자에게 구성 요소 사용자 정의를 위한 몇 가지 진입점을 제공할 수 있습니다.

                                                              - CSS 변수(예: --button-radius - 또는 테마 토큰)
                                                              - CSS 재정의(성급이 승리함 - 종류)

                                                              ```js
                                                                <Button className={`${buttonStyles} ${buttonOverrides}`} />
                                                                ```

                                                                - 확장 스타일(버튼 스타일을 가져와 스타일() 방법의 기준으로 사용합니다.)

                                                                ```js
                                                                  import { buttonStyles } from './button.css'

                                                                    const newButtonStyles = style([...buttonStyles, { backgroundColor: 'red' }])
                                                                    ```

                                                                    <div class="content-ad"></div>

                                                                    그러나 이렇게 하려면 일종의 기본 단추를 내보내거나 구성요소 스타일을 스타일 오버라이드와 스왑하는 요소를 제공해야 합니다.

                                                                    ```js
                                                                      // Primitive button used **outside** design system
                                                                        import { PrimitiveButton } from 'design-system'

                                                                          const Button = (props) => (
                                                                              <PrimitiveButton {...props} className={yourButtonStyles}>
                                                                                    {children}
                                                                                        </PrimitiveButton>
                                                                                          )

                                                                                            // Override prop **inside** the design system
                                                                                              const Button = ({ styleOverride }) => (
                                                                                                  <button className={styleOverride ?? buttonStyles}>{children}</button>
                                                                                                    )
                                                                                                    ```

                                                                                                    또한 구성요소 구조가 1단계 깊이라고 가정하며, 아이콘과 같이 구성요소에 스타일링이 필요한 "하위" 요소를 포함할 수 있습니다.
                                                                                                    해결책?: 스타일을 완전히 재정의할 수 있는 구성요소에 대한 스타일 지지대를 만듭니다. 또한 병합되는 className 프로포트를 수락하여 증분 변경사항을 할당할 수 있습니다. 그리고 물론, 그들은 항상 구성 요소 범위의 CSS 변수들을 바꿀 수 있습니다. 가장 극단적이고 간단한 변경 사항을 처리합니다. 옛날의 MUI를 떠올리게 하고 그들이 처리하던 방식을 떠올리게 하는 것 같아.

                                                                                                    ### 변종도 이상해진다.

                                                                                                    구성요소 축척 또는 색상표 변경과 같이 구성요소의 단순 스타일 반복을 작성해야 하는 경우 변형을 사용하는 경우가 많습니다. Vanilla Extract는 styleVariant 메소드를 사용하여 구성 요소가 스왑할 수 있는 여러 스타일 세트를 만듭니다.

                                                                                                    <div class="content-ad"></div>

                                                                                                    ```js
                                                                                                    import { styleVariants } from '@vanilla-extract/css';

                                                                                                    export const variant = styleVariants({
                                                                                                      primary: { background: 'blue' },
                                                                                                        secondary: { background: 'aqua' }
                                                                                                        });

                                                                                                        // In React:
                                                                                                        <button className={variant[props.variant]}>
                                                                                                        ```

                                                                                                        우리가 제어할 수 있을 때는 잘 작동하지만 사용자가 직접 삽입해야 할 때는...펑키한

                                                                                                        ```js
                                                                                                        // A user importing our component and extending with a new variant
                                                                                                        <Button style={overrideVariantHow...}>
                                                                                                        ```

                                                                                                        특정 변형(colorSchemeOverride)을 오버라이드할 수 있는 요소를 만들거나, 다른 소품 아래에서 자체 변형을 만들거나(코드 더블업) CSS 변수를 사용하여 스타일을 제어할 수 있습니다. 그러나 사용자가 특정 변종 API를 좋아하는 경우 구성 요소 아키텍처에 약간 갇혀 꺼내기 작업을 수행해야 하지만 몇 가지를 추가하거나 조정해야 합니다.

                                                                                                        ### 변형에 선택기 없음

                                                                                                        <div class="content-ad"></div>

                                                                                                        이것은 스타일을 제한합니다. :hover 상태를 변형에 추가하거나 :before selector(구성 요소 뒤에 배치해야 하는 경우)를 추가할 수 없습니다.

                                                                                                        예를 들어, 구성 요소 뒤에 추가 테두리를 배치해야 하는 포커스 상태가 있습니다. 그것은 "이중"의 국경 효과를 만들어낸다.

                                                                                                        이것은 또한 변형을 제한한다. 예를 들어, 유사 선택기가 있는 단추에 대해 "고스트" 또는 "오프라인" 변형을 생성하여 다른 상태(호버, 비활성화 등)를 변경할 수 없습니다. 이 스타일은 모든 선택기 상태에 걸쳐 작동하기를 바라는 "일률적인" 솔루션입니다.

                                                                                                        또 다른 옵션은 이러한 "복잡한" 스타일 변형마다 별도의 구성요소를 만드는 것입니다. 하지만 사이징/패딩/기타 변형과 같은 스타일이 여러 번 겹치는 등 동일한 구성요소를 몇 번 작성하는 느낌입니다. 유사 선택기를 통해 스타일링의 깊이를 더 깊이 있게 활용할 수 있습니다.

                                                                                                        ### 레시피 API

                                                                                                        <div class="content-ad"></div>

                                                                                                        Recipes API는 "새로운 버튼을 만들어야 하지만 여전히 이 버튼의 기본 규칙 내에서 유지하기를 원한다"라는 문제를 다룬다.

                                                                                                        그러나 새로운 변형을 추가하거나 스타일을 확장하는 문제는 여전히 해결되지 않습니다. 의사들은 레시피를 취해서 다른 사람의 기준으로 사용할 수 있는 기능에 대해서는 언급하지 않고 단지 특성이나 변종만 변경할 뿐입니다.

                                                                                                        # 문제들

                                                                                                        ## 스토리북 신뢰할 수 없는 HMR

                                                                                                        스토리북의 HMR 또는 Hot Module Reloading 기능을 사용하면 전체 앱이 다시 만들어질 때까지 기다리지 않고 소스 코드를 편집하여 변경 사항을 빠르게 표시할 수 있습니다. 이 기능은 Vanilla Extract와 약간 충돌하여 스타일링에 불일치를 일으킵니다. 그것을 고치는 유일한 방법은 CSS를 제대로 다시 로드하는 스토리북 앱을 열심히 새로 고치는 것입니다. 예를 들어 구성 요소의 스타일에서 속성을 변경하면 구성 요소 스타일을 완전히 다시 로드하고 제거할 수 있습니다.

                                                                                                        <div class="content-ad"></div>

                                                                                                        # Vanilla Extract를 사용해야 하나요?

                                                                                                        특히 슈퍼 콤플렉스 CSS가 진행되지 않는 보다 단순한 디자인 시스템의 경우 견고한 스타일링 솔루션이라고 생각합니다. Typescript 통합만으로도 스타일 저작 경험이 훨씬 더 즐겁고 안전합니다.

                                                                                                        빠르고, 더럽고, 최첨단의 무언가를 만들고 있다면 - 입력(및 출력)을 제한하기 때문에 추천할 수 있을지 잘 모르겠지만 - 저는 이러한 경우에는 Styled Components 또는 Impaction과 같은 옵션을 사용할 것입니다.

                                                                                                        # 젤라토 UI

                                                                                                        Github에서 실험하던 디자인 시스템을 찾을 수 있습니다. 저는 그것을 만드는데 필요한 바닐라 추출물을 기리기 위해 Gelato UI라고 불렀습니다.

                                                                                                        <div class="content-ad"></div>

                                                                                                        # 참조 사항

                                                                                                        - @vanilla-extract/css를 사용하는 게시된 NPM 라이브러리
                                                                                                        - Vanilla Extract로 리액트 응용 프로그램 테밍

'css' 카테고리의 다른 글

전체 플렉스박스 가이드  (0) 2022.02.23
리미탄도 문자 컴 3 폰틴호  (0) 2022.02.23
Cadastro de DEVs  (0) 2022.02.23
스파이더맨 위키  (0) 2022.02.23
javascript를 사용한 단위 변환  (0) 2022.02.23

댓글