본문 바로가기
css

바닐라 추출이 포함된 TypeScript의 CSS

by code-box 2021. 10. 13.
반응형

바닐라 추출은 프레임워크에 구애받지 않는 새로운 CSS-in-TypeScript 라이브러리입니다. 가볍고 견고하며 직관적인 스타일 작성 방법입니다. 바닐라 추출은 규범적인 CSS 프레임워크가 아니라 유연한 개발자 툴링입니다. CSS 툴링은 PostCSS, Sass, CSS 모듈 및 스타일링된 컴포넌트가 2017년 이전에 출시되어 지난 몇 년간 비교적 안정적인 공간이었으며 오늘날에도 인기가 있습니다. Tailwind는 지난 몇 년 동안 CSS 툴링에서 많은 것을 변화시킨 몇 가지 도구 중 하나입니다.

바닐라 아이스크림은 상황을 다시 뒤흔드는 것을 목표로 한다. 이 제품은 올해 출시되었으며 다음과 같은 최신 트렌드를 활용할 수 있다는 장점이 있습니다.

  • TypeScript로 전환하는 자바스크립트 개발자
  • CSS 사용자 지정 속성에 대한 브라우저 지원
  • 유틸리티 우선 스타일링

바닐라 추출에는 아주 많은 영리한 혁신들이 있는데, 이것이 큰 문제가 된다고 생각합니다.

제로 런타임

 

CSS-in-JS 라이브러리는 일반적으로 런타임에 문서에 스타일을 삽입합니다. 여기에는 중요한 CSS 추출 및 동적 스타일링을 비롯한 이점이 있습니다.

그러나 일반적으로 별도의 CSS 파일이 더 성능이 우수합니다. 이는 자바스크립트 코드가 더 비싼 파싱/컴파일을 거쳐야 하는 반면, HTTP2 프로토콜은 추가 요청 비용을 낮추는 동안 별도의 CSS 파일을 캐시할 수 있기 때문이다. 또한 사용자 지정 속성은 많은 동적 스타일을 무료로 제공할 수 있습니다.

따라서 런타임에 스타일을 주입하는 대신 바닐라 추출은 리나리아와 아스트로터프를 닮았습니다. 이러한 라이브러리를 사용하면 빌드 시 리핑되어 CSS 파일을 구성하는 데 사용되는 자바스크립트 함수를 사용하여 스타일을 작성할 수 있습니다. TypeScript에서 바닐라 추출을 쓰더라도 프로덕션 JavaScript 번들의 전체 크기에 영향을 미치지 않습니다.

타이프스크립트

큰 바닐라 추출 가치 제안은 타이핑을 받는 것입니다. 나머지 코드베이스 유형을 안전하게 유지하는 것이 중요하다면 스타일도 똑같이 하는 것이 어떨까요?

 
TypeScript provides a number of benefits. First, there’s autocomplete. If you type “fo” then, in a TypeScript-friendly editor, you get a list of font options in a drop down — `fontFamily`, `fontKerning`, `fontWeight`, or whatever else matches — to choose from. This makes CSS properties discoverable from the comfort of your editor. If you can’t remember the name of `fontVariant` but know it’s going to start with the word “font” you type it and scroll through the options. In VS Code, you don’t need to download any additional tooling to get this to happen.

이렇게 하면 스타일 작성 속도가 상당히 빨라집니다.

이것은 또한 여러분의 편집자가 여러분이 좌절스러운 버그를 일으킬 수 있는 철자 실수를 하지 않도록 어깨너머로 지켜보고 있다는 것을 의미합니다.

또한 바닐라 추출 유형은 유형 정의의 구문에 대한 설명과 편집 중인 CSS 속성에 대한 MDN 설명서에 대한 링크를 제공합니다. 이것은 스타일이 예기치 않게 동작할 때 미친 듯이 구글링하는 단계를 제거한다.

 

Writing in TypeScript means you’re using camel-case names for CSS properties, like `backgroundColor`. This might be a bit of a change for developers who are used regular CSS syntax, like `background-color`.

통합

바닐라 브랜드는 모든 최신 번들러를 위한 1등급 통합 기능을 제공합니다. 다음은 현재 지원되는 통합의 전체 목록입니다.

  • 웹 팩
  • 에스빌드
  • 비테
  • 스노우팩
  • 넥스트JS
  • 개츠비
 

그것은 또한 완전히 틀에 구애받지 않습니다. vanilla-Extract에서 클래스 이름을 가져오기만 하면 빌드 시 문자열로 변환됩니다.

사용법

To use vanilla-Extract, you write up a `.css.ts` file that your components can import. Calls to these functions get converted to hashed and scoped class name strings in the build step. This might sound similar to CSS Modules, and this isn’t by coincidence: one of the creators of vanilla-Extract, Mark Dalgleish, is also co-creator of CSS Modules.
You can create an automatically scoped CSS class using the `style()` function. You pass in the element’s styles, then export the returned value. Import this value somewhere in your user code, and it’s converted into a scoped class name.
// title.css.ts
import {style} from "@vanilla-extract/css";

export const titleStyle = style({
    backgroundColor: "hsl(210deg,30%,90%)",
    fontFamily: "helvetica, Sans-Serif",
    color: "hsl(210deg,60%,25%)",
    padding: 30,
    borderRadius: 20,
});
 
// title.css.ts
import {style} from "@vanilla-extract/css";

export const titleStyle = style({
    backgroundColor: "hsl(210deg,30%,90%)",
    fontFamily: "helvetica, Sans-Serif",
    color: "hsl(210deg,60%,25%)",
    padding: 30,
    borderRadius: 20,
});
// title.ts
import {titleStyle} from "./title.css";

document.getElementById("root").innerHTML = `<h1 class="${titleStyle}">Vanilla Extract</h1>`;
// title.ts
import {titleStyle} from "./title.css";

document.getElementById("root").innerHTML = `<h1 class="${titleStyle}">Vanilla Extract</h1>`;

미디어 쿼리 및 유사 선택기를 스타일 선언 내부에 포함할 수도 있습니다.

// title.css.ts
backgroundColor: "hsl(210deg,30%,90%)",
  fontFamily: "helvetica, Sans-Serif",
    color: "hsl(210deg,60%,25%)",
      padding: 30,
        borderRadius: 20,
          "@media": {
              "screen and (max-width: 700px)": {
                    padding: 10
              }
          },
            ":hover":{
                backgroundColor: "hsl(210deg,70%,80%)"
            }
 
// title.css.ts
backgroundColor: "hsl(210deg,30%,90%)",
  fontFamily: "helvetica, Sans-Serif",
    color: "hsl(210deg,60%,25%)",
      padding: 30,
        borderRadius: 20,
          "@media": {
              "screen and (max-width: 700px)": {
                    padding: 10
              }
          },
            ":hover":{
                backgroundColor: "hsl(210deg,70%,80%)"
            }
These `style` function calls are a thin abstraction over CSS — all of the property names and values map to the CSS properties and values you’re familiar with. One change to get used to is that values can sometimes be declared as a number (e.g. `padding: 30`) which defaults to a pixel unit value, while some values need to be declared as a string (e.g. `padding: "10px 20px 15px 15px"`).
The properties that go inside the style function can only affect a single HTML node. This means you can’t use nesting to declare styles for the children of an element — something you might be used to in Sass or PostCSS. Instead, you need to style children separately. If a child element needs different styles based on the parent, you can use the `selectors` property to add styles that are dependent on the parent:
// title.css.ts
export const innerSpan = style({
    selectors:{[`${titleStyle} &`]:{
          color: "hsl(190deg,90%,25%)",
          fontStyle: "italic",
          textDecoration: "underline"
    }
              });
// title.css.ts
export const innerSpan = style({
    selectors:{[`${titleStyle} &`]:{
          color: "hsl(190deg,90%,25%)",
          fontStyle: "italic",
          textDecoration: "underline"
    }
              });
 
// title.ts
import {titleStyle,innerSpan} from "./title.css";
document.getElementById("root").innerHTML = 
  `<h1 class="${titleStyle}">Vanilla <span class="${innerSpan}">Extract</span></h1>
<span class="${innerSpan}">Unstyled</span>`;
// title.ts
import {titleStyle,innerSpan} from "./title.css";
document.getElementById("root").innerHTML = 
  `<h1 class="${titleStyle}">Vanilla <span class="${innerSpan}">Extract</span></h1>
<span class="${innerSpan}">Unstyled</span>`;

또는 테밍 API(다음 내용)를 사용하여 하위 노드에서 사용되는 상위 요소에 사용자 지정 속성을 만들 수도 있습니다. 이는 제한적으로 들릴 수 있지만, 대규모 코드베이스에서 유지 관리성을 높이기 위해 의도적으로 이 방법을 사용했습니다. 즉, 프로젝트의 각 요소에 대해 스타일이 선언된 위치를 정확히 알 수 있습니다.

You can use the `createTheme` function to build out variables in a TypeScript object:
// title.css.ts
import {style,createTheme } from "@vanilla-extract/css";

// Creating the theme
export const [mainTheme,vars] = createTheme({
    color:{
          text: "hsl(210deg,60%,25%)",
          background: "hsl(210deg,30%,90%)"
    },
    lengths:{
          mediumGap: "30px"
    }
})

// Using the theme
export const titleStyle = style({
    backgroundColor:vars.color.background,
    color: vars.color.text,
    fontFamily: "helvetica, Sans-Serif",
    padding: vars.lengths.mediumGap,
    borderRadius: 20,
});
 
// title.css.ts
import {style,createTheme } from "@vanilla-extract/css";

// Creating the theme
export const [mainTheme,vars] = createTheme({
    color:{
          text: "hsl(210deg,60%,25%)",
          background: "hsl(210deg,30%,90%)"
    },
    lengths:{
          mediumGap: "30px"
    }
})

// Using the theme
export const titleStyle = style({
    backgroundColor:vars.color.background,
    color: vars.color.text,
    fontFamily: "helvetica, Sans-Serif",
    padding: vars.lengths.mediumGap,
    borderRadius: 20,
});
Then vanilla-extract allows you to make a variant of your theme. TypeScript helps it ensure that your variant uses all the same property names, so you get a warning if you forget to add the `background` property to the theme.

image

일반 테마와 다크 모드를 만드는 방법은 다음과 같습니다.

// title.css.ts
import {style,createTheme } from "@vanilla-extract/css";

export const [mainTheme,vars] = createTheme({
    color:{
          text: "hsl(210deg,60%,25%)",
          background: "hsl(210deg,30%,90%)"
    },
    lengths:{
          mediumGap: "30px"
    }
})
// Theme variant - note this part does not use the array syntax
export const darkMode = createTheme(vars,{
    color:{
          text:"hsl(210deg,60%,80%)",
          background: "hsl(210deg,30%,7%)",
    },
    lengths:{
          mediumGap: "30px"
    }
})
// Consuming the theme 
export const titleStyle = style({
    backgroundColor: vars.color.background,
    color: vars.color.text,
    fontFamily: "helvetica, Sans-Serif",
    padding: vars.lengths.mediumGap,
    borderRadius: 20,
});
 
// title.css.ts
import {style,createTheme } from "@vanilla-extract/css";

export const [mainTheme,vars] = createTheme({
    color:{
          text: "hsl(210deg,60%,25%)",
          background: "hsl(210deg,30%,90%)"
    },
    lengths:{
          mediumGap: "30px"
    }
})
// Theme variant - note this part does not use the array syntax
export const darkMode = createTheme(vars,{
    color:{
          text:"hsl(210deg,60%,80%)",
          background: "hsl(210deg,30%,7%)",
    },
    lengths:{
          mediumGap: "30px"
    }
})
// Consuming the theme 
export const titleStyle = style({
    backgroundColor: vars.color.background,
    color: vars.color.text,
    fontFamily: "helvetica, Sans-Serif",
    padding: vars.lengths.mediumGap,
    borderRadius: 20,
});

그런 다음 자바스크립트를 사용하여 바닐라 추출로 반환된 클래스 이름을 동적으로 적용하여 테마를 전환할 수 있습니다.

// title.ts
import {titleStyle,mainTheme,darkMode} from "./title.css";

document.getElementById("root").innerHTML = 
  `<div class="${mainTheme}" id="wrapper">
  <h1 class="${titleStyle}">Vanilla Extract</h1>
  <button onClick="document.getElementById('wrapper').className='${darkMode}'">Dark mode</button>
</div>`
// title.ts
import {titleStyle,mainTheme,darkMode} from "./title.css";

document.getElementById("root").innerHTML = 
  `<div class="${mainTheme}" id="wrapper">
  <h1 class="${titleStyle}">Vanilla Extract</h1>
  <button onClick="document.getElementById('wrapper').className='${darkMode}'">Dark mode</button>
</div>`
How does this work under the hood? The objects you declare in the `createTheme` function are turned into CSS custom properties attached to the element’s class. These custom properties are hashed to prevent conflicts. The output CSS for our `mainTheme` example looks like this:
 
.src__ohrzop0 {
    --color-brand__ohrzop1: hsl(210deg,80%,25%);
    --color-text__ohrzop2: hsl(210deg,60%,25%);
    --color-background__ohrzop3: hsl(210deg,30%,90%);
    --lengths-mediumGap__ohrzop4: 30px;
}
.src__ohrzop0 {
    --color-brand__ohrzop1: hsl(210deg,80%,25%);
    --color-text__ohrzop2: hsl(210deg,60%,25%);
    --color-background__ohrzop3: hsl(210deg,30%,90%);
    --lengths-mediumGap__ohrzop4: 30px;
}
And the CSS output of our `darkMode` theme looks like this:
.src__ohrzop5 {
    --color-brand__ohrzop1: hsl(210deg,80%,60%);
    --color-text__ohrzop2: hsl(210deg,60%,80%);
    --color-background__ohrzop3: hsl(210deg,30%,10%);
    --lengths-mediumGap__ohrzop4: 30px;
}
.src__ohrzop5 {
    --color-brand__ohrzop1: hsl(210deg,80%,60%);
    --color-text__ohrzop2: hsl(210deg,60%,80%);
    --color-background__ohrzop3: hsl(210deg,30%,10%);
    --lengths-mediumGap__ohrzop4: 30px;
}
 
So, all we need to change in our user code is the class name. Apply the `darkmode` class name to the parent element, and the `mainTheme` custom properties get swapped out for `darkMode` ones.

레시피스 API

The `style` and `createTheme` functions provide enough power to style a website on their own, but vanilla-extract provides a few extra APIs to promote reusability. The Recipes API allows you to create a bunch of variants for an element, which you can choose from in your markup or user code.

첫째, 별도로 설치해야 합니다.

npm install @vanilla-extract/recipes
 
npm install @vanilla-extract/recipes
Here’s how it works. You import the `recipe` function and pass in an object with the properties `base` and `variants`:
// button.css.ts
import { recipe } from '@vanilla-extract/recipes';

export const buttonStyles = recipe({
    base:{
          // Styles that get applied to ALL buttons go in here
    },
    variants:{
          // Styles that we choose from go in here
    }
});
// button.css.ts
import { recipe } from '@vanilla-extract/recipes';

export const buttonStyles = recipe({
    base:{
          // Styles that get applied to ALL buttons go in here
    },
    variants:{
          // Styles that we choose from go in here
    }
});
Inside `base`, you can declare the styles that will be applied to all variants. Inside `variants`, you can provide different ways to customize the element:
 
// button.css.ts
import { recipe } from '@vanilla-extract/recipes';
export const buttonStyles = recipe({
    base: {
          fontWeight: "bold",
    },
    variants: {
          color: {
                  normal: {
                            backgroundColor: "hsl(210deg,30%,90%)",
                  },
                  callToAction: {
                            backgroundColor: "hsl(210deg,80%,65%)",
                  },
          },
          size: {
                  large: {
                            padding: 30,
                  },
                  medium: {
                            padding: 15,
                  },
          },
    },
});
// button.css.ts
import { recipe } from '@vanilla-extract/recipes';
export const buttonStyles = recipe({
    base: {
          fontWeight: "bold",
    },
    variants: {
          color: {
                  normal: {
                            backgroundColor: "hsl(210deg,30%,90%)",
                  },
                  callToAction: {
                            backgroundColor: "hsl(210deg,80%,65%)",
                  },
          },
          size: {
                  large: {
                            padding: 30,
                  },
                  medium: {
                            padding: 15,
                  },
          },
    },
});

그런 다음 마크업에 사용할 변형을 선언할 수 있습니다.

// button.ts
import { buttonStyles } from "./button.css";

<button class=`${buttonStyles({color: "normal",size: "medium",})}`>Click me</button>
// button.ts
import { buttonStyles } from "./button.css";

<button class=`${buttonStyles({color: "normal",size: "medium",})}`>Click me</button>
 

또한 바닐라 추출 기능은 TypeScript를 활용하여 사용자 자신의 변형 이름에 대해 자동 완성합니다!

원하는 대로 변형 이름을 지정하고 원하는 속성을 원하는 대로 입력할 수 있습니다.

// button.css.ts
export const buttonStyles = recipe({
    variants: {
          animal: {
                  dog: {
                            backgroundImage: 'url("./dog.png")',
                  },
                  cat: {
                            backgroundImage: 'url("./cat.png")',
                  },
                  rabbit: {
                            backgroundImage: 'url("./rabbit.png")',
                  },
          },
    },
});
// button.css.ts
export const buttonStyles = recipe({
    variants: {
          animal: {
                  dog: {
                            backgroundImage: 'url("./dog.png")',
                  },
                  cat: {
                            backgroundImage: 'url("./cat.png")',
                  },
                  rabbit: {
                            backgroundImage: 'url("./rabbit.png")',
                  },
          },
    },
});
 
You can see how this would be incredibly useful for building a design system, as you can create reusable components and control the ways they vary. These variations become easily discoverable with TypeScript — all you need to type is `CMD/CTRL + Space` (on most editors) and you get a dropdown list of the different ways to customize your component.

스프링클 포함 유틸리티 우선

스프링클스는 바닐라 추출물 위에 구축된 유틸리티 우선 프레임워크입니다. 바닐라 추출 문서는 다음과 같이 설명합니다.

기본적으로, 이것은 여러분만의 실행 시간 제로, 타입 세이프 버전의 테일윈드, 스타일링 시스템 등을 만드는 것과 같습니다.

So if you’re not a fan of naming things (we all have nightmares of creating an `outer-wrapper` div then realising we need to wrap it with an . . . `outer-outer-wrapper` ) Sprinkles might be your preferred way to use vanilla-extract.
 

스프링클 API도 별도로 설치해야 합니다.

npm install @vanilla-extract/sprinkles
npm install @vanilla-extract/sprinkles

이제 유틸리티 기능을 사용할 빌딩 블록을 만들 수 있습니다. 두 개의 개체를 선언하여 색과 길이의 목록을 만들어 보겠습니다. 자바스크립트 키 이름은 우리가 원하는 어떤 것이든 될 수 있다. 이 값은 다음에 사용할 CSS 속성에 대한 유효한 CSS 값이어야 합니다. 값이어야 합니다.

// sprinkles.css.ts
const colors = {
    blue100: "hsl(210deg,70%,15%)",
    blue200: "hsl(210deg,60%,25%)",
    blue300: "hsl(210deg,55%,35%)",
    blue400: "hsl(210deg,50%,45%)",
    blue500: "hsl(210deg,45%,55%)",
    blue600: "hsl(210deg,50%,65%)",
    blue700: "hsl(207deg,55%,75%)",
    blue800: "hsl(205deg,60%,80%)",
    blue900: "hsl(203deg,70%,85%)",
};

const lengths = {
    small: "4px",
    medium: "8px",
    large: "16px",
    humungous: "64px"
};
 
// sprinkles.css.ts
const colors = {
    blue100: "hsl(210deg,70%,15%)",
    blue200: "hsl(210deg,60%,25%)",
    blue300: "hsl(210deg,55%,35%)",
    blue400: "hsl(210deg,50%,45%)",
    blue500: "hsl(210deg,45%,55%)",
    blue600: "hsl(210deg,50%,65%)",
    blue700: "hsl(207deg,55%,75%)",
    blue800: "hsl(205deg,60%,80%)",
    blue900: "hsl(203deg,70%,85%)",
};

const lengths = {
    small: "4px",
    medium: "8px",
    large: "16px",
    humungous: "64px"
};
We can declare which CSS properties these values are going to apply to by using the `defineProperties` function:
  • 속성 속성을 가진 개체를 전달합니다.
  • properties에서는 사용자가 설정할 수 있는 CSS 속성(유효한 CSS 속성이어야 함)인 객체를 선언하며, 값은 이전에 생성한 객체(컬러lengths 목록)이다.
// sprinkles.css.ts
import { defineProperties } from "@vanilla-extract/sprinkles";

const colors = {
    blue100: "hsl(210deg,70%,15%)"
    // etc.
}

const lengths = {
    small: "4px",
    // etc.
}

const properties = defineProperties({
    properties: {
          // The keys of this object need to be valid CSS properties
          // The values are the options we provide the user
          color: colors,
          backgroundColor: colors,
          padding: lengths,
    },
});
// sprinkles.css.ts
import { defineProperties } from "@vanilla-extract/sprinkles";

const colors = {
    blue100: "hsl(210deg,70%,15%)"
    // etc.
}

const lengths = {
    small: "4px",
    // etc.
}

const properties = defineProperties({
    properties: {
          // The keys of this object need to be valid CSS properties
          // The values are the options we provide the user
          color: colors,
          backgroundColor: colors,
          padding: lengths,
    },
});
 
Then the final step is to pass the return value of `defineProperties` to the `createSprinkles` function, and export the returned value:
// sprinkles.css.ts
import { defineProperties, createSprinkles } from "@vanilla-extract/sprinkles";

const colors = {
    blue100: "hsl(210deg,70%,15%)"
    // etc.
}

const lengths = {
    small: "4px",
    // etc. 
}

const properties = defineProperties({
    properties: {
          color: colors,
          // etc. 
    },
});
export const sprinkles = createSprinkles(properties);
// sprinkles.css.ts
import { defineProperties, createSprinkles } from "@vanilla-extract/sprinkles";

const colors = {
    blue100: "hsl(210deg,70%,15%)"
    // etc.
}

const lengths = {
    small: "4px",
    // etc. 
}

const properties = defineProperties({
    properties: {
          color: colors,
          // etc. 
    },
});
export const sprinkles = createSprinkles(properties);
Then we can start styling inside our components inline by calling the `sprinkles` function in the class attribute and choosing which options we want for each element.
// index.ts
import { sprinkles } from "./sprinkles.css";
document.getElementById("root").innerHTML = `<button class="${sprinkles({
    color: "blue200",
    backgroundColor: "blue800",
    padding: "large",
})}">Click me</button>
</div>`;
 
// index.ts
import { sprinkles } from "./sprinkles.css";
document.getElementById("root").innerHTML = `<button class="${sprinkles({
    color: "blue200",
    backgroundColor: "blue800",
    padding: "large",
})}">Click me</button>
</div>`;

JavaScript 출력에는 각 스타일 속성에 대한 클래스 이름 문자열이 있습니다. 이러한 클래스 이름은 출력 CSS 파일의 단일 규칙과 일치합니다.

<button class="src_color_blue200__ohrzop1 src_backgroundColor_blue800__ohrzopg src_padding_large__ohrzopk">Click me</button>
<button class="src_color_blue200__ohrzop1 src_backgroundColor_blue800__ohrzopg src_padding_large__ohrzopk">Click me</button>

보시다시피 이 API를 사용하면 미리 정의된 제약 조건 집합을 사용하여 마크업 내부의 요소를 스타일링할 수 있습니다. 또한 모든 요소에 대한 클래스 이름을 찾는 어려운 작업도 피할 수 있습니다. 그 결과 Tailwind와 비슷한 느낌이 들지만 TypeScript를 중심으로 구축된 모든 인프라에서 이점을 얻을 수 있습니다.

 

또한 스프링클 API를 사용하면 유틸리티 클래스를 사용하여 응답 스타일을 만들 수 있는 조건 및 단축키를 작성할 수 있습니다.

마무리하는 중

바닐라 추출은 CSS 툴링의 큰 새로운 단계처럼 느껴집니다. 정적 타이핑이 제공하는 모든 힘을 활용하는 스타일링을 위한 직관적이고 강력한 솔루션으로 구축하기 위해 많은 고민을 했습니다.

  • 바닐라 첨가 문서
  • 바닐라 추출에 관한 마크의 연설
  • 바닐라 추출 불협화음
  • CodeSandbox의 코드 예제

댓글