# [ 살펴보기 ] Tailwind - Tips & Tricks

## Dynamic Value

TailwindCSS를 사용할 때 주의할 점 중 하나는 class utility에 dynamic한 값을 삽입하여 사용할 수 없다는 것이다.

```typescript
const Home = () => {
  const [color, setColor] = useState("purple");
  const handleColor = (color: string) => {
    setColor(color);
  };
  return (
    <main>
      <div className="p-5 space-y-5">
        <div className={`text-${color}-500`}>Test Text</div>
        <div>
          <select value={color} onChange={(e) => handleColor(e.target.value)}>
            <option value="purple">Purple</option>
            <option value="orange">Orange</option>
          </select>
        </div>
      </div>
    </main>
  );
};

export default Home;
```

위에서 select option을 orange나 purple로 변경해도 실제 text color는 변경되지 않는다. 그렇기에 위의 상황과 같이 상태에 따라 다른 class utility를 적용하고 싶으면 적용하고자 하는 class utility의 전부를 선언해 놓고 사용해야 한다.

```typescript
const Home = () => {
  const [color, setColor] = useState("text-purple-500");
  const handleColor = (color: string) => {
    setColor(color);
  };
  return (
    <main>
      <div className="p-5 space-y-5">
        <div className={`${color}`}>Test Text</div>
        <div>
          <select value={color} onChange={(e) => handleColor(e.target.value)}>
            <option value="text-purple-500">Purple</option>
            <option value="text-orange-500">Orange</option>
          </select>
        </div>
      </div>
    </main>
  );
};

export default Home;
```

## Tailwind-merge

대부분의 component에는 default로 적용되는 style이 존재한다. 그리고 대부분의 경우 component의 재활용성을 위해 prop을 전달하여 default style을 어느정도 override할 수 있게 component를 구성한다. 위에서 살펴보았듯이 tailwind에선 class utility에 dynamic value를 삽입하여 style을 적용할 수 없다. 그렇기에 tailwind-merge와 같은 library를 통해 style을 merge 시키거나 override해서 최종적으로 생성된 class utility를 component에 적용한다.

```typescript
import { twMerge } from "tailwind-merge";

const TextComp = ({ className }: { className: string }) => {
  return (
    <p
      className={twMerge(
        "px-2 text-red-500",
        className
      )}
    >
      Click me!
    </p>
  );
};

const Home = () => {
  return (
    <main>
      <div className="p-5 space-y-5">
        <TextComp className="text-blue-500" />
      </div>
    </main>
  );
};

export default Home;
```

위의 예제에서 TextComp의 default class는 px-2와 text-red-500으로 설정되어 있다. 그리고 해당 component를 사용할 때 text-blue-500이라는 새로운 className을 prop으로 전달하고 있다.

TextComp에 적용되는 className은 tailwind-merge pacakge가 제공하는 twMerge 함수를 통해 두 번째로 전달된 값과 merge되어 적용된다. merge가 발생할 때 이미 선언되어 있는 class와 같은 class가 전달되면 두 번째 argument로 전달된 class가 사용된다.

결과적으로 위에서 TextComp의 text 색상은 text-blue-500이 된다.

여기서 주의할 사항이 있다. 만약 tailwind.config를 다음과 같이 수정했다고 가정해보자.

```typescript
import type { Config } from "tailwindcss";

const config = {
  ...
  theme: {
    extend: {
      fontSize: {
        h1: "36px",
        h2: "32px",
        h3: "28px",
        h4: "24px",
        h5: "20px",
        h6: "16px",
        body1: "18px",
        body2: "16px",
        subtitle1: "14px",
        subtitle2: "13px",
      },
      colors: {
        primary: "#0066FF",
        secondary: "#80deea",
        disabled: "#30344140",
      },
    },
  },
} satisfies Config;
export default config;
```

Project에 적용해야 하는 design system이 있다면 위의 예제와 같이 theme을 확장하거나 overrider할 수 있을 것이다.

하지만 위에서 확장한 theme을 merge에서 사용해보면 merge가 원하는대로 동작하지 않는 경우가 있다는 것을 확인할 수 있다. 아래의 예제를 살펴보자.

```typescript
import { twMerge } from "tailwind-merge";

const TextComp = ({ className }: { className: string }) => {
  return (
    <p
      className={twMerge(
        "px-2 text-primary",
        className
      )}
    >
      Click me!
    </p>
  );
};

const Home = () => {
  return (
    <main>
      <div className="p-5 space-y-5">
        <TextComp className="text-h2" />
      </div>
    </main>
  );
};

export default Home;
```

위의 예제에서 TextComp는 default color로 text-primary를 가지고 있고 Component에 text-h2 class를 전달하여 기본 default style은 그대로 쓰되 font-size만 변경하려는 것이 우리가 의도한 바일 것이다.

하지만 결과는 text-primary와 text-h2가 merge 과정에서 conflict로 취급되어 text-h2가 text-primary를 override 해버린다. 결과적으로 font-size는 적용되지만 font-color가 사라지는 결과가 나타난다.

이를 해결하기 위해선 twMerge 함수에 우리가 설정한 custom theme중에 어떤 property가 color를 위한 property이고 어떤 property가 font-size를 위한 property인지 알려주어야 한다. 그리고 그 작업을 아래와 같이 custom merge 함수를 하나 더 정의하여 사용할 수 있다.

```typescript
import config from "../../tailwind.config";

export const customTwMerge = extendTailwindMerge({
  extend: {
    classGroups: {
      "font-size": Object.keys(config.theme.extend.fontSize)?.map(
        (key: string) => `text-${key}`
      ),
    },
    theme: {
      colors: Object.keys(config.theme.extend.colors),
    },
  },
});
```

위의 예제에서 customTwMerge를 하나 더 만들어 custom으로 추가한 theme의 key들이 무슨 역할을 하는지에 대한 정보를 제공하고 있다.

그리고 위에서 만든 customTwMerge를 기존의 twMerge 대신 사용하면 위에서 발생하는 문제를 해결할 수 있다.

```typescript
import config from "../../tailwind.config";

export const customTwMerge = extendTailwindMerge({
  extend: {
    classGroups: {
      "font-size": Object.keys(config.theme.extend.fontSize)?.map(
        (key: string) => `text-${key}`
      ),
    },
    theme: {
      colors: Object.keys(config.theme.extend.colors),
    },
  },
});

const TextComp = ({ className }: { className: string }) => {
  return (
    <p
      className={customTwMerge(
        "px-2 text-primary",
        className
      )}
    >
      Click me!
    </p>
  );
};

const Home = () => {
  return (
    <main>
      <div className="p-5 space-y-5">
        <TextComp className="text-h2" />
      </div>
    </main>
  );
};

export default Home;
```

## Clsx package

UI 작업을 하다보면 특정 조건에 따라 다른 style을 적용해야 할 때가 많다. Clsx package를 통해 특정 조건에 따라 element에 최종적으로 적용할 class를 보다 쉽게 구성할 수 있다.

우선 clsx package를 설치한다.

```typescript
npm install clsx
```

그리고 clsx를 실제로 적용해보자. 아래의 예제는 userType의 값에 따라 다른 class를 적용하는 예제이다. 만약 userType이 user라면 text-purple-500을 적용하고 admin이라면 text-blue-500을 적용한다.

```typescript
...
import { extendTailwindMerge } from "tailwind-merge";
import clsx from "clsx";

export const customTwMerge = extendTailwindMerge({
  ...
});

const Home = () => {
  const userType: UserType = "admin";
  const testClass = clsx(
      "p4 text-orange-500",
      { "text-purple-500": userType === "user" },
      { "text-blue-500": userType === "admin" },
    );

  return (
    <main>
        <div className={customTwMerge(testClass)}>Test 123</div>
    </main>
  );
}
```

위의 예제에서 element에 class를 적용할 때 customTwMerge를 통해 중복되는 class를 merge & override하므로 최종적으로 적용되는 class는 다음과 같다

```typescript
...

const Home = () => {
  const userType: UserType = "admin";
  const testClass = clsx(
      "p4 text-orange-500",
      { "text-purple-500": userType === "user" },
      { "text-blue-500": userType === "admin" },
    );

  return (
    <main>
        <div className="p4 text-blue-500">Test 123</div>
    </main>
  );
}
```

class를 조건에 따라 구성할 때 아래와 같이 구성할 수도 있다. 아래의 예제는 위의 clsx 예제와 동일한 역할을 한다.

```typescript
const testClass2 = clsx(
  "p4",
  "text-orange-500",
  userType === "user" && "text-purple-500",
  userType === "admin" && "text-blue-500",
);
```

clsx는 여러 syntax를 통해 class를 구성하는 방법을 제공하므로 더 자세한 내용은 [documentation](https://github.com/lukeed/clsx#readme)을 참조하자.

## Class-variance-authority

Design system을 기반으로 component를 구성하고 UI theme을 구성한다면 대부분 component에 대한 variant나 size에 사용되는 unit을 정해놓고 작업할 것이다. class-variance-authority library를 통해 보다 쉽게 component 전달되는 variant나 size에 따라 다른 style을 구성할 수 있다.

우선 필요한 package를 설치한다.

```typescript
npm i class-variance-authority
```

그리고 class-variance-authority ( cva )를 통해 다음과 같은 variants를 생성해보자.

```typescript
import { cva } from "class-variance-authority";

const textVariant = cva(["p2"], {
  variants: {
    variant: {
      primary: ["bg-blue-300", "hover:bg-orange-600", "hover:text-white"],
      secondary: ["bg-green-300", "hover:bg-purple-600", "hover:text-white"],
    },
    size: {
      small: ["text-sm"],
      medium: ["text-base"],
      large: ["text-lg"],
    },
  },
  defaultVariants: {
    variant: "primary",
    size: "medium",
  },
});
```

위의 예제에서 cva function에 처음 인수로 전달하는 array ( `[“p2”]` )는 variants에 정의한 variant나 size와 상관없이 default로 전달할 class list를 정의한다. 그리고 variants property에 해당 함수를 호출할 때 원하는 class를 선택할 수 있게 option으로 전달할 property를 설정한다.

위와 같이 cva variants를 구성하여 변수에 할당하면 해당 변수는 추후에 호출할 수 있는 function이 된다.

```typescript
const textVariant = cva(["p2"], {
  variants: {
    variant: {
      primary: ["bg-blue-300", "hover:bg-orange-600", "hover:text-white"],
      secondary: ["bg-green-300", "hover:bg-purple-600", "hover:text-white"],
    },
    size: {
      small: ["text-sm"],
      medium: ["text-base"],
      large: ["text-lg"],
    },
  },
  ...
});

const titleTextVariant = textVariant({size:"large", variant:"secondary"});
```

위와 같이 textVariant function을 호출할 때 size를 large로 전달하고 variant를 secondary로 전달하면 cva를 통해 설정했던 class가 전달된다.

만약 다음과 같이 textVariant를 호출할 때 아무런 params을 전달하지 않으면 defaultVariants에 설정했던 값이 적용된다.

```typescript
const textVariant = cva(["p2"], {
  ...
  defaultVariants: {
    variant: "primary",
    size: "medium",
  },
});

const titleTextVariant = textVariant();
```

이를 component에 적용하면 아래와 같을 것이다.

```typescript
import { cva } from "class-variance-authority";

const TextComp = ({ className, size, variant }: Props) => {
  const textClass = textVariant({ variant, size });
  return <p className={textClass}>Test Text Component!</p>;
};

const textVariant = cva(["p4 borer-solid border-2 border-blue-500"], {
  variants: {
    variant: {
      primary: ["bg-blue-300", "hover:bg-orange-600", "hover:text-white"],
      secondary: ["bg-green-300", "hover:bg-purple-600", "hover:text-white"],
    },
    size: {
      small: ["text-sm"],
      medium: ["text-base"],
      large: ["text-lg"],
    },
  },
  defaultVariants: {
    variant: "primary",
    size: "medium",
  },
});
```

이제 예제의 컴포넌트가 전달받는 variant와 size prop에 따라 적용되는 class가 달라진다. 이렇듯 cva를 통해 Design System에 따른 component UI 구성을 보다 수월하게 처리할 수 있다.

여기서 조금 더 추가해야 할 사항이 아직 남아있다. 바로 default로 적용되는 class로 선택한 variant에 같은 class가 존재해서 conflict가 발생할 수 있다. 그리고 이를 해결하기 위해 주로 tailwind merge function을 함께 쓴다. 아래 예제에서는 위에서 custom으로 만든 merge 함수를 적용하겠다.

```typescript
import { extendTailwindMerge } from "tailwind-merge";
import { cva } from "class-variance-authority";

export const customTwMerge = extendTailwindMerge({
  ...
});

const textVariant = cva(["p4 bg-orange-300"], {
  variants: {
    variant: {
      primary: ["bg-blue-300", "hover:bg-orange-600", "hover:text-white"],
      ...
    },
    ...
  },
  ...
});

const titleVariant = textVariant({ size: "large", variant: "primary" });
const titleVariantMerged = customTwMerge(titleVariant);

<div className={titleVariantMerged}>Test</div>
```

위의 예제에서 볼 수 있듯이 cav로 전달하는 default class중 bg-color class가 존재하고 primary varaint에도 bg-color class가 존재한다. priamary variant 사용시 conflict로 인해 variant style이 적용되지 않는 문제가 없도록 merge 함수를 거쳐서 실제 element에 적용한다.

위의 상황을 조금 더 복잡하게 만들어보자. 만약 primary style에 더불어 특정 조건에 따라 style을 일부만 변경해야 한다면 어떻게 해야할까? 이럴 때는 merge와 clsx를 함께 사용할 수 있다.

```typescript
const userType: UserType = "admin";

const titleVariantMergedWithClsx = customTwMerge(
  clsx(
    textVariant({ size: "large", variant: "secondary" }),
    { "bg-indigo-500": userType === "user" },
    { "bg-pink-500": userType === "admin" },
  ),
);

<div className={titleVariantMergedWithClsx}>Test</div>
```

위의 예제에서 clsx를 통해 userType이 admin일 때 bg color를 pink-500으로 주고 있으므로 div element에 적용되는 최종 bg color는 pink-500이다.

그리고 위의 merge와 clsx를 하나의 util 함수로 묶어서 사용할 수 있다. 아래의 예제에선 cn이라는 이름의 util 함수로 만들어 적용하고 있다.

```typescript
import clsx, { ClassValue } from "clsx";
import { extendTailwindMerge } from "tailwind-merge";
import { cva } from "class-variance-authority";

export const customTwMerge = extendTailwindMerge({
  ...
});

const cn = (...inputs: ClassValue[]) => {
  return customTwMerge(clsx(inputs));
};

const textVariant = cva(["p4 bg-orange-300"], {
  ...
});

const userType: UserType = "admin";

const titleVariantWithCn = cn(
  textVariant({ size: "large", variant: "secondary" }),
  { "bg-indigo-500": userType === "user" },
  { "bg-pink-500": userType === "admin" },
);

<div className={titleVariantWithCn}>Test</div>
```

## Tailwind Prettier Plugin

Tailwind Prettier plugin은 prettier가 code를 auto format할 때 사용자가 정의한 class utility의 순서를 tailwind가 실제로 class를 배치하는 순서대로 변경해준다.다음 예제를 살펴보자.

```typescript
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .test1 {
        color: blue;
      }
      .test2 {
        color: orange;
      }
    </style>
  </head>
  <body>
    <div class="test2 test1">Test Text</div>
  </body>
</html>
```

위의 예제에서 Test Text div element에 test2 class가 먼저 추가되고 그리고 test1 class가 추가되었다. css rule에 익숙하지 않으면 test1의 style인 color:blue가 적용될 것이라고 예상할 수 있지만 실제로 적용되는 style은 test2 class의 style이다.

왜냐하면 class가 적용되는 우선순위는 class를 사용한 순서가 아니라 class가 실제 선언된 순서다. 즉, &lt;style&gt; tag에서 test2 class가 test1 class 이후에 선언되었으므로 두 class가 같이 선언되었을 때 우선순위를 갖는건 test2 class다.

그렇기에 tailwing prettier plugin은 tailwind가 실제로 class를 배치하는 순서에 맞게 element에 적용한 class utility의 순서를 변경해준다.

Plugin 적용을 위해 우선 필요한 package를 설치하자.

```typescript
npm install -D prettier prettier-plugin-tailwindcss
```

그리고 project root 경로에 .prettierrc.json 파일을 생성해 prettier에 적용할 rule을 설정하고 추가로 plugin을 추가해준다.

```typescript
{
  ...
  "plugins": ["prettier-plugin-tailwindcss"]
}
```

만약 ./src/app/page.tsx 파일에 다음과 같이 tailwind class가 적용되어 있다고 가정해보자.

```typescript
...
return (
  <div className="pt-2 p-6">Test 123</div>
)
```

`npx prettier ./src/app/page.tsx --write` 명령어를 통해 위의 파일을 prettier를 통해 format하면 class의 순서는 다음과 같이 변경된다.

```typescript
...
return (
  <div className="p-6 pt-2">Test 123</div>
)
```

물론 실제 개발할 때 command line으로 prettier를 적용하기 보다 VSCode나 IntelliJ와 같은 Editor를 사용하고 있다면 extension을 통해 파일을 save할 때 자동으로 format하게 설정할 수 있으므로 Editor 설정을 통해 prettier를 적용하도록 하자.

만약 clsx를 사용하고 있다면 prettier에서 clsx를 format할 수 있도록 .prettierrc.json에 추가 설정을 해주어야 한다.

```typescript
{
  ...
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindFunctions": ["clsx"]
}
```

위와 같이 설정을 추가하면 같은 clsx에서 선언한 class의 순서도 함께 format해준다.

```typescript
import clsx from "clsx";

...
const isActive = true;
const testClass = clsx("p-6 pt-2", { "bg-blue-500": isActive });

return (
  <div className={`${testClass}`}>Test 123</div>
)
```

한 가지 주의할 점은 사용 문법에 따라 prettier가 format을 해주는 범위에 한계가 존재한다. 아래 예제와 같은 경우는 자동으로 순서를 format해주지 못한다.

```typescript
const testClass = clsx("pt-2", { "p-6": true }, { "bg-blue-500": isActive });
const testClass2 = clsx({ "pt-2": true, "p-6": true }, { "bg-blue-500": isActive });
const testClass3 = clsx("pt-2", "p4");
```

원래라면 p-6 pt-2 순서로 변경해 주겠지만 위의 경우에는 다른 object로 선언된 class이기에 object의 순서까지 자동으로 format 해주진 않는다.

하지만 다음과 같은 경우는 class 순서를 format을 해준다.

```typescript
const testClass = clsx("p4 pt-2", { "bg-blue-500": isActive });
const testClass2 = clsx({ "p-2 pt-2": true }, { "bg-blue-500": isActive });
```

위와 같이 사용 문법에 따라 format의 적용 여부가 달라지니 사용시 주의가 필요하다.
