💡 시작하며
현재 사이드 프로젝트를 진행하고 있는데, 프론트엔드 CSS 아키텍처를 선택할 때 Vanilla-Extract를 사용하기로 해서 사용 방법을 공부하고 정리해두려고 한다.
👍🏻 Vanilla Extract 장점
우선 Vanilla Extract는 아래와 같은 장점이 있다.
- 빌드타임에 ts 파일을 css 파일로 만든다. (CSS Modules-in-TyepScript)
- type-safe하게 theme을 다룰 수 있다.
- 프론트엔드 프레임워크에 구애받지 않는다.
- Tailwind처럼 Atomic CSS를 구성할 수 있다.
- Sttitches 처럼 variant 기반 스타일링을 구성할 수 있다.
⚙️ Vanilla Extract 사용 방법
0️⃣ 설치
vanilla-extract를 설치한다.
npm install @vanilla-extract/css
1️⃣ 번들러 설정
build 타임에 css 파일로 변환되고 head 태그에 삽입되기 때문에 bundle 설정은 필수이다. 우리 프로젝트는 Next.js를 사용하고 있어서 bundler-integration에서 Next.js 가이드를 따라 설정했다.
우선 next-plugin 설치를 해준 뒤, next.config.js 에 플러그인을 추가한다.
npm install --save-dev @vanilla-extract/next-plugin
// next.config.js
const {
createVanillaExtractPlugin
} = require('@vanilla-extract/next-plugin');
const withVanillaExtract = createVanillaExtractPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = withVanillaExtract(nextConfig);
2️⃣ 기본 스타일 만들기
container는 하나의 로컬 범위 클래스를 만들고 export한다. 10 뒤에 생략되는 단위는 'px'이다.
import { style } from '@vanilla-extract/css';
export const container = style({
padding: 10
});
이 스타일을 사용하려면 다음과 같이 하면 된다.
import { container } from './app.css.ts';
document.write(`
<section class="${container}">
...
</section>
`);
3️⃣ Utility Style, CSS Variable
하나의 Wrapper를 만들고 싶다면 이렇게 하나의 공통 스타일로 만들어서 유틸로 사용할 수 있다. 주의할 점은 PascalCase를 사용하여 작성해야 한다.
export const flexCenterWrapper = style({
padding: 24,
display: 'center',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
});
또한 CSS Variable을 만들고 싶다면 createVar를 사용한다.
import { style, createVar } from '@vanilla-extract/css';
const myVar = createVar();
const myStyle = style({
vars: {
[myVar]: 'purple'
}
});
4️⃣ Media / Container / Layer / Support Query 만들기
Media Query: 스타일 정의 내에 미디어 쿼리를 포함할 수 있다. 이렇게 반응형에 따라 단일 데이터 구조에 쉽게 함께 구성할 수 있다.
import { style } from '@vanilla-extract/css';
const myStyle = style({
'@media': {
'screen and (min-width: 768px)': {
padding: 10
},
'(prefers-reduced-motion)': {
transitionProperty: 'color'
}
}
});
Container Query: 컨테이너 쿼리는 미디어 쿼리와 동일하게 작동하며, 키 내부에 중첩된다. 다만 브라우저가 컨테이너 쿼리를 지원하는지 확인해야 한다.
import { style } from '@vanilla-extract/css';
const myStyle = style({
'@container': {
'(min-width: 768px)': {
padding: 10
}
}
});
createContainer를 사용하여 범위가 지정된 컨테이너를 만들 수도 있다.
import {
style,
createContainer
} from '@vanilla-extract/css';
const sidebar = createContainer();
const myStyle = style({
containerName: sidebar,
'@container': {
[`${sidebar} (min-width: 768px)`]: {
padding: 10
}
}
});
Layer Query: 스타일 정의 내의 키를 사용하여 레이어에 스타일을 할당할 수 있다.
import { style } from '@vanilla-extract/css';
const text = style({
'@layer': {
typography: {
fontSize: '1rem'
}
}
});
계층 layer Api를 통해 생성된 범위가 지정된 계층 참조도 허용한다.
import { style, layer } from '@vanilla-extract/css';
const typography = layer();
const text = style({
'@layer': {
[typography]: {
fontSize: '1rem'
}
}
});
Support Query: 키 내부에 중첩되며, 미디어 쿼리와 동일하게 작동한다.
import { style } from '@vanilla-extract/css';
const myStyle = style({
'@supports': {
'(display: grid)': {
display: 'grid'
}
}
});
5️⃣ Selector 지정하기
단순한 선택자는 매개 변수를 사용하지 않으므로 정적으로 입력할 수 있다. 주의할 점은 css 속성 및 css 변수만 포함 가능하다.
import { style } from '@vanilla-extract/css';
const myStyle = style({
':hover': {
color: 'pink'
},
':first-of-type': {
color: 'blue'
},
'::before': {
content: ''
}
});
더 복잡한 선택자는 selectors라는 key를 사용하여 다양한 스타일 규칙을 적용할 수 있다. 단일 블록만 대상으로 지정하여 유지 및 관리가 용이해진다. 이를 적용하려면 현재 요소에 대한 & 문자를 대상으로 지정해야 한다.
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} &`]: {...}
}
});
현재 요소 내의 자식 노드를 전역적으로 대상으로 지정해야 하는 경우 대신 globalStyle을 사용해야 한다. globalStyle에 대해서는 아래에서 다시 소개하겠다.
import { style, globalStyle } from '@vanilla-extract/css';
export const parent = style({});
globalStyle(`${parent} a[href]`, {
color: 'pink'
});
선택자가 서로 의존하는 경우, 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
}
}
});
6️⃣ 대체 스타일
일부 브라우저에서 지원하지 않는 CSS 속성 값을 사용할 때 속성을 두 번 선언하는 경우가 많으며 이전 브라우저는 이해하지 못하는 값을 무시하게 된다. 하지만 여기에서는 동일한 키를 두 번 선언할 수 없다는 점을 주의하자. JS 객체를 사용하지 못하므로 대신 배열을 사용하여 대체 값을 정의해야 한다.
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']
});
7️⃣ Theme - Global Style
Theme은 응용 프로그램 전반에 걸친 글로벌 스타일 규칙으로 사용된다. createTheme을 사용하여 테마를 만들 수 있다. 호출한 뒤 반환하는 값은 테마 클래스 이름과 테마 데이터 구조이다.
import { createTheme } from '@vanilla-extract/css';
export const [themeClass, vars] = createTheme({
color: {
brand: 'blue'
},
font: {
body: 'arial'
}
});
이 테마의 대체 버전을 만들기 위해서는 createTheme을 다시 호출하면 된다. 하지만 이번에는 기존 테마 규칙과 새 값을 전달한다.
import { createTheme } from '@vanilla-extract/css';
export const [themeClass, vars] = createTheme({
color: {
brand: 'blue'
},
font: {
body: 'arial'
}
});
export const otherThemeClass = createTheme(vars, {
color: {
brand: 'red'
},
font: {
body: 'helvetica'
}
});
이렇게 createTheme을 사용하면 편리하지만 몇 가지 장단점이 있다. 테마 규칙을 특정 테마 구현할 때 연결하고, 모든 대체 테마가 원본 테마를 가져와야 한다. 이로 인해 원래 테마의 css도 의도치 않게 가져오게 되어 테마를 코드 분할할 수 없게 된다.
여기에서 createThemeContract를 사용하면 css를 생성하지 않고 규칙을 정의할 수 있다. 아래처럼 작성하면 동일한 규칙을 구현하는 두 테마가 있지만 둘 중 하나를 가져오면 해당 CSS만 가져온다.
import { createThemeContract } from '@vanilla-extract/css';
export const vars = createThemeContract({
color: {
brand: ''
},
font: {
body: ''
}
});
이 규칙을 기반으로 개별 테마를 만들 수 있다. 각 테마는 전체를 채워야 한다.
import { createTheme } from '@vanilla-extract/css';
import { vars } from './contract.css.ts';
export const blueThemeClass = createTheme(vars, {
color: {
brand: 'blue'
},
font: {
body: 'arial'
}
});
import { createTheme } from '@vanilla-extract/css';
import { vars } from './contract.css.ts';
export const redThemeClass = createTheme(vars, {
color: {
brand: 'red'
},
font: {
body: 'helvetica'
}
});
import { createThemeContract } from '@vanilla-extract/css';
export const vars = createThemeContract({
color: {
brand: ''
},
font: {
body: ''
}
}
그렇다면 동적 테마를 만들려면 어떻게 해야 할까? assignInlineVars Api를 사용하면 된다.
npm install @vanilla-extract/dynamic
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { container, themeVars } from './theme.css.ts';
interface ContainerProps {
brandColor: string;
fontFamily: string;
}
const Container = ({
brandColor,
fontFamily
}: ContainerProps) => (
<section
className={container}
style={assignInlineVars(themeVars, {
color: { brand: brandColor },
font: { body: fontFamily }
})}
>
...
</section>
);
const App = () => (
<Container brandColor="pink" fontFamily="Arial">
...
</Container>
);
8️⃣ 스타일 구성
스타일을 구성하고 재사용하가 위해서 단일 클래스 이름인 것처럼 사용할 수 있다.
import { style } from '@vanilla-extract/css';
const base = style({ padding: 12 });
const primary = style([base, { background: 'blue' }]);
const secondary = style([base, { background: 'aqua' }]);
import { style } from '@vanilla-extract/css';
const base = style({ padding: 12 });
const primary = style([base, { background: 'blue' }]);
const text = style({
selectors: {
[`${primary} &`]: {
color: 'white'
}
}
});
여러 기존 클래스가 구성될 때 새 식별자가 생성되어 클래스 목록에 추가된다.
import { style, globalStyle } from '@vanilla-extract/css';
const background = style({ background: 'mintcream' });
const padding = style({ padding: 12 });
// container = 'styles_container__8uideo2'
export const container = style([background, padding]);
globalStyle(`${container} *`, {
boxSizing: 'border-box'
});
✍🏻 마무리
여기까지 기본 사용법을 알아보았고, 그 외에 사용하기 좋은 패키지에는 Sprinkles, Recipes 등이 있다. 실제로 최종적으로 디자인시스템이 완료된다면 다음에는 디자인 시스템 구성 시 vanilla-extract에서 어떤 패키지를 사용했는지, 실제로는 어떤 기능이 유용했는지, 어떤 것에 중점을 두고 개발했는지 등을 정리해 보아야겠다. 항상 '왜?'를 생각하며 적용하자!
'Programming > FrontEnd' 카테고리의 다른 글
[OpenAI] 음악 추천 서비스 만들어보기 (React) 2.5부 (2) | 2023.03.21 |
---|---|
[OpenAI] 음악 추천 서비스 만들어보기 (React) 2부 (2) | 2023.02.22 |
[OpenAI] 음악 추천 서비스 만들어보기 (React) 1부 (0) | 2023.02.18 |
[CSS] Flexbox의 여러 활용법 정리 (0) | 2021.08.31 |
[HTML] <table>의 row index와 cell index 구하는 법 (0) | 2021.07.23 |
댓글