과거의 웹 개발에서는 HTML 파일과 CSS 파일이 엄격히 분리되어 있었다. 하지만 프로젝트가 커지면서 기존 CSS 방식(전역 스타일링)의 단점이 드러났다.
위의 문제를 해결하기 위해 스타일을 자바스크립트 컴포넌트 안에 함께 관리하자는 취지로 CSS-in-JS가 등장했다. (ex. Styled-components, Emotion)
<style> 태그를 만들어 DOM에 삽입 - 유저가 페이지를 렌더링할 때마다 계산 비용이 발생하므로, 복잡한 앱일수록 렌덜이 속도가 느려짐recipe 기능이 내장되어 있어, 일관된 디자인 시스템을 구축하고 유지보수하기에 최적화1. 번들러 플러그인의 개입
.css.ts 파일을 찾아 처리한다.2. 빌드 타임에 .css.ts 코드 평가
.css.ts 파일을 브라우저가 아닌 빌드 환경에서 미리 평가한다.style, recipe, globalStyle 등으로 작성한 스타일 정의가 실행된다.3. CSS 변환 및 정적 파일 추출
.button_d6y21).css 파일로 추출된다.4. JavaScript 코드에는 클래스명 참조만 남김
5. 단, recipe는 최소한의 런타임 로직이 남을 수 있음
style()은 보통 클래스명 문자열만 남는다.recipe()처럼 variant에 따라 클래스를 조합해야 하는 경우에는, 선택한 variant에 맞는 클래스명을 반환하는 작은 함수 로직이 런타임에 남을 수 있다.// 예시 스타일 정의 - Button.css.ts
import { style, createVar, fallback } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';
// 1. createVar: 런타임에 동적으로 바뀔 값을 담을 'CSS 변수 이름표' 생성
export const buttonBackgroundVar = createVar();
// 2. style & fallback: 기본적인 CSS 뼈대
const baseButton = style({
display: 'inline-flex',
alignItems: 'center',
cursor: 'pointer',
border: 'none',
borderRadius: '8px',
color: 'white',
// fallback: CSS 변수가 없으면 기본 색상 사용
backgroundColor: fallback(buttonBackgroundVar, '#2563eb'),
});
// 3. recipe: 디자인 시스템의 핵심, 조건부 스타일(Variants) 관리
export const buttonStyle = recipe({
base: baseButton, // baseButton 스타일을 상속받음
variants: {
size: {
small: {
padding: '8px 12px',
fontSize: '14px',
},
large: {
padding: '16px 24px',
fontSize: '18px',
},
},
},
defaultVariants: {
size: 'small',
},
});
// 예시 컴포넌트 - Button.tsx
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { buttonStyle, buttonBackgroundVar } from './Button.css';
interface ButtonProps {
size?: 'small' | 'large';
backgroundColor?: string;
children: React.ReactNode;
}
export const Button = ({
size,
backgroundColor,
children,
}: ButtonProps) => {
return (
<button
// recipe를 호출해 조건에 맞는 클래스명을 반환받음
className={buttonStyle({ size })}
// assignInlineVars: 동적인 값을 vanilla-extract 변수에 안전하게 주입
style={assignInlineVars({
[buttonBackgroundVar]: backgroundColor,
})}
>
{children}
</button>
);
};
const baseButton = style({
display: 'inline-flex',
alignItems: 'center',
cursor: 'pointer',
border: 'none',
borderRadius: '8px',
color: 'white',
backgroundColor: fallback(buttonBackgroundVar, '#2563eb'),
});
style은 하나의 CSS 클래스를 생성하는 API다. 위의 코드는 버튼이 공통으로 가져야 하는 스타일 정의이다. 버튼의 크기, 상태와 관계없이 항상 적용되는 기본 스타일이다.
vanilla-extract는 위 객체를 빌드 타임에 CSS로 변환하고, 고유한 클래스명을 생성한다.
export const buttonBackgroundVar = createVar();
createVar는 vanilla-extract에서 사용할 CSS 변수를 생성하는 API이다. 위의 buttonBackgroundVar는 버튼의 배경색을 외부에서 동적으로 변경할 수 있도록 하는 변수이다.
이는 나중에 값을 넣을 수 있는 CSS 변수 자리를 만들어 두는 역할을 한다. 해시가 포함된 고유한 이름으로 생성되기 때문에, 다른 CSS 변수와 이름이 충돌할 가능성을 줄일 수 있다.
backgroundColor: fallbackVar(buttonBackgroundVar, '#2563eb'),
fallbackVar는 CSS 변수에 값이 없을 때 사용할 기본값을 지칭하는 API이다. 버튼에 buttonBackgroundVar 값이 주입되어 있으면 그 값을 사용하고, 값이 없으면 '#2563eb' 값을 사용한다는 의미이다.
// fallback 값인 "#2563eb" 사용
<Button>기본 버튼</Button>
// 외부에서 색상을 전달하므로 "#ef4444" 사용
<Button backgroundColor="#ef4444">
삭제하기
</Button>
이처럼 createVar와 fallbackVar를 함께 사용하면 기본 스타일은 정적으로 유지하면서, 필요한 값만 런타임에 변경할 수 있다.
export const buttonStyle = recipe({
base: baseButton,
variants: {
size: {
small: {
padding: '8px 12px',
fontSize: '14px',
},
large: {
padding: '16px 24px',
fontSize: '18px',
},
},
},
defaultVariants: {
size: 'small',
},
});
recipe는 여러 variant를 가진 컴포넌트 스타일을 관리할 때 사용한다. 버튼 컴포넌트는 size처럼 정해진 선택지에 따라 스타일이 달라질 수 있다.
size: {
small: {
padding: '8px 12px',
fontSize: '14px',
},
large: {
padding: '16px 24px',
fontSize: '18px',
},
},
size variant는 버튼의 크기를 제어하기 위한 옵션이다. small과 large 값을 가지며, 선택된 값에 따라 padding과 font-size가 달라진다.
base: baseButton,
recipe의 base에는 앞에서 만든 baseButon을 연결한다. 즉, 모든 버튼은 기본적으로 baseButton 스타일을 가지고, 그 위에 size variant 스타일이 추가되는 구조이다.
defaultVariants는 컴포넌트 사용자가 매번 모든 variant 값을 넘기지 않아도 일관된 기본 스타일을 유지할 수 있도록 특정 variant 값이 전달되지 않았을 때 사용할 기본값을 정의한다.
style={assignInlineVars({
[buttonBackgroundVar]: backgroundColor,
})}
assignInlineVars는 createVar로 만든 CSS 변수에 런타임 값을 주입할 때 사용한다. 위의 코드에선 Button 컴포넌트가 backgroundColor prop을 받으면, 그 값을 buttonBackgroundVar에 연결한다.
<Button>기본 버튼</Button>
backgroundColor를 전달하지 않으면 assignInlineVars에서 주입할 값이 없기 때문에 buttonBackgroundVar는 inline style에 추가되지 않는다. 그러면 CSS 변수 값이 없으므로 fallback 값인 '#2563eb'가 적용된다.
정리하면, fallbackVar는 값이 없을 때 사용할 기본값을 CSS에 정의하고, assignInlineVars는 값이 있을 때 그 기본값을 덮어쓸 런타임 값을 주입한다.
| API | 역할 | 실행 시점 |
|---|---|---|
createVar() | 고유한 CSS 변수 이름 생성 | 빌드 타임 |
fallback() | CSS 변수 값이 없을 때 기본값 지정 | 브라우저 CSS 계산 시 |
assignInlineVars() | 런타임에서 CSS 변수에 값 할당 | 런타임 |
recipe() | variant에 맞는 클래스명 조합 | 런타임 |
style() | 정적 CSS 클래스 생성 | 빌드 타임 |