CSS

프론트엔드 팀을 위한 Tailwind CSS: 설정에서 규칙까지

윤종석 프로필 이미지

윤종석

2024.04.01

14 min read

프론트엔드 팀을 위한 Tailwind CSS: 설정에서 규칙까지

안녕하세요, 코멘토 프론트엔드 개발자 윤종석입니다. 코멘토 프론트엔드 팀은 이번에 Next.js로 기존 프로젝트를 전환하는 준비를 하고 있는데요. 전환 과정에서 새로운 스택을 정말 많이 도입하고 있고 그 중에서 이번에는 Tailwind CSS를 도입한 과정을 공유해보려고 합니다. 최근에 아주 인기가 많은 프레임워크이고 혼자서도 종종 써본 기술이지만 개발팀이 다같이 쓸 수 있게 설정하고 사용하는 것은 완전히 다른 도전이었습니다. 도입하며 만난 다양한 문제와 해결법을 소개해드리려 합니다.

우리 디자인에 맞게 커스텀하기

Tailwind를 처음에 설치하면 기본값을 가지고 있습니다. 예를 들면, mt-4margin-top: 1rem;을 의미합니다. 하지만 저희가 기존에 사용하던 유틸리티 클래스는 mt-4 = margin-top: 4px;처럼 보이는 숫자 그대로의 값을 가지고 있었습니다. 숫자만 다른 것이 아니었습니다. 색상, 텍스트 등 저희가 사용하는 이름과 값은 모두 Tailwind의 기본값과 달랐습니다.

다행히 Tailwind에는 기본값을 변경할 수 있는 Theme 설정이 있습니다. tailwind.config.js에서 theme 속성을 설정하면 되는데요. 아래와 같이 변경하면 원하는 값으로 사용할 수 있고 에디터에서 자동 완성도 변경한 값을 기준으로 작동해서 편리하게 사용할 수 있습니다.

// tailwind.config.js
import { height } from './height.js';

module.exports = {
	theme: {
	  // 이렇게 바로 theme 아래에서 바꾸면 기본값을 모두 덮어씁니다.
		colors: {
			primary: '#2a7de1',
			...
		},
		extend: {
		  // extend 아래에서 설정하면 Tailwind의 클래스를 유지하면서 내가 설정한 값만 변경합니다.
		  // h-1/2, h-full같은 클래스가 유용하기 때문에 extend로 사용하는 것이 좋습니다
			height: height, // 이렇게 코드의 결과값을 사용할 수도 있습니다
		}
	},
}

// height.js
const height = {};

for (let i = 0; i <= 300; i++) {
	if (i % 2 === 0) {
		height[i] = `${i}px`;
	}
}

export { height };

위와 같이 유틸리티 클래스 값도 있지만 기존 프로젝트에는 html, body 등 전역에 적용하거나 태그의 속성 자체를 바꾸는 CSS도 있었는데요. html, body 태그는 직접 클래스명을 사용해서 변경할 수 있고 아래 예시처럼 @layer를 이용해서 전체에 적용되는 스타일을 지정할 수 있습니다.

// layout.tsx
import '@/styles/index.css';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <html>
    <!-- 이렇게 클래스를 전달해주거나 -->
	  <body className="min-h-screen">
		  {children}
		</body>
	</html>;
}
// styles/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;

// 클래스명으로만 하기 힘든 전역 설정은 아래처럼 적을 수 있다
@layer base {
 img, picture, video, canvas, svg {
   display: block;
	 max-width: 100%;
	}
}

이렇게 만든 설정을 다른 곳에서도 사용하려면?

프로젝트에 문제없이 설정을 적용했지만 다른 곳에도 이 설정을 가지고 가려면 어떻게 해야 할까요? 기존 방식대로 공통 컴포넌트는 별도의 라이브러리로 분리하려고 하니까 설정을 다시 해줘야 하는 문제가 있었습니다. 물론 간단한 값이라면 두 군데 모두 만들어도 괜찮겠지만 만들고보니 이미 많이 커진 상황이었습니다. 나중을 생각해도 똑같이 관리해줘야 하는 값이 여러 군데 존재하면 유지보수가 어려워질 수밖에 없었습니다.

그렇게 고민하고 해결책을 찾다가 플러그인을 떠올리게 됐습니다. 플러그인을 사용하면 이런 설정을 여러 곳에서 공유할 수 있습니다. 참고로, 지금 보고 계시는 블로그도 typography라는 공식 플러그인을 사용하고 있습니다.

문법은 tailwind.config.js와 거의 동일합니다. 저희는 아래와 같이 작성했습니다.

import plugin from 'tailwindcss/plugin';
import { colors, spacing, borderRadius, fontSize, fontWeight, height, screens, boxShadow } from './theme';

export default plugin(
	function ({ addBase, theme }) {
		// @layer base에서 설정한 값은 CSS가 아니라 여기서 이렇게 설정합니다.
		addBase({
			// 참조: <https://www.joshwcomeau.com/css/custom-css-reset/>
			'*, *::before, *::after': {
				'box-sizing': 'border-box',
			},
			'*': {
				margin: 0,
			},
			// macOS에서만 다르게 나타나는 폰트 문제를 통일
			body: {
				'font-family': theme('fontFamily.sans'),
				'-webkit-font-smoothing': 'antialiased',
				'-moz-osx-font-smoothing': 'grayscale',
			},
			'img, picture, video, canvas, svg': {
				display: 'block',
				'max-width': '100%',
			},
			'input, button, textarea, select': {
				font: 'inherit',
			},
			// z-index 문제를 없애기 위해
			'#root, #__next': {
				isolation: 'isolate',
			},
		});
	},
	// theme 설정으로 기본값을 덮어쓰거나 추가합니다.
	{
		theme: {
			colors,
			spacing,
			fontSize,
			fontWeight,
			screens,
			boxShadow,
			extend: {
				fontFamily: {
					sans: [
						'"Pretendard Variable"',
						'Pretendard',
						'-apple-system',
						'BlinkMacSystemFont',
						'system-ui',
						'Roboto',
						'"Helvetica Neue"',
						'"Segoe UI"',
						'"Apple SD Gothic Neo"',
						'"Noto Sans KR"',
						'"Malgun Gothic"',
						'"Apple Color Emoji"',
						'"Segoe UI Emoji"',
						'"Segoe UI Symbol"',
						'sans-serif',
					],
				},
				borderRadius,
				height,
			},
		},
	},
);

위에서 CSS와 JS를 둘다 활용해서 설정한 것과 달리 플러그인은 모든 설정을 JS로 해야 하기 때문에 약간 다른 모습을 하고 있습니다. 기존에 @layer base 에 넣어준 값이 addBase 아래에 들어간 것을 볼 수 있습니다.

이제 이 플러그인을 사용할 프로젝트에서 불러와서 설정에 넣어줍니다.

// tailwind.config.ts

import type { Config } from 'tailwindcss';

const config: Config = {
  content: [
	  ...
  ],
  plugins: [
    require('@comento/tailwind-plugin'),
		...
  ],
};
export default config;

외부 컴포넌트에 문제가 있을 때

플러그인까지 만들고 나서 개발에 속도를 붙이고 있었습니다. 그런데 어느 날 보니 CSS가 이상하게 적용되는 현상을 발견했습니다. 모두 그런 것은 아니었는데 특정 클래스가 전혀 작동하지 않거나 불러온 컴포넌트가 디자인이 제대로 적용되지 않는 문제가 있었습니다.

개발자 도구를 이용해서 분석하다보니 마치 CSS가 두 번 적용되는 듯한 모습을 찾을 수 있었습니다. 커밋을 하나씩 되돌려 가면서 시점을 분석해보니 컴포넌트 라이브러리를 설치한 시점이었는데요. Tailwind의 작동 방식 때문에 생긴 문제를 해결하려다 보니까 오히려 꼬여버린 문제였습니다.

Tailwind는 파일에서 클래스명이 사용된 것을 찾아서 클래스를 적용시키는 방식을 사용합니다. Tailwind를 처음 사용하면 많이 하는 아래의 실수가 있습니다. Tailwind는 아래 예시처럼 사용하면 어떤 클래스가 쓰일지 파악할 수 없기 때문에 나중에 동적으로 클래스가 정해져도 그 클래스를 사용하지 않습니다.

// ❌
<p className={`text-${props.color}`}>Text</p>

// ✅
<p className={color === 'primary' ? 'text-primary' : 'text-info'}>Text</p>

컴포넌트 라이브러리를 사용했을 때는 Tailwind가 컴포넌트 파일을 보고 있지 않기 때문에 컴포넌트의 CSS가 제대로 적용되지 않고 있는 문제가 있었습니다. 그래서 라이브러리에서 CSS를 컴파일해서 그 CSS를 불러왔는데 그러고 나니 같은 플러그인을 쓰는 라이브러리와 프로젝트의 CSS가 서로 충돌하거나 두 번 불러와지는 문제가 있었습니다.

복잡해보이는 문제였지만 해결법은 굉장히 단순했고 공식 문서에도 나와있는 해결법이었습니다. 바로 이 문서처럼 해주는 것이었습니다.

// tailwind.config.ts
import type { Config } from 'tailwindcss';

const config: Config = {
  content: [
    './src/**/*.{js,ts,jsx,tsx,mdx}',
    // 이렇게 라이브러리의 경로를 지정해준다.
    './node_modules/@comento/design-system/dist/index.{cjs,js}',
  ],
  plugins: [
    require('@comento/tailwind-plugin'),
    require('tailwindcss-animate'),
    require('tailwind-scrollbar'),
  ],
};
export default config;

Tailwind는 content에 설정된 경로의 파일을 분석해서 사용하는 클래스를 결정합니다. 그래서 node_modules는 성능을 위해 일반적으로는 참조하지 않지만 이렇게 라이브러리를 따로 빼서 쓰거나 다른 라이브러리를 가져와서 쓸 때면 그 경로를 참조하게 설정만 해주면 됩니다.

참고로, 글을 쓰는 시점에 알파 버전인 4버전에서는 이런 문제가 설정이 필요 없이 해결될 예정이라고 합니다.

클래스 간 충돌을 막자

클래스 기반의 Tailwind를 쉽게 사용하기 위해 저희는 클래스명 조작을 도와주는 clsx 라이브러리를 사용했습니다. 하지만 clsx와 Tailwind를 같이 사용하면 의도대로 작동하지 않는 경우가 종종 발생합니다.

const isBold = true;
const classNames = clsx('text-regular text-body1', { 'text-semi-bold': isBold });

<p className={classNames}>Bold Text</p> // 글자가 굵게 나오지 않을 수도 있다

위의 예시처럼 코드를 작성하면 반드시 된다는 보장이 없습니다. 스타일 적용의 순서는 클래스의 순서와 무관하기 때문입니다. 반드시 안되는 것은 아니고 되는 경우도 있기 때문에 파악하기 더 어려운 문제입니다.

그래서 같이 사용하는 라이브러리가 바로 tailwind-merge입니다. 이 라이브러리는 Tailwind를 사용할 때 클래스 간의 충돌을 없애주는 클래스입니다. 그래서 다음과 같은 유틸 함수를 생성했습니다.

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

다만, 바로 사용할 수는 없었는데요. 앞에서 저희가 테마를 커스텀해서 사용하고 있었기 때문입니다. 특히 몇몇 클래스는 이름까지 전부 바꿔서 사용하고 있기 때문에 twMerge가 알 수가 없는데요. 다행히 이런 문제를 처리하기 위한 설정도 있습니다. 문서를 참고해서 다음과 같이 설정하고 사용했습니다.

const customTwMerge = extendTailwindMerge({
  extend: {
    classGroups: {
      // 이렇게 해야 저희가 설정한 색상과 텍스트 클래스가 병합되지 않습니다.
      'font-size': [
        'text-display1',
        'text-headline1',
        'text-headline2',
        'text-headline3',
        'text-headline4',
        'text-headline5',
        'text-headline6',
        'text-headline7',
        'text-body1',
        'text-body2',
        'text-caption1',
        'text-caption2',
      ],
      // tailwind-animate라는 라이브러리를 사용할 때도 문제가 있어 등록했습니다.
      animate: ['animate-in', 'animate-out', 'animate-none'],
    },
  },
});

export function cn(...args: ClassValue[]) {
  // 위에서 설정한 customTwMerge를 사용합니다.
  return customTwMerge(clsx(...args));
}

모든 개발자가 같은 규칙으로

이후로는 Tailwind가 원하는 대로 작동하지 않는 문제는 거의 없었습니다. 하지만 어떤 규칙으로 Tailwind의 클래스를 정리해야 일관되게 쓸 수 있을까하는 고민을 하게 되었습니다. 혼자서도 명확한 규칙 없이 클래스 순서를 쓰고 있다는 느낌이 들었는데 모두가 다같이 쓴다면 더 해결하기 어려운 문제일 것이 분명했습니다.

이미 많은 분이 겪은 문제여서 그런지 해결책도 이미 있었습니다. 가장 먼저 찾은 것은 Tailwind의 공식 Prettier 플러그인이었습니다. 이 플러그인을 사용하면 클래스명의 순서가 항상 일정 규칙에 맞춰 정렬됩니다. 무엇보다 자동 수정을 지원하기 때문에 실수로 누락하거나 다른 순서를 커밋할 일도 없고요.

하지만 조금 더 찾아보니 커뮤니티에서 관리하는 이 플러그인을 찾을 수 있었습니다. 이 플러그인은 공식 플러그인의 클래스명 순서 규칙보다 조금 더 많은 규칙을 가지고 있습니다. 역시 대부분의 규칙이 자동 수정을 지원하고요. 팀에서 선택한 규칙은 다음과 같습니다.

  1. classnames-order: 클래스명의 순서를 강제합니다. 모든 사람이 같은 규칙으로 클래스를 정렬합니다.
  2. enforces-shorthand: 복수의 클래스가 하나로 줄어들 수 있다면 줄입니다. 예를 들어 pt-4 pb-4py-4로 줄일 수 있습니다.
  3. migration-from-tailwind2: Tailwind는 현재 3버전이지만 2버전 클래스명을 여전히 지원합니다. 이 규칙은 구버전의 클래스명을 자동으로 새 버전의 클래스명으로 바꿔줍니다.
  4. no-custom-classname: Tailwind의 클래스가 아닌 다른 클래스의 사용을 금지합니다. 실수로 클래스명을 잘못 입력했을 때 찾아내기도 좋은 규칙입니다.
  5. no-contradicting-classname: 서로 모순되는 클래스명을 사용할 수 없습니다. 실수로 덮어써버리거나 적용되지 않는 클래스를 쓰는 경우를 방지합니다.

이렇게 사용하면 모든 개발자가 같은 모습으로 Tailwind를 사용할 수 있게 됩니다. 추가 설정으로 className 속성뿐 아니라 위에서 만든 cn 함수 등에서도 사용할 수 있습니다.

// .eslintrc.js
module.exports = {
  ...
	settings: {
		tailwindcss: {
		  // 이렇게 하면 아래 함수에서도 작동합니다.
		  callees: ['classnames', 'clsx', 'cn'],
		  // 일반 변수에 사용할 수 없어서 tw라는 템플릿 함수를 만들고 사용
		  tags: ['tw'],
		}
	}
	...
}

// 일반 변수 사용 예시
const classNames = tw`bg-white p-4`;

마무리

이렇게 새 프로젝트에서 사용할 Tailwind의 설정을 마쳤습니다. 처음에 팀에서 Tailwind를 쓰자고 할 때는 이렇게 많은 설정이 필요할 것이라고 생각하지 못했었는데요. 혼자서 가볍게 써볼 때는 별다른 설정 없이 쓸 수 있다는 것도 큰 장점이라 생각한 프레임워크인데 역시 팀이 앞으로 다같이 쓰는 상황에서 설정하는 것은 훨씬 복잡한 일이었습니다. 하지만 이런 경험으로 Tailwind가 과연 규모가 커져도 괜찮을까 하던 의문도 많이 해소할 수 있었습니다. 이번 글이 Tailwind 설정을 앞두고 계시거나 설정 중에 문제와 마주친 분에게 도움이 되었으면 좋겠습니다. 굉장히 긴 글이 되었는데 읽어주셔서 감사합니다.


커리어의 성장을 돕는 코멘토에서 언제나 함께 성장할 개발자를 기다리고 있습니다. 채용 페이지에서 코멘토가 어떤 회사인지, 어떤 사람을 찾는지 더 자세히 확인해보세요. 😊