Vanilla Extract 공부하기 - Styling

주예·2024년 5월 29일

CSS

목록 보기
1/1

이번 포스팅은 회사 DS에서 사용하게 될 vanilla-extract를 공부하기위해 남기는 기록용이다.
주로 Docs를 번역하는 위주로 작성되며, 오역이 있을 수 있다.
추가적으로 공부 기록용이기때문에 내가 이해하기 쉽게 적혀있다는 점!

Vanilla Extract란?

vanilla-extract는 전처기로 타입스크립트를 사용할 수 있게 해주는 Zero-runtime stylesheet입니다.
크게 4가지의 장점을 다음과 같습니다.

  1. Type-safe static CSS
    Sass나 LESS처럼 빌드타임때 모든 스타일이 생성된다는 공통점을 가지고 있지만, vanilla-extract는 이 과정을 타입스크립트와 함께합니다.
  2. First-class theming
    하나의 글로벌 테마 혹은 여러가지 테마를 만들 때, 타입이 안전한 토큰을 사용하여 만들어줍니다.
  3. Framwork agnositc
    공식적으로 webpack, esbuild, vite 그리고 Next.js를 지원합니다.
  4. Built for extension
    Sprinkles, Recipes 그리고 Dessert Box (혹은 스스로 만든) 라이브러리들을 사용할 수 있습니다.

Getting Started

Create a style

vanilla-extract는 TS in CSS 이기때문에, .css.ts로 파일을 생성해야합니다.
그리고 생성 후에는 1. 지역 스코프를 가진 클래스를 생성하고, 2. 생성한 클래스 이름을 export 해야합니다.

import { style } from '@vanilla-extract/css';

export const container = style({
	padding: 10
});

Apply the style

각 요소에 스타일을 적용시키기 위해서는, 우리가 만들었던 클래스 네임을 class 속성에 제공해줘야합니다.
👀 일단 나는 React기때문에 className 속성으로 제공해준다고 생각했다!

import { container } from './style.css.ts';

const Box = () => {
	return(
    	<div className={container}>
      		...
      	</div>
    )
}

import 방식의 사이드 이펙트로서, CSS가 선택된 번들러 통합 과정에서 또한 처리될 것이라는 점 입니다.
👀 이건 솔직히 무슨 말인지 잘 모르겠다! 추가로 알아볼 필요가 있는 듯!

Styling

vanilla extract에서의 모든 스타일링 API는 스타일 객체를 input으로서 사용합니다.
스타일을 JavaScript 개체로 설명하면 스타일이 나머지 애플리케이션 코드와 마찬가지로 형식화된 데이터 구조이므로 스타일 코드를 통해 TypeScript를 훨씬 더 잘 사용할 수 있습니다.
이것은 또한 타입 안정성 그리고 CSS 자동완성까지 이어집니다.

CSS Properties

스타일 객체의 최상위 레벨에, CSS Properties는 보통 CSS class를 작성할 때 처럼 작성하면 됩니다.
유일한 차이점은 모든 properties가 kebab-case가 아닌 camelCase를 사용한다는 점 입니다.

import { style, globalStyle } from '@vanilla-extract/css';

export const myStyle = style({
  display: 'flex',
  paddingTop: '3px' // ⭐️ camelCase로 적힌 속성!
});

globalStyle('body', {
  margin: 0
});

Unitless Properties

몇몇 properties는 value로서 숫자를 허용합니다. unitless properties를 제외하고, 숫자로 받은 값은 pixel로 간주되어 숫자로 받은 값에 자동으로 px를 넣어서 인식합니다.

import { style } from '@vanilla-extract/css';

export const myStyle = style({
  // cast to pixels >> 숫자로 입력했지만 자동으로 px이라고 인식
  padding: 10,
  marginTop: 25,

  // unitless properties >> 단위가 필요없는 속성
  flexGrow: 1,
  opacity: 0.5
});

Vendor Prefixes

만약 -webkit-tap-highlight-color 처럼 특정 vendor prefixes를 사용하고 싶다면, -를 삭제하고PascalCase를 사용합니다.

import { style } from '@vanilla-extract/css';

export const myStyle = style({
  // -webkit-tap-highlight-color
  WebkitTapHighlightColor: 'rgba(0, 0, 0, 0)'
});

CSS Variables

보통 CSS에서 변수들은 규칙 내 다른 속성들과 함께 사용할 수 있게 되어있었습니다.
vanilla extract에서 CSS 변수들은 vars 라는 키워드 내에서 중첩되어서 사용됩니다.
해당 키워드(vars)는 다른 CSS 속성에 대해 보다 정확한 정적 유형 지정을 제공합니다.

import { style } from '@vanilla-extract/css';

const myStyle = style({
  vars: {
    '--my-global-variable': 'purple'
  }
});

또한 varscreateVar API를 통해 생성한 범위가 지정된 CSS 변수도 허용합니다.

import { style, createVar } from '@vanilla-extract/css';

const myVar = createVar();

const myStyle = style({
  vars: {
    [myVar]: 'purple'
  }
});

createVar
single scoped CSS 변수 참조를 생성하는 API 입니다.

import { createVar, style } from '@vanilla-extract/css';
export const accentVar = createVar();

보시다싶이 createVar로 우리가 변수를 생성할 때 어떤 CSS도 생성되지 않습니다. 오직 나중에 사용할 수 있게 참조만 생성하는 것 입니다.
참조를 생성한 후에 변수를 세팅하기 위해서는 vars 키워드를 사용해야합니다.

import { createVar, style } from '@vanilla-extract/css';
export const accentVar = createVar();
export const blue = style({
  vars: {[accentVar]: 'blue'}
});
export const pink = style({
  vars: {[accentVar]: 'pink'}
});

기억해둬야할 것은 변수의 값은 다른 클래스 혹은 심지어 미디어 쿼리 안에서도 바뀔 수 있다는 것 입니다.

Media Queries

보통 CSS와는 다르게 Vanilla Extract는 @media 키워드를 사용하여 스타일 정의 내에 미디어 쿼리를 삽입할 수 있습니다. 이를 통해 스타일의 반응형 규칙을 단일 데이터 구조에 쉽게 배치할 수 있습니다.

import { style } from '@vanilla-extract/css';

const myStyle = style({
  '@media': {
    'screen and (min-width: 768px)': {
      padding: 10
    },
    '(prefers-reduced-motion)': {
      transitionProperty: 'color'
    }
  }
});

👀 개인적으로 조금 헷갈려서 기본 CSS와 비교하자면, vanilla extract는 @media를 키워드 안에 각각에 대한 반응형 조건 및 스타일링이 작성되어 있어 좀 더 알기쉬운 느낌?!

@media screen and (min-width: 768px) {
  .styles_myStyle__1hiof570 {
    padding: 10px;
  }
}
@media (prefers-reduced-motion) {
  .styles_myStyle__1hiof570 {
    transition-property: color;
  }
}

코드를 CSS로 처리할 때, vanilla extract는 항상 미디어 쿼리를 파일 제일 하단으로 내려서 처리할 것입니다.
이것은 즉, @media 안에 있는 스타일이 CSS 규칙 순서에 의해 다른 스타일들보다 우선순위가 높다는 것을 의미합니다.

Selectors

주어진 스타일을 selector를 지정하는 방식은 두가지가 있는데, 1. 다른 CSS properties들과 함께 사용할 수 있는 simple pseudo selector 2. 훨씬 복잡한 규칙을 허용하는 selectors 옵션이 있습니다.

globalStyle에는 모든 선택기를 사용할 수 없습니다. 이 API는 selector를 첫 번째 매개변수로 허용합니다(예: ul li:first-of-type, a > span). 선택기를 병합하면 예상치 못한 결과가 발생할 수 있습니다.

Simple Pseudo Selectors

👀 우리가 흔히알고있는 가상선택자이다!
Simple Pseudo Selectors는 어떠한 파라미터도 받지 않기때문에 쉽게 찾고 정적으로 입력해서 사용할 수 있습니다. 이는 다른 CSS 속성과 함께 최상위 레벨에서 사용할 수 있으며, CSS 속성 및 CSS 변수만 포함할 수 있습니다.

import { style } from '@vanilla-extract/css';

const myStyle = style({
  ':hover': {
    color: 'pink'
  },
  ':first-of-type': {
    color: 'blue'
  },
  '::before': {
    content: ''
  }
});

Complex Selectors

더 복잡한 규칙을 작성하기 위해서는 selectors 키워드를 사용해야 합니다.
유지보수성을 향상시키기 위해서 각 스타일 블락은 오직 하나의 요소만 집중해야합니다.
이를 강화하려면, 모든 selector가 현재 요소에 대한 참조인 & 문자를 대상으로 해야 합니다.

import { style } from '@vanilla-extract/css';

const link = style({
  selectors: {
    '&:hover:not(:active)': {
      border: '2px solid aquamarine'
    },
    'nav li > &': {
      textDecoration: 'underline'
    }
  }
});

selectors는 또한 다른 스코프에 있는 클래스명도 참조할 수 있습니다.

import { style } from '@vanilla-extract/css';

export const parent = style({});

export const child = style({
  selectors: {
    [`${parent}:focus &`]: {
      background: '#fafafa'
    }
  }
});

유효하지 않은 선택자는 현재 클래스가 아닌 요소를 대상으로 삼으려고 시도하는 선택자입니다.

import { style } from '@vanilla-extract/css';

const invalid = style({
  selectors: {
    // ❌ ERROR: Targetting `a[href]`
    '& a[href]': {...},

    // ❌ ERROR: Targetting `.otherClass`
    '& ~ div > .otherClass': {...}
  }
});

다른 범위의 클래스를 대상으로 지정하려면 대신 해당 클래스의 스타일 블록 내에 정의해야 합니다.

import { style } from '@vanilla-extract/css';

// Invalid example:
export const child = style({});
export const parent = style({
  selectors: {
    // ❌ ERROR: Targetting `child` from `parent`
    [`& ${child}`]: {...}
  }
});

// Valid example:
export const parent = style({});
export const child = style({
  selectors: {
    [`${parent} &`]: {...}
  }
});

👀 무조건 결국 본인에 대한 스타일링 정의를 해야한다! 그래서 허용되는 범위를 보면 끝이 다 &이다!!

현재 요소(예: '& a[href]') 내의 하위 노드를 전역적으로 대상으로 지정해야 하는 경우 대신 globalStyle을 사용해야 합니다.

import { style, globalStyle } from '@vanilla-extract/css';

export const parent = style({});

globalStyle(`${parent} a[href]`, {
  color: 'pink'
});

Circular Selectors

만약 selector가 서로서로 의존적이라면 (순환참조), getter를 사용하면됩니다.

import { style } from '@vanilla-extract/css';

export const child = style({
  background: 'blue',
  get selectors() {
    return {
      [`${parent} &`]: {
        color: 'red'
      }
    };
  }
});

export const parent = style({
  background: 'yellow',
  selectors: {
    [`&:has(${child})`]: {
      padding: 10
    }
  }
});

Container Queries

Container Queries는 media queries와 똑같이 작동하고 @container 키 안에서 중첩되서 사용합니다.
대상 브라우저가 컨테이너 쿼리를 지원하는지 확인해야합니다. 바닐라 추출은 컨테이너 쿼리 구문을 지원하지만 지원되지 않는 브라우저에서는 기능을 폴리필하지 않습니다.

import { style } from '@vanilla-extract/css';

const myStyle = style({
  '@container': {
    '(min-width: 768px)': {
      padding: 10
    }
  }
});

createContainer를 사용해서도 범위가 지정된 container를 생성할 수 있습니다.

import {
  style,
  createContainer
} from '@vanilla-extract/css';

const sidebar = createContainer();

const myStyle = style({
  containerName: sidebar,
  '@container': {
    [`${sidebar} (min-width: 768px)`]: {
      padding: 10
    }
  }
});

createContainer
CSS Container Queries에서 사용할 단일 스코프 컨테이너명을 만들어주는 API입니다.
이렇게 하면 다른 컨테이너와의 잠재적인 이름 충돌을 방지할 수 있습니다.

Layers

위의 미디어 쿼리와 마찬가지로 Vanilla Extract를 사용하면 스타일 정의 내에서 @layer 키를 사용하여 레이어에 스타일을 할당할 수 있습니다.

import { style } from '@vanilla-extract/css';

const text = style({
  '@layer': {
    typography: {
      fontSize: '1rem'
    }
  }
});

@layer 키 또한 lyaer API를 통해 범위가 지정된 layer 참조를 만들 수 있습니다.

import { style, layer } from '@vanilla-extract/css';

const typography = layer();

const text = style({
  '@layer': {
    [typography]: {
      fontSize: '1rem'
    }
  }
});

더 많은 layer들을 관리하기 위해서는 layerglobalLayer API를 확인해야합니다.

Supports Queries

Supports Queries는 Media Queries와 동일하게 동작하고 @supports 키 안에서 중첩되서 사용해야합니다.

import { style } from '@vanilla-extract/css';

const myStyle = style({
  '@supports': {
    '(display: grid)': {
      display: 'grid'
    }
  }
});

Fallback Styles

브라우저에서 제공해주지 않는 CSS 속성을 사용해야할 때, 우리는 종종 속성을 두번씩 작성하고 오래된 브라우저는 이해하지 못한 속성을 무시하곤합니다.
하지만 JS 객체에서는 같은 키를 두번 작성할 수 없습니다.
대신에 우리는 fallback value를 정의하는 배열을 사용합니다.

import { style } from '@vanilla-extract/css';

export const myStyle = style({
  // In Firefox and IE the "overflow: overlay" will be
  // ignored and the "overflow: auto" will be applied
  overflow: ['auto', 'overlay']
});

0개의 댓글