Tailwind를 이용한 효율적인 React Component 관리! tailwind-merge, cva, clsx 파헤치기
Tailwind를 이용한 효율적인 React Component 관리: tailwind-merge, cva, clsx 파헤치기

안녕하세요. 오늘은 Tailwind CSS를 파헤쳐보겠습니다. 저는 부트스트랩으로 처음 CSS 라이브러리에 입문하였고, Tailwind를 사용하게 된 건 홍플릭스 프로젝트를 할 때였어요. 아시다시피 Tailwind는 편리한 만큼 단점이 있죠. 하지만 그에 비해 너무 편하다는 장점이 있었어요, 적어도 제게는 말이죠.
그래서 Tailwind의 공식 문서도 많이 보고, 더 효율적으로 사용할 수 있는 방법을 찾아서 무엇보다도 많이들 사용하는 라이브러리 못지 않게 유연하게 사용할 수 있는 방법을 찾기 위해 다른 분들의 사용 방법도 많이 찾아봤답니다. 결국에 저는 보석 같은 글을 발견하고, 제가 사용하기 편하게 적용해서 개인 프로젝트에도 팀 프로젝트에도 아주 유용하게 사용하고 있습니다.
React 스터디에 어울리는 자료인지는 고민을 많이 했지만, React를 사용하며 Component를 효율적으로 만드는 것은 아주 중요하고 거기엔 또 CSS가 빠져서는 안 되기 때문에! 저처럼 Tailwind가 좋은 분들을 위하여 포스팅 합니다.
세 가지 라이브러리 소개
세 가지의 라이브러리를 사용합니다. 모두 유틸 함수로 만들어서 간편하게 사용하는 방법을 알려드릴 테니 역할 정도만 알고 가시죠.
clsx
이 녀석의 역할을 조건부 렌더링이 간편하게 해줍니다! 공식 문서를 확인해보면 삼항 연산자와 같은 조건식을 제공해주는 것을 확인할 수 있습니다.
예시:
clsx(
'base-button',
primary && 'primary-button',
danger && 'danger-button',
disabled && 'disabled-button'
);
// 결과: <button class="base-button primary-button disabled-button">Click me</button>
cva (class-variance-authority)
핵심이라 생각하는 cva입니다! 'class-variance-authority'
예시:
cva('button', {
variants: {
size: {
small: 'text-sm px-2 py-1',
medium: 'text-base px-4 py-2',
large: 'text-lg px-6 py-3',
},
color: {
primary: 'bg-blue-500 text-white',
secondary: 'bg-gray-300 text-gray-700',
},
},
});
위 예시를 보시면 size 및 color의 이름을 지정해주어 변수처럼 사용할 수 있습니다. 이걸 모두 적는 것 보단 로 땡 치는 게 가독성에 좋을 것이고, 확장성도 챙길 수 있어요. Tailwind와의 호환도 좋습니다.
tailwind-merge
우리는 Tailwind를 사용하며 이런 상황을 겪을 수가 있습니다. 상상을 해봅시다. 우리는 버튼 컴포넌트를 하나 만들었어요. 그런데 그 버튼 컴포넌트에는 다양한 스타일이 적용되어 있겠죠.
예시:
<button className="p-4 text-white bg-red-400 px-2 py-1">Button</button>
여기서 addClassName은 적용되지 않습니다. 하지만 twMerge를 이용하면 p-10이 정상적으로 적용 됩니다.
핵심은 마지막에 적용시킨 클래스를 적용시켜주는 역할을 하게 됩니다! 이제 컴포넌트를 따로 수정하지 않아도 추가적으로 부여해주는 className으로 다양한 상황에 대처할 수 있겠죠.
유틸 함수 만들기
라이브러리 설명이 길었습니다. 우리에겐 세 자루의 칼이 생겼고 이제 의 멱을 따러 갑시다. 아주 간단합니다. 우리는 유틸 함수 하나만 만들면 됩니다.
import { ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
TS 기준으로 설명하겠습니다. JS 경우에는 ClassValue라는 타입 지정 부분만 빼주시면 됩니다. cn이라는 함수는 이제 inputs를 통해 class를 받아와서 twMerge와 clsx를 적용시켜줍니다.
Button 컴포넌트 예시
이 유틸 함수를 import해서 이제 본격적으로 컴포넌트를 만들어볼게요.
import { VariantProps, cva } from 'class-variance-authority';
import { ButtonHTMLAttributes, FC } from 'react';
import { cn } from '../utils/cn'; // 유틸 함수 import
const ButtonVariants = cva(
// 모든 경우에 공통으로 들어갈 CSS
'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 hover:translate-x-10',
{
variants: {
// variant, size에 따라 다른 디자인을 보여줄 수 있다
variant: {
blue: 'bg-blue-500',
gray: 'bg-gray-500',
red: 'bg-red-500',
active: 'active:scale-100',
},
size: {
sm: 'bg-slate-300 w-[6rem] h-[2rem] text-[1rem] rounded-md',
md: 'w-[21rem] h-[7rem] text-[2rem] rounded-3xl',
lg: 'w-[24rem] h-[5rem] text-[2rem]',
circle: 'w-[6rem] h-[6rem] rounded-full',
},
},
defaultVariants: {
variant: 'blue',
size: 'md',
},
}
);
interface ButtonProps
extends VariantProps<typeof ButtonVariants>,
ButtonHTMLAttributes<HTMLButtonElement> {
// Button의 속성을 타입 지정 통해 손쉽게 사용
label?: string;
// 라벨은 단지 string을 넣을 때 사용
children?: React.ReactNode;
// icon component 같은 React 컴포넌트에 사용
additionalClass?: string;
}
/**
* @variant 색상 지정 ex) gray, blue, red
* @size 사이즈 지정 md, lg, wlg
* @children ReactElement 아이콘 같은 걸 넣어준다
* @label String을 넣어 버튼 라벨을 지정해준다
* @additionalClass 추가할 클래스 속성을 넣어준다
* @props 추가할 버튼 속성을 넣어준다
*/
const Button: FC<ButtonProps> = ({
variant,
size,
children,
label,
additionalClass,
...props
}) => {
return (
<button
className={cn(ButtonVariants({ variant, size }), additionalClass)}
{...props}
>
{children || label}
</button>
);
};
export default Button;
자, 버튼 컴포넌트를 만들었어요. 저는 템플릿처럼 만들어 두고 여기저기 class만 변경해서 사용한답니다. 주석을 천천히 보시다 보면 각각의 역할을 아실 수 있을 거예요.
사용 예시
이렇게 해서 사용한 예시까지 보시겠습니다. 여기까진 장황하고 복잡해 보이는데 사용하면 진짜 편해요... 진짜루. 몇 가지의 버튼을 준비해봤습니다.
파랗고 큰 버튼:
<Button variant="blue" size="lg" label="Click me" onClick={handleClickScroll} />
추가 클래스를 부여해준 붉은 버튼:
<Button
variant="red"
size="md"
label="Red Button"
additionalClass="hover:bg-red-600 p-10"
/>
조건식을 넣어준 동그란 버튼:
<Button
size="circle"
label="Circle"
additionalClass={clsx({ 'bg-black text-white': isDarkMode })}
/>
다크모드 버튼 색상을 보면 글씨색이 안 바뀌죠. 지정 안 해줘서 그럽니다. 이 또한 clsx가 조건식을 간편하게 사용하게 해주기 때문에 금방 적용시킬 수 있겠죠.
// Button.tsx 타입 지정 다시 해주기
// 조건식 추가 클래스 적어주기
이렇게 길고 긴 글이 끝났습니다. 다들 자신만의 방법으로 멋진 컴포넌트를 만들어 멋진 프로젝트 잘하시죠. 참고로 스토리북과의 호환도 좋다 합니다. 언젠간 스토리북으로 찾아오겠습니다. 완전 마음에 드는 라이브러리들 이네요..!
댓글
-
글 완전 멋져요! 감사합니다 잘 배우고 갑니다!
-
궁금한 점이 있는데요. 올려주신 Button 컴포넌트 내에서 clsx 활용은 어떻게 하는 건가요? 보기에는 Button 컴포넌트에 props로 전달받은 additionalClass가 ClassValue 타입을 갖고 있어야지 clsx가 적용될 것 같은데, 작성해주신 내용에는 additionalClass?: string 타입을 갖고 있어서 제가 이해를 잘 못한 건지 모르겠지만 혹시 clsx도 활용하는 방법 예시로 알려주시면 감사합니다.
답변:
className={cn(ButtonVariants({ variant, size }), additionalClass)}이렇게 cn 함수 안에서 사용하시면 됩니다!