서론이 이전에 길었는데 어쨌건
cn에 대해서 파헤쳐 보자
util 함수에 자동적으로 생성되는 함수를 보면
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
이런 함수가 보이는데 twMerge가 먼저 보인다.
그러나 twMerge를 보기 이전에 clsx
와 cva
에 관해 간단히 짚고 넘어가자
clsx는 저번 프로젝트에서 사용했었다.
기존 jsx에서 css.module을 사용해서 태그에 class를 여러 개 적용하려면
<div className={`${styles.container1} ${styles.container2}`}>
<h1>I have two Classes</h1>
</div>
위와 같이 적용을 해야한다... 그런데 귀찮지 않겠는가? 귀찮다고 하자
이럴때 사용할 수 있는 유용한 라이브러리가 clsx
라이브러리다
clsx를 사용하여 간단하게 쓸수 있다.
<div className={clsx(styles.title, styles.margin, styles.color)}>
</div>
기본 적으로 clsx함수를 불러와서 css module과 사용하면 찰떡궁합이다.
뿐만아니라 조건부로도 사용 할수가 있다.
<div className={clsx({
[styles.red]: count === 1,
[styles.yellow]: count === 2,
[styles.blue]: count === 3,
})}>
</div>
이렇게 조건부로 true일 때 어떤 스타일을 반환 할 수 있고
기본 스타일은 유지하고 조건부로도 가능하다.
<div className={clsx(styles.default, {
[styles.red]: Boolean(red),
[styles.blue]: !Boolean(red)
})}>
</div>
위의 3가지를 보면 정말 훌륭한 라이브러리임이 분명하지만
내가 좋아하는 것은 타입 부분이다.
export type ClassValue = ClassArray | ClassDictionary | string | number | null | boolean | undefined;
export type ClassDictionary = Record<string, any>;
export type ClassArray = ClassValue[];
export function clsx(...inputs: ClassValue[]): string;
export default clsx;
clsx
에 들어올수 있는 값에 null
, boolean
, undefined
type이 들어올 수 있는데
반드시 3항 연산자를 사용해서 style을 적용시켜야 '만' 하느 것이 아니라 옵셔널 연산자를 사용해 스타일을 적용 할 수 있는 것이다. 3항 연산자는 조금 맘이 불편해져서..
저번 프로젝트에서 처음 사용한 라이브러리인데 여러 조건부가 붙은 style을 객체 형태로 반환하여 사용할 때 훌륭하다.
나는 캘린더에서 css.module을 이용해 스타일을 조건부로 사용할 때 사용했었다.
const salesVariant = cva([styles.salesBase], {
variants: {
sales: {
MAX: styles.salesMax,
MIN: styles.salesMin,
NONE: styles.salesNone,
},
},
});
위와 같이 default css를 첫 번째 인자에 배열 형태로 넣어주고
그 다음 조건부로 넣을 style들을 객체안의 객체 안에 조건부로 넣어준다.
사용법은
<span
className={salesVariant({
sales: salesData && getMinMaxSalesType?.(salesData),})}
>
cva와 clsx를 합칠 수도 있는데
import { cva } from 'class-variance-authority';
import clsx from 'clsx';
const statusVariant = cva([styles.statusCalendarBase], {
variants: {
calendarType: {
CURRENT: styles.currentCalendar,
NOT_CURRENT: styles.notCurrentCalendar,
},
monthType: {
CURRENT: styles.statusCurrentMonth,
NOT_CURRENT: styles.statusNotCurrent,
},
dateType: {
PREV: styles.statusPrevDate,
CURRENT: styles.statusCurrentDate,
AFTER: styles.statusAfterDate,
},
holidayType: {
HOLIDAY: styles.statusHoliday,
},
},
});
<div
className={clsx({
[statusVariant({
calendarType: getStatusCalendarType(day, currentDate),
monthType: getStatusMonthType(currentDate, day),
dateType: getStatusDateType(day),
holidayType: holiday?.[0]?.name ? HOLIDAY : null,
})]: page === STATUS_PAGE,
})}
//... >
이렇게 조건부로 clsx와 cva를 합쳐 clsx로 반환된 className들을 div태그에 적용 할 수 있다.
이제 twMerge를 살펴보자
twMerge는 tailwind를 설치할 때 내장된것이 아니다.
npx i tailwind-merge
yarn add tailwind-merge
각 패키지 명령어로 설치해줘야 한다 .
tailwind-merge의 What is it for 문서에서는
React, Vue와 같은 컴포넌트 기반 라이브러리와 함께 tailwind css 를 사용하는 경우에는
컴포넌트의 일부 스타일을 오직 한곳에서만 변경하고 싶은 상황에 익숙할 것입니다!
이렇게 써져있는데 뭔 말인진 잘 모르겠지 않은가??
코드로 확인해보자
// React components with JSX syntax used in this example
function MyGenericInput(props) {
const className = `border rounded px-2 py-1 ${props.className || ''}`
return <input {...props} className={className} />
}
function MySlightlyModifiedInput(props) {
return (
<MyGenericInput
{...props}
className="p-3" // ← Only want to change some padding
/>
)
}
바로 이런 상황에서 인데
props에서 다시 한번더 p-3
으로 패딩값을 바꾸려 했지만,
여기서 p-3 스타일을 적용시키기 위해서는 px-2, py-1을 제거해야만 하는데
이러한 문제를 tailwind-merge
가 해결해준다.
function MyGenericInput(props) {
// ↓ Now `props.className` can override conflicting classes
const className = twMerge('border rounded px-2 py-1', props.className)
return <input {...props} className={className} />
}
tailwind-merge가 제공하는 twMerge함수를 이용하면 다음과 같이 작성 할 수 있고
이렇게 기존 클래스와 충돌하게 되는 클래스를 override
해줄 수 있고
우리가 생각하는 cascading style이 되는것이라 볼 수 있다.
근데 왜 우리가 생각하는 css처럼 안되는 것일까??
-> 설명 잘한 블로그
tailwind css 빌드의 결과물을 살펴보면 원인을 알 수 있는데
tailwind에서 정의한 순서가 있기에 안되는 것
import { type ClassValue, clsx } from "clsx"
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
위의 코드가 이해가 가지 않는가??
내가 cva에서 내 캘린더 css 조건부를 봤을 때 cva함수안에 clsx가 포함된것이 아니라 clsx함수 안에 cva함수가 포함 되어있었고 clsx에 들어올 수 있는 type은 정말 다 가능하기에
cn
에 들어올 수 있는 classValue의 타입지정은 clsx의 타입으로 하고
tailwind를 사용하기에 twMerge를 return 하는 것은 기정 사실이고
이렇게 3개의 포스팅을 해서 내 스스로 납득이 가진다.
cf)
하나의 utility 함수를 알기 위해서 정말 많은 시간과 노력을 할애 한것 같다.
쓸모없는 경험은 없다고, 프로젝트에서 css.module을 사용하여 조건부 css를 하기 위해 사용했던
clsx
, cva
가 shadcn/ui에서 활용 되기에 조금은 맘 편히 어떻게 custom 하는지 이해도 어느정도 간다.
다양한 실험과 시행착오를 거치며 경험을 얻는 것이 지금은 중요한것 같다.