Dev Camp : utility CN-shadcn (3)

최문길·2024년 3월 14일
1

dev-camp

목록 보기
3/5

서론이 이전에 길었는데 어쨌건

cn에 대해서 파헤쳐 보자

cn

util 함수에 자동적으로 생성되는 함수를 보면

import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

이런 함수가 보이는데 twMerge가 먼저 보인다.

그러나 twMerge를 보기 이전에 clsxcva에 관해 간단히 짚고 넘어가자



clsx

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항 연산자는 조금 맘이 불편해져서..



cva

저번 프로젝트에서 처음 사용한 라이브러리인데 여러 조건부가 붙은 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를 살펴보자

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으로 패딩값을 바꾸려 했지만,


우리가 일반적으로 아는 cascading style css가 적용 되지 않는다.

여기서 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)

  • clsx : 조건부 css , tailwindcss사용시 전체를 한줄의 text로 만듬 ,twMerge
  • cva : variable 한 컴포넌트 ui에서 size, font, color를 선택적으로 사용할 수 있으며 객체 형태로 가독성이 좋음


마치며

하나의 utility 함수를 알기 위해서 정말 많은 시간과 노력을 할애 한것 같다.

쓸모없는 경험은 없다고, 프로젝트에서 css.module을 사용하여 조건부 css를 하기 위해 사용했던

clsx, cva가 shadcn/ui에서 활용 되기에 조금은 맘 편히 어떻게 custom 하는지 이해도 어느정도 간다.

다양한 실험과 시행착오를 거치며 경험을 얻는 것이 지금은 중요한것 같다.

0개의 댓글

관련 채용 정보