😎세가지를 조합하면 아주 멋진 것을 만들 수 있다.
https://xionwcfm.tistory.com/322
https://xionwcfm.tistory.com/323
https://xionwcfm.tistory.com/325
지금까지 진행한 세가지 라이브러리에 대한 포스트입니다.
이 세가지를 조합하면 아주 멋있고 유용한 CSS를 작성할 수 있습니다.
npm install --save clsx
npm install tailwind-merge
npm install class-variance-authority
우선 tailwind css는 설치되어있다고 가정하고
위 라이브러리들을 설치해줍니다.
유틸함수를 만들어 주겠습니다.
import { ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export const cn = (...inputs: ClassValue[]) => {
return twMerge(clsx(inputs));
};
이 유틸함수는 tailwind를 merge할 때 발생할 수 있는 클래스 충돌 문제를 해결해줍니다.
왜 필요한지는 cva만 단독으로 사용하시다보면 자연스럽게 깨닫게되니
처음에는 사용하지 않는 것도 추천드립니다.
import { ButtonHTMLAttributes, Children, FC } from 'react';
import { cva, VariantProps } from 'class-variance-authority';
import { cn } from '@/utils/cn';
export const ButtonVariants = cva(
`
flex justify-center items-center active:scale-95 rounded-xl
text-sm font-bold text-slate-100 transition-all shadow-md
hover:scale-105 duration-200
`,
{
variants: {
variant: {
default: ' shadow-none active:scale-100',
grey: ' bg-slate-buttongrey ',
blue: ' bg-accent-blue',
},
size: {
default: '',
md: ' w-[6.875rem] h-[2.375rem] text-[1rem] rounded-md',
lg: 'w-[21.875rem] h-[7.5rem] text-[3rem] rounded-3xl',
wlg: 'w-[24rem] h-[5.25rem] text-[2rem]',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof ButtonVariants> {
label?: string;
children?: React.ReactElement;
}
const Button: FC<ButtonProps> = ({
variant,
size,
children,
label,
...props
}) => {
return (
<button className={cn(ButtonVariants({ variant, size }))} {...props}>
{children && children}
{label && label}
</button>
);
};
export default Button;
다음은 cva와 아까 만들어둔 cn을 이용하여 정의한 유틸 컴포넌트입니다.
variant와 size의 값에 따라 다른 디자인을 보여줄 수 있도록 구성했습니다.
cva의 첫번째 인자에는 모든 경우에 공통으로 들어가게 될 css를 입력하게 됩니다.
그리고 두번째 인자에는 객체를 넣어줍니다.
이 객체에서 위와 같이 각각의 케이스에 따라 정의를 해주시면 되겠습니다.
저는 버튼 내부에 컴포넌트를 넣게되는 경우와 텍스트를 넣는 경우
두개 다 넣는 경우를 고려하여 위와 같이 추가로 인자를 받도록 타입을 작성했습니다.
타입을 작성할 때에는 HTMLElement와 우리가 작성한 cva 함수의 반환값에 해당하는 타입을 넣어주면 됩니다.
VariantProps는 cva가 제공하는 타입입니다.
이 타입의 제네릭으로 우리가 작성한 함수의 반환값을 넣어주면 되는것이지요
props로는 위에서 정의한 것들을 받고
유틸함수 cn으로 cva 함수의 반환값인 함수를 호출해줍니다.
이때 이 함수의 인자에 유동적으로 값을 넣어주는 것을 통해 디자인을 다양하게 구성해줄 수 있는것이지요
import Button from '@/components/ui/Button';
import MoonIcon from '@/assets/icons/MoonIcon';
export default function Home() {
return (
<main>
<Button variant="grey" size={'lg'} label={'Try Again'}></Button>
<Button variant={'grey'} size={'md'} label={'장바구니'}></Button>
<Button variant={'blue'} size={'wlg'} label={'회원가입'}></Button>
<Button>
<MoonIcon />
</Button>
</main>
);
}
다음은 실제 사용사례입니다.
위와 같이 각 요소들을 조합하는 것을 통해 다양한 디자인을 만들 수 있어졌습니다.
아주 유용하게 재사용할 수 있겠죠?
거기다가 이 cva는 스토리북과의 호환성도 아주 좋습니다.
import { cva, VariantProps } from 'class-variance-authority';
import { HTMLAttributes } from 'react';
import { cn } from '../../utils/cn';
const FooterVariants = cva(
'py-3 text-xs text-lightgray border-lightgray border-t flex justify-center items-center flex-col',
{
variants: {
variant: {
primary: '',
secondary: '',
},
weight: {
default: ' font-thin',
sm: 'font-normal',
lg: 'font-extrabold',
},
},
defaultVariants: {
variant: 'primary',
weight: 'default',
},
},
);
export interface FooterProps
extends HTMLAttributes<HTMLElement>,
VariantProps<typeof FooterVariants> {}
const Footer = ({ variant, weight }: FooterProps) => {
return (
// <footer className="fcc flex-col border-t border-lightgray py-3 text-xs font-thin text-lightgray">
<footer
className={cn(FooterVariants({ variant: variant, weight: weight }))}
>
<div className=" flex items-center justify-center text-center">
<span>개인정보 처리방침</span>
<span>이용약관</span>
</div>
<p>All rights reserved @ Codestates</p>
</footer>
);
};
export default Footer;
import type { Meta, StoryObj } from '@storybook/react';
import Footer from '../components/layouts/Footer';
const meta = {
title: 'Example/Footer',
component: Footer,
tags: ['autodocs'],
} satisfies Meta<typeof Footer>;
export default meta;
export const PrimaryFooter: StoryObj<typeof Footer> = {
args: {
weight: 'lg',
variant: 'primary',
},
};
export const NotPrimaryFooter: StoryObj<typeof Footer> = {
args: {
weight: 'sm',
variant: 'secondary',
},
};
다음 코드는 Footer에 대하여 cva로 css를 작성하고 이를 스토리북으로 사용한 예제입니다.
prop 으로 받는 값을 통해 다른 디자인을 보여주는 cva의 패턴은
스토리북과도 아주 잘 맞는 것을 확인할 수 있어요!
위 코드를 스토리북으로 실행시켜보면 이런 형태로 사용을 해볼 수 있답니다.
개인적으로 tailwindcss의 개발경험이 좋다고 생각하지만
그런 와중에도 아쉬운 지점들이 있었는데 (css-in-js의 편리한 조건부렌더 기능과 달리 지저분한 조건부렌더라든지)
그런 부분들을 말끔히 해결하면서 tailwind의 단점인 지저분한 html 문제도
함수화를 통해 깔끔하게 빼낼 수 있다는 장점이 있는 것 같습니다.
여러분들도 많이 사용해보시길 권해드려요!
'css' 카테고리의 다른 글
Radix UI로 Headless UI 맛보기 (1) | 2023.07.21 |
---|---|
왜 emotion같은 css-in-js 프레임워크들은 ssr이 힘든걸까? (4) | 2023.05.26 |
yarnberry, vite, typescript, tailwindcss 환경에서 storybook을 적용하는 방법 (0) | 2023.05.24 |
how to tailwind css cva autocomplete (1) | 2023.05.23 |
clsx 사용법과 twMerge와 함께 유틸함수화 해두기 (1) | 2023.05.21 |