Tailwind CSS 잘 활용하기 with Clxs, CVA, twMerge
최근 팀 내에서 Tailwind CSS Class를 사용할 때 좀 더 효율성 있게 작성하는 방식에 대해 논의하게 되었습니다. 기존 작업 중인 프로젝트에서는 Styled Component와 Tailwind CSS를 같이 쓰는 방식을 채택하여 사용 중인데 새롭게 들어가는 프로젝트에선 clsx, class-variance-authority, tailwind-merge를 조합하여 사용하는 방식을 사용해 보려고 합니다.
필요 라이브러리
※ React에서 Tailwind CSS 설정이 되어 있는 환경을 가정합니다.
clsx는 조건부 className 처리를 위해 사용되며 라이브러리 크기가 매우 작은 장점을 가지고 있습니다.
import { clsx } from 'clsx';
clsx('w-full', true && 'h-full', { 'px-4': true, 'py-2': false }, [
'flex',
'items-center',
]);
// => w-full h-full px-4 flex items-center
cva(class-variance-authority)는 props에 따른 className 처리를 위해 사용됩니다.
import { cva } from 'class-variance-authority';
export const ComponentVariants = cva(
`
flex items-center justify-center
`,
{
variants: {
variant: {
default: '',
full: 'w-full h-full',
},
},
defaultVariants: {
variant: 'default',
},
},
);
// <div className={ComponentVariants({ variant })} />
tailwind-merge는 className 충돌을 막아주어 겹치는 className의 경우 마지막에 선언된 className 기준 오버라이드 처리됩니다.
import { twMerge } from 'tailwind-merge'
twMerge('px-2 py-1 bg-red-600 p-3 bg-grey-800')
// => p-3 bg-grey-800
npm install clsx class-variance-authority tailwind-merge --dev
# or
yarn add clsx class-variance-authority tailwind-merge --dev
util 함수 작성
clsx, class-variance-authority, tailwind-merge를 조합하여 사용하기 위해 유틸함수를 만듭니다.
// utils/style.ts
import { ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export const cn = (...inputs: ClassValue[]) => {
return twMerge(clsx(inputs));
};
clsx와 tailwind-merge를 통해 조건부 className과 className 충돌을 방지하는 유틸함수 입니다.
cva와 작성된 유틸함수 cn을 활용한 Button 예제입니다.
// components/Button.tsx
import { cva, VariantProps } from 'class-variance-authority';
import { cn } from '@/utils/style';
export const ButtonVariants = cva(
`
flex items-center justify-center
bg-gray-950 text-white
rounded-sm text-base font-medium
`,
{
variants: {
variant: {
default: 'shadow-none',
grey: 'bg-gray-150 text-gray-950',
red: 'bg-red-600',
},
size: {
default: 'px-2 py-1',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
xl: 'px-8 py-4 text-xl',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
interface ButtonProps extends VariantProps<typeof ButtonVariants> {
className?: string;
children?: React.ReactNode;
}
function Button({ variant, size, className, children }: ButtonProps) {
return (
<button className={cn(ButtonVariants({ variant, size, className }))}>
{children && children}
</button>
);
}
export default Button;
cva에서는 선언된 variants 외에도 className을 추가로 받을 수 있습니다. 추가할 className이 없다면 이 부분은 생략하셔도 됩니다.
작성된 Button Component 사용 예제입니다.
import Button from '@/components/Button';
function Component() {
return (
<div className="w-full h-full p-10 space-y-2">
<Button size="md">btn1</Button>
<Button className="text-red-600" variant="grey" size="lg">
btn2
</Button>
<Button variant="red" size="xl">
btn3
</Button>
</div>
);
}
export default Component;
VSCode 추가 설정
VSCode에서 Tailwind CSS IntelliSense extension을 사용할 경우 cva 내부에서 작동을 안 하는 것을 확인할 수 있습니다. cva 내부에서 작동할 수 있도록 setting.json에 해당 항목을 추가합니다.
{
...
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}
마무리
Tailwind CSS와 같이 사용할 수 있는 라이브러리 덕분에 Tailwind CSS를 잘 활용할 수 있는 방법이 계속 나오고 있습니다. 이전에는 Styled Components와 Tailwind CSS를 같이 쓰는 방식을 포스팅했었는데 작업 환경에 맞게 여러 방식을 선택하여 사용하면 되겠습니다. 단순히 Tailwind CSS를 쓰는 것에 그치지 않고 좀 더 활용할 수 있는 방안을 계속 찾아봐야겠습니다.
참고 자료
https://xionwcfm.tistory.com/328
https://xionwcfm.tistory.com/325