최근 개인 프로젝트에서 Vanilla Extract 를 사용할 일이 생겨 부랴부랴 조사했습니다.
Nexters 24기 프로젝트를 시작한 후, 같은 팀의 프론트엔드 개발자 분과 회의를 거쳐 스타일링 라이브러리를 Vanilla Extract 로 정했다. 처음에는 내가 애용하는 tailwindCSS 를 제안했으나 상대 분이 Atomic CSS 에 강렬한 반대를 표하셔서 어쩔 수 없이 (...) CSS - in - JS를 사용해야 했다.
Emotion, styled-components 등 이미 유명하고 익숙한 선택지도 있었지만 내가 이 라이브러리를 추천한 이유는 나에게 프론트엔드를 알려준 친구가 추천을 해주기도 했고, 빌드 타임에서 CSS를 생성함으로서 얻을 수 있는 여러 이점들이 크다고 생각했다. 그리고 새로운 기술을 좀 써보고 싶기도 했다.
하지만 정작 나도 이 라이브러리를 한번도 써본적이 없어서 나름의 공부를 시작했고 그 결과를 글로서 정리하고자 한다.
sprinkle
라이브러리를 사용하여 Atomic CSS 를 사용할 수 있다.recipe
라이브러리를 사용하여 Variant 기반의 스타일링을 구성할 수 있다.❓ 왜 굳이 CSSOM 에 스타일을 추가하는 방식을 사용하는가?
CSSStyleSheet.insertRule()
메서드를 사용하여 현재 스타일시트에 새로운 스타일을 삽입하는 방식을 쓴다.<style>
태그를 주입하여 스타일을 수정하는 방식을 쓴다. (DOM 수정)ServerStyleSheet.collectStyles
메서드로 이를 구현한다.<style>
태그로 묶어 삽입하는 방법.extractCritical
메서드를 통해 필요한 CSS 를 사전에 추출한다.React.renderToString
) 이 진행될 때 스타일시트를 수집하고 이를 주입하는 형식이다.renderToPipeableStream
) 에 대해서도 지원을 시작하고 있지만 아직 여러 이슈가 있는 것으로 파악되었다.<link>
태그를 기반으로 브라우저에 제공된다.개발 중인 프로젝트의 상황에 따라서 적절한 것을 도입하는 게 좋다.
IE
에 국한된 문제기 때문에 이제는 딱히 의미가 없다. dynamic
모듈로 런타임 과정에서 CSS Variable 을 생성할 수 있다.Vanilla Extract 플러그인의 대략적인 사용 방법에 대해서 정리하였다.
스타일링 방식
style
함수 내에 스타일 객체을 추가한다.className
으로 변환되어 만들어진다.import { style } from '@vanilla-extract/css';
export const myStyle = style({
display: 'flex',
paddingTop: '3px'
});
export const testStyle = style({
display: 'flex',
flexDirection: 'column'
})
export const mergeStyle = style([
testStyle, {
justifyContent: 'space-around',
gap: '0px 8px'
}
])
Atomic CSS 부분 제공
sprinkles
라이브러리를 통해 CSS token 을 생성할 수 있다.defineProperties
함수 내 객체에 정의한다.createSprinkles
함수를 통해 유틸 함수 sprinkles
를 생성하고 이를 내보낸다.import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
const colors = {
'blue-50': '#eff6ff',
'blue-100': '#dbeafe',
'blue-200': '#bfdbfe',
'gray-700': '#374151',
'gray-800': '#1f2937',
'gray-900': '#111827'
// etc.
};
const colorProperties = defineProperties({
conditions: {
lightMode: {},
darkMode: { '@media': '(prefers-color-scheme: dark)' }
},
defaultCondition: 'lightMode',
properties: {
color: colors,
background: colors
}
});
export type Sprinkles = Parameters<typeof sprinkles>[0];
export const sprinkles = createSprinkles(
colorProperties
);
sprinkles
함수를 호출하고, 인자로 지정한 property
에 맞는 값을 적용한다.import { style } from '@vanilla-extract/css';
import { sprinkles } from './sprinkles.css.ts';
export const container = style([
sprinkles({
color: 'gray-800',
background: 'blue-50'
}),
{
':hover': {
outline: '2px solid currentColor'
}
}
]);
Variant 기반의 스타일링 시스템 제공
recipe
라이브러리를 활용하여 Variant 기반의 스타일링 시스템 제공import { recipe } from '@vanilla-extract/recipes';
export const button = recipe({
base: {
borderRadius: 6
},
variants: {
color: {
neutral: { background: 'whitesmoke' },
brand: { background: 'blueviolet' },
accent: { background: 'slateblue' }
},
size: {
small: { padding: 12 },
medium: { padding: 16 },
large: { padding: 24 }
},
rounded: {
true: { borderRadius: 999 }
}
},
RecipeVariants
유틸 타입을 활용하면 더 좋다.import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react';
import type { RecipeVariants } from '@vanilla-extract/recipes';
import * as styles from './style.css';
// RecipeVariants 에 의해 style.css.ts 에 정의한 button Recipe 타입 추론
type ButtonProps = RecipeVariants<typeof styles.button> &
PropsWithChildren<ComponentPropsWithoutRef<'button'>>;
const Button = ({
children,
color,
size,
rounded,
type = 'button',
}: ButtonProps) => (
<button className={styles.button({ color, size, rounded })}>
{children}
</button>
);