ev
설정 객체 하나로 타입 안전한 스타일 함수를 만들어요.
const button = ev(config);사용법
ev(config)
컴포넌트에 시각적인 축이 둘 이상 있고(size, color, 채움 방식, 상태 같은 것들), 그걸 한곳에 모아 선언하고 싶을 때 ev를 사용하세요. variant 값을 넣어 호출하는 함수를 돌려주고, 그 결과는 Emotion의 css prop에 그대로 넘기면 돼요.
import { ev } from 'emotion-variants';
const button = ev({
base: {
padding: '10px 20px',
borderRadius: 8,
cursor: 'pointer',
},
variants: {
size: {
small: {
fontSize: 12,
},
large: {
fontSize: 16,
},
},
color: {
primary: {
background: 'blue',
color: 'white',
},
secondary: {
background: 'gray',
color: 'white',
},
},
},
defaultVariants: {
size: 'small',
color: 'primary',
},
});설정 객체는 필수예요. 아직 선언할 게 없다면 ev({})를 넘기세요.
파라미터
config.base(CSSObject): 모든 호출에 적용되는 스타일이에요. 호출할 때마다가 아니라ev()가 실행될 때 한 번만 직렬화돼요.config.variants(Record<string, Record<string, CSSObject>>): variant 축이에요. 각 축은 값과 그 값의 스타일을 짝지어요. 값이 객체 키라서 Boolean과 숫자 값은true,false,1처럼 키로 쓰면 되고, 타입은boolean과 숫자 literal로 되돌아와요.config.compoundVariants(Array<{ variants, style }>): 나열한 variant 값이 모두 일치할 때만 적용되는 스타일이에요. 키 이름이style단수인 점을 눈여겨보세요.config.defaultVariants(Partial<Record<Axis, Value>>): 호출하는 쪽에서 축을 생략했을 때 쓰이는 값이에요.
반환 값
(EvFunction<T>): 호출 가능한 객체예요. 호출하면 스타일을 돌려주고, className() · classNames · variants()를 꺼내 쓸 수도 있어요.
button(variants)
variant 함수를 호출하면 그 조합의 스타일을 받아요. 결과는 css prop이나 Emotion의 css()에 넘기세요.
type ButtonProps = NonNullable<Parameters<typeof button>[0]> & {
children: React.ReactNode;
};
function Button({ size, color, children }: ButtonProps) {
return <button css={button({ size, color })}>{children}</button>;
}<Button>defaultVariants가 적용돼요</Button>
<Button size="large" color="primary">Large primary</Button>값은 키 단위로 ??로 결정돼요. 그래서 생략하거나 undefined를 넘기면 defaultVariants로 떨어져요. 위 Button이 선택 prop을 따로 걸러내지 않고 그대로 넘길 수 있는 이유예요. false와 0은 실제 값이라서 그대로 쓰여요.
null도 런타임에서는 기본값으로 떨어지지만, EvVariants 타입에는 null이 없어요. strictNullChecks에서 button({ size: null })은 타입 에러예요. 호출 전에 undefined로 바꿔 주세요.
button({ size: undefined }); // button()과 완전히 같아요prop도 기본값도 없는 축은 아예 건너뛰어요.
파라미터
variants(EvVariants<T>, 선택): variant 값이에요. 모든 키가 선택이에요.
반환 값
(SerializedStyles[]): 일치하는 스타일이 하나뿐일 때도 항상 배열이에요. 순서는 base → 일치하는 variants 항목(축을 선언한 순서) → 일치하는 compoundVariants(배열 순서)예요. CSS가 충돌하면 뒤에 오는 게 이기고, 넘기는 키의 순서는 결과에 영향을 주지 않아요.
button.className(variants)
css prop이 아니라 평범한 class 문자열이 필요할 때 className()을 사용하세요. React가 아닌 환경, className만 받는 외부 컴포넌트, 테스트 단정 같은 경우예요. base와 일치하는 variants, 그리고 compound variants까지 하나의 class로 합쳐 줘요.
<button className={button.className({ size: 'large', color: 'primary' })}>Click me</button>;
// defaultVariants는 button()과 똑같이 적용돼요.
button.className();파라미터
variants(EvVariants<T>, 선택): variant 값이에요. 모든 키가 선택이에요.
반환 값
(string): 조합 전체를 담은 class name 하나를 반환해요.
button.classNames
개별 class name을 모아 둔 조회용 테이블이에요. base 하나와 variant 값마다 하나씩 들어 있어요. class 목록을 직접 조립하는 코드와 함께 쓰거나, 무엇이 생성됐는지 들여다볼 때 편해요.
const button = ev({
base: {
padding: 10,
},
variants: {
size: {
small: {
fontSize: 12,
},
large: {
fontSize: 16,
},
},
outlined: {
true: {
border: '1px solid',
},
},
level: {
1: {
zIndex: 1,
},
},
},
});
button.classNames.base; // 'css-abc123'
button.classNames.variants.size.small; // 'css-def456'키는 값을 문자열로 바꾼 형태예요. 그래서 Boolean과 숫자 축은 true, 1로 읽어요.
button.classNames.variants.outlined.true;
button.classNames.variants.level[1];classNames에 처음 접근할 때까지는 문서에 아무것도 주입되지 않고, 결과는 memoize돼요. css prop만 쓰는 코드는 이 비용을 전혀 치르지 않아요.
button.classNames === button.classNames; // trueWARNING
classNames는 base와 단일 variant 값만 다뤄요. compound variants는 여기에 들어 있지 않아요. 조합에 대응하는 키가 없어요. compound 스타일이 필요하면 className()을 사용하세요.
이 class들을 이어 붙이는 건 함수를 호출하는 것과 달라요. 각각이 specificity가 같은 별개의 규칙이라서, base와 variant가 같은 property를 지정하면 나열한 순서가 아니라 Emotion이 먼저 주입한 쪽이 이겨요. variant가 base를 이겨야 한다면 className()을 쓰세요.
설정에 base가 없으면 base는 빈 문자열이에요.
반환 값
(EvClassNames<T>): { base, variants } 형태예요. variants[축][값]이 class name이에요.
button.variants()
축 이름을 선언한 순서대로 반환해요. Storybook argTypes나 props 표를 만들 때 쓸 만해요.
const button = ev({
variants: {
size: {
small: {},
large: {},
},
color: {
primary: {},
secondary: {},
},
},
});
button.variants(); // ['size', 'color']variants가 없는 설정은 []를 반환해요.
반환 값
(Array<VariantKeys<T>>): 축 이름이에요. 호출할 때마다 같은 배열 instance를 돌려주니 읽기 전용으로 다뤄 주세요.
사용해보기
variant 값을 바꿔 보면서 버튼이 어떻게 달라지는지 확인해 보세요.
타입
EvConfig, EvFunction, EvVariants 등 export되는 타입 정의는 타입에서 확인하세요.
@emotion/css와 함께 쓸 때
className()과 classNames는 @emotion/css를 거쳐요. @emotion/css는 @emotion/react와 별개인 자체 global cache를 갖고 있어요. 그래서 CacheProvider의 key나 nonce 같은 옵션을 무시하고, 서버 렌더링에서 이 스타일을 뽑아내려면 @emotion/server가 필요해요.
같은 엘리먼트에 css={button(...)}와 className={button.className(...)}를 함께 쓰는 건 피하세요. 두 cache는 주입 순서를 공유하지 않아서, 덮어쓰기가 예상대로 동작하지 않을 수 있어요.
서버 렌더링에서는 뽑아내야 할 cache가 하나가 아니라 둘이라는 뜻이에요. @emotion/react의 cache만 모으면 className()과 classNames가 만든 규칙이 빠진 마크업이 나가서, hydration 전까지 스타일이 없는 화면이 보여요.