css

cva, tailwindmerge, clsx를 조합하여 재사용 가능한 UI 만들기

냠냠맨 2023. 5. 25. 19:22

😎세가지를 조합하면 아주 멋진 것을 만들 수 있다.

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 문제도

함수화를 통해 깔끔하게 빼낼 수 있다는 장점이 있는 것 같습니다.

 

여러분들도 많이 사용해보시길 권해드려요!

 

반응형