Tailwind: Nextjs 13 App Directory에서 사용하기

기존에는 style를 만들때 css in JS 방식으로 emotion을 사용했엇는데 Nextjs 13버전부터 app directory를 사용하면서 component를 client, server로 최적화해서 사용을 하다보니, emotion을 사용하려면 Client Component만 사용했어야 했습니다.

그래서 대안으로 css, scss, tailwind를 사용하려 했습니다.

우선 css와 scss의 경우에는 기존에 많이 사용하던 방식이었기 때문에 쉽게 사용할 수 있었지만, 컴포넌트별로 파일을 분리해야 되었습니다. className을 일일이 만들어서 사용해야 했기 때문에 코드를 확인할때 정확히 어떤 css가 사용되었는지 확인하려면 해당 css파일에서 해당 class를 찾아서 확인을 해야 했기 때문에 가독성면에서 떨어져서 기존에도 css in js 방식을 사용했었기 때문에 tailwind를 사용해보기로 했습니다. 또한 Nextjs 팀에서 tailwind를 추천하고 있어서 tailwind를 사용하기로 했습니다.

마음에든점

  1. 예약된 className과 기본 style

기본적으로 tailwind는 예약된 className을 사용하여 css를 작성하는 방식입니다. 아래와 같이 디자인이 완벽하지 않아도 tailwind의 기본 style을 사용하여 우선적으로 개발을 시작할 수 있엇습니다.

<div className="bg-primary p-4">Hello</div>

tailwind 에서 제공되는 style의 경우 rem 단위의 기본적인 style을 제공하고 있어서 디자인이 없어도 빠르게 작업을 할 수 있었습니다.

  1. tailwind.config.js

이후에 디자인이 완벽해지면 tailwind.config.js에서 값을 수정할 수 있었는데 디자인 시스템을 개발을 할때 매우 유용했습니다.

module.exports = {
theme: {
  colors: {
    'primary': '#1fb6ff',
    ...
    },
  }
}
  1. 반응형

    css에서는 반응형을 작성을하려면 @media를 사용하여 코드 코드가 길어지는데 tailwind에서는 반응형을 작성할때 매우 간단하게 작성할 수 있었습니다.

<div className="bg-primary md:bg-secondary">Hello</div>

md, lg와 같은 화면 사이즈 또한 tailwind.config.js에서 수정할 수 있기때문에 일괄적으로 디자인을 수정할때에도 유용했습니다.



문제점과 해결방법

tailwind는 자체로만 사용을 해도 되었지만 몇가지 문제가 있었습니다.

1. 중복된 스타일

tailwind를 사용하다보면 중복된 css class가 생기는 경우가 있습니다.

<div className="px-4 py-2 p-4" />

위와 같이 사용할경우 css의 Cascading방식으로 인해 의도치 않은 결과가 나올수 있으며, 가독성면에서 매우 떨어지게 됩니다.

  1. prettier로 css 순서 조정하기

우선 prettier-plugin-tailwindcss를 사용하게 되면 tailwind의 css class를 정렬해주어서 중복된 css class를 찾기 쉽게 해줍니다.

  1. 중복 css 병합

중복된 스타일의 경우 tailwind-merge 라이브러리를 통해서 해결할 수 있었습니다.

import { twMerge } from "tailwind-merge";

<div
  className={twMerge("px-2 py-1 bg-red hover:bg-dark-red", "p-3 bg-red-500")}
/>;
// ->  'hover:bg-dark-red p-3 bg-red-500'}

위와 같이 중복된 padding값을 마지막에 사용된 p-3만 족용되게 되어서 중복된 css class를 제거할 수 있었습니다.

2. 스타일 동적할당 오류

className에 css를 작성하면 build 타임에 사용되는 css class만 프로젝트에 추가하여 최적화를 해주는데, 스타일을 동적 할당할때 생기는 문제가 있었습니다.

const Button = ({ children, color }) => {
  return <button className={`bg-${color}`}>{children}</button>;
};

위에 코드를 보면 color값에다라 버튼의 배경색이 변경되는 코드입니다. tailwind에서는 사용되지 않는 css는 제거를 해주기 때문에 위와 같이 코드를 작성하게 되면, 올바른 color값을 넘기더라도 해당 css값이 없어서 동작하지 않습니다.

이러한 스타일을 동적으로 할당할때에는 2가지 방법이 있습니다.

  1. 사용하려는 style를 명시적으로 작성하기
const Button = ({ children, color }) => {
  return (
    <button
      className={`${color === "primary" ? "bg-primary" : "bg-secondary"}`}
    >
      {children}
    </button>
  );
};

위와 같이 명시적으로 사용하려는 style를 작성하면 해당 style이 사용되지 않더라도 css가 제거되지 않습니다.

  1. tailwind.config.js에 항상 사용할 style를 추가하기
module.exports = {
  safelist: ["bg-primary", "bg-secondary"],
};

tailwind.config.js에 위와같이 사용할 스타일을 항상 포함하도록 할 수 있습니다. 또한 정규표현식을 이용한 pattern으로도 광범위하게 지정을 할 수 도 있습니다.

2.1. 스타일 조건부 동적 할당 활용

clsx라이브러리로 tailwind-merge라이브러리와 함꼐 사용하여 조건부로 스타일을 좀더 편하게 할 수 있습니다.

clsx는 조건부로 문자열을 구성하기 위한 라이브러리입니다. 비슷한 기능을 라이브러리중 239B크기의 작은 라이브러리로 가장 작으며, 쉽게 사용할 수 있는 라이브러리 입니다.

비슷한 라이브러리중 classNames도 있었지만, classNames는 객체방식으로 작동하여 clsx가 좀더 직관적인거 같아서 사용했습니다.

clsx("foo", true && "bar", "baz");
// -> 'foo bar baz'
clsx(["foo"], ["", 0, false, "bar"], [["baz", [["hello"], "there"]]]);
//=> 'foo bar baz hello there'

위와 같이 조건부 문자열을 문자열로 만들때 사용하여 className에 적용할때 tailwind-merge와 함꼐 아래와 같이 사용할 수 있습니다.

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

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

4. tailwind에서 class를 구조화 하기

tailwind를 사용하기에 앞어서 tailwind의 경우에는 className에 직접적으로 css를 사용하다 보니 코드가 길어지는 단점이 있었습니다. 또한 3번과 같이 조건부로 스타일을 사용할 경우에 가독성이 많이 떨어지게 됩니다.

<Button variant="primary" size="lg">
  Hello
</Button>

위 코드와 가이 variant나 size 등 다양한 조건부로 스타일을 사용할때에에 cva를 사용하여 아래와 같이 코드를 구성할 수 있습니다.

const buttonVariants = cva(
  "flex items-center justify-center whitespace-nowrap rounded-md text-sm",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground",
        secondary: "bg-secondary text-secondary-foreground",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

const Button = ({ children, variant, color, className }) => {
  return (
    <button className={cn(buttonVariants(variant, color, className))}>
      {children}
    </button>
  );
};

위와 같이 cva를 사용하여 조건부로 스타일을 사용할때에 컴포넌트를 구조화 하여 가독성을 높여서 사용할 수 있었습니다.

비슷한 라이브러리로 https://www.tailwind-variants.org/ 도 있었지만 github star수가 cva가 3배이상 많아서 cva를 사용했습니다.


결론

개발을 할때에 기존의 css in js방식인 emotion을 사용할때보다 class로 바로 style를 적용할 수 있는 tailwind가 편리하고 빠르게 개발을 할 수 있었습니다. 가장 고려하던 문제점으로 className에 있는 코드 가독성이 떨어진다는 것이 였는데, 기존의 emotion과 비슷하게 컴포넌트를 구조화해서 스타일를 조건부로 할당할 수 있기 때문에 tailwind를 사용하면서도 가독성을 높일 수 있었습니다.

또한 필요에 따라서 tailwind는 기존의 css in js 라이브러리나 css, scss등과 같이 사용할 수 있어서 여러 라이브러리를 혼합하여 사용할 수 있어서 tailwind를 도입하는 것에 대한 부담이 적었습니다.

스타일 라이브러리를 선택하는것은 개발 요구사항에 따라 달라지겠지만, Nextjs와 같이 Server Component방식을 사용할 경우 css, scss와 같은 방식이 마음에 들지 않다면 좋은 대안이 될 것 같고 다른 대안이 나오지 않는다면 tailwind를 지속적으로 사용하게 될 것 같습니다.