Tailwind CSS 잘 사용하기

우현민·2025년 8월 4일
15

dev

목록 보기
10/10
post-thumbnail

어느덧 Tailwind CSS가 프론트엔드 생태계에서 주류 CSS 라이브러리로 올라온 지도 꽤 많은 시간이 지났습니다. 거부하시는 분들도 많고 추종하시는 분들도 많은 호불호가 정말 심한 기술인데요, 개인적으로는 추종할 만 한 포인트도 분명 있고 싫어할 만 한 포인트도 분명 있기에 잘 쓰면 매우 좋은 기술이라고 생각합니다. 이번 글에서는 Tailwind CSS에 대한 몇 가지 오해를 풀고, 이 기술을 잘 사용하는 법을 소개하려 합니다.

혹시 이 글을 읽는 분들 중 Tailwind CSS가 처음인 분이 계실까 하여 짤막하게 소개드리면 <div class="flex flex-col gap-2"> 와 같이 class에 직접 유틸성 스타일을 넣어 두는 기술입니다. 자세한 내용은 이 글에서 다루기엔 너무 길어서, 공식문서를 참고해 주세요.

Tailwind CSS의 철학

Tailwind CSS는 스스로를 utility-first CSS framework 라고 소개합니다. 유틸성으로 사용할 수 있는 class들을 정의해두고 html tag에서 조립해서 사용한다는 것인데요, 위에서 봤던 이 div 태그는 아래와 같이 적용됩니다.

HTMLtailwind가 빌드 시 자동 생성하는 CSS
<!-- 개발자가 작성한 html -->
<div class="flex flex-col gap-2">
.flex { display: flex; }
.flex-col { flex-direction: column; }
.gap-2 { gap: 0.5rem; }

위와 같이,
1. 빌드 시 Tailwind CSS 가 코드를 문자열로 읽으면서
2. Tailwind CSS가 미리 정의해 둔 utility class에 해당하는 게 있다면
3. 해당 utility class를 빌드된 css에 넣어줍니다.

CSS Modulesstyled components와 같이 최근 몇 년 동안 주류였던 기술들과는 꽤 다른 접근 방법입니다.

styled components 등Tailwind CSS
하나의 HTML 요소에 하나의 unique한 class를 할당
하나의 class가 여러 CSS 속성을 담당
하나의 HTML 요소에 여러 utility class를 할당
하나의 class가 1~2개의 CSS 속성을 담당

Tailwind CSS를 만든 Adam Wathan 이 2017년에 작성한 블로그 글: CSS Utility Classes and "Sparation of Concerns"를 보면 왜 이런 방안을 채택했는지 알 수 있는데요, 한 번쯤 읽어보시는 걸 추천드립니다.



이야기거리들

앞서 언급했듯 호불호가 매우 심한 라이브러리이고 쟁점도 많습니다. 관련 몇 가지 주제를 잡고 하나씩 이야기를 풀어보려 합니다.

마크업과 스타일의 관심사 분리가 되지 않는다?

개인적으로는 공감하지 않는 주장입니다. 마크업과 스타일은 애초에 관심사 분리가 되지 않는 게 더 낫다고 생각합니다.

마크업과 스타일이 관심사 분리되어 개발된 간단한 컴포넌트를 보겠습니다. BEM 기반으로 작성했지만, CSS Modulesstyled-components 등도 사실 이 관점에서 차이는 없습니다.

HTMLCSS
<!-- 개발자가 작성한 html -->
<div class="modal__header">
  <h2 class="modal__title">정보 상세 조회</h2>
  <button>
    <IconX />
  </button>
</div>
.modal__header {
  display: flex;
  align-items: center;
  gap: 8px;
}

.modal__title {
  flex: 1;
}

흔한 코드인데요, 잠시 시간을 들여 생각해보면 조금 어색합니다.

  • HTMLdivh2 태그는 CSS 코드가 가진 지식인 modal__headermodal__title 이라는 선택자가 있다는 사실에 의존합니다.
  • CSSmodal__title 선택자는 HTML 코드가 가진 지식인 나를 감싸는 부모는 display: flex 속성이 있을 것이다는 사실에 의존합니다.

사실 웹에서 마크업 구조와 스타일시트는 서로 매우 강하게 의존합니다. 부모의 display: flex와 자식의 flex: 1, 부모의 position: absolute 와 자식의 position: relative, 부모의 grid-template-columns: 1fr 1fr 와 자식의 grid-column: span 2 / span 2 등등 많은 css 코드는 마크업 구조가 자신의 의도와 맞아야만 정상 동작하므로 많은 CSS 코드는 HTML에 의존합니다. 그리고 HTML은 보통 CSS 선택자를 불러와서 사용하므로 CSS에 의존합니다.

오히려 styled-componentsCSS Modules와 같은 기술을 사용하면 이렇게 서로 의존하는 결합된 모듈이 분리되어 있는 바람에 우리 프론트엔드 개발자들은 파일을 두 개 켜 놓고 혹은 파일 위아래로 계속 왔다갔다하면서 개발해야 하는 불편을 항상 겪어왔습니다.

따라서 마크업과 스타일시트는 그냥 결합해버리는 게 더 깔끔합니다. 하나의 화면이 여러 스타일을 가질 것도 아니고 HTML이 있는 곳에 CSS가 지원되지 않을 상황이 생길 것도 아니기에 마크업과 스타일이 관심사 분리가 되어야 할 이유는 딱히 없습니다. 옛날에는 하나의 마크업에 여러 스타일을 줄 수 있다는 장점이라도 있었으나, 디자인이 브랜딩의 한 축이 된 현재 IT 생태계에서는 하나의 마크업에 여러 스타일을 줄 상황이 생기지 않습니다.

아래와 같은 Tailwind CSS의 방식이 오히려 결합해야 할 것을 적절히 결합했기에 관심사 측면에서 더 좋습니다.

<div class="flex items-center gap-2">
  <h2 class="flex-1">정보 상세 조회</h2>
  <button>
    <IconX />
  </button>
</div>

인라인 스타일과 비슷해서 거부감이 느껴진다는 의견도 있는데요, 사실 인라인 스타일은 성능, 기능, 확장성 측면에서 문제가 있어서 나쁜 것이고 코드 유지보수성이나 생산성 관점에서는 나쁜 기술이 아닙니다.


조금 더 나아가서, 모던 웹 프레임워크들은 모두 컴포넌트 기반으로 설계되었습니다. 가령 가장 대중적인 React도 마찬가지로 컴포넌트 하나가 마크업(html), 스타일(css), 로직(js) 정보를 모두 가지도록 설계됩니다. 이들 모두를 합쳐 두면 관심사 분리가 되지 않는다는 느낌을 받게 되는 게 사실입니다.

그런데 여기서 조금 더 생각해 보면, 사실 (html+js) <-> css 로 분리하는 것보다 (html+css) <-> js 로 분리하는 게 아키텍처나 테스팅, 실제 효용 측면에서 더 자연스럽고 유용하고 일리있습니다. 이렇게 분리하는 방법이 더 궁금하시다면 제 다른 글 복잡한 컴포넌트에서 뷰와 로직 분리하기을 읽어주시면 좋을 것 같습니다.



스타일 가독성이 떨어진다?

개인적으로 절반 정도 공감하는 주장입니다. 앞 섹션의 예시와 같은 희망편도 있는 반면 <div className="mx-auto mt-10 w-full max-w-md rounded-2xl border border-red-500/30 bg-gradient-to-b from-zinc-900 to-black p-6 font-bold font-[Pretendard] text-2xl text-center text-zinc-100 shadow-[0_10px_40px_-10px_rgba(220,38,38,0.4)] relative after:absolute after:inset-0 after:hidden hover:after:block after:bg-red 와 같은 절망편도 있습니다. 이렇게 가독성이 떨어지면 어떤 스타일이 적용되어 있는지 파악하기가 어렵고 버그가 발생할 확률이 높아지며 눈이 피로해집니다. 이 문제를 해결하기 위해서는 몇 가지 내부/외부적인 제한이 필요한데요,


스타일이 디자인 시스템 토큰화되어 있어야 합니다

제품의 룩앤필을 통일하기 위해 스타일은 적절한 단위로 재사용되는 경우가 많습니다. 많은 회사들에서는 이를 더 발전시켜 디자인 시스템 토큰으로 만들어둡니다.

가령 Typography가 잘 토큰화되어 있다면 이를 Tailwind CSS config에 등록해둔 다음 font-bold font-[Pretendard] text-2xl leading-1.5 로 작성할 것을 text-title3 과 같은 토큰으로 짧게 작성할 수 있어 마크업 코드 길이가 크게 개선됩니다. 특히 폰트가 토큰화되어있는지 아닌지가 체감상 큰 차이를 줍니다.

box-shadow 같은 것도 마찬가지로, 잘 토큰화되어 있다면 shadow-[0_10px_40px_-10px_rgba(220,38,38,0.4)] 로 쓸 것을 shadow-s 로 쓸 수 있습니다.

반대로 말하면 이런 스타일들이 토큰화되어있지 않아 매번 반복해서 작성해야 할 경우에는 class가 실제로 너무 길어지기에 힘들어질 수 있습니다. 이런 상황이라면 Tailwind CSS의 단점이 더욱 부각되므로 Tailwind CSS를 사용하지 않을 이유가 매우 커집니다.


state variant 기능은 Tailwind CSS와 궁합이 좋진 않습니다

disabled: after: md: 와 같은 기능을 사용하는 순간 class 길이가 두 배 세 배로 늘어나기에 가독성이 크게 떨어지는 게 사실입니다. 이런 문제를 해결하기 위해 아래와 같이 강제로 문자열을 분리하고 js로 합치는 컨벤션도 있지만..

<button
  className={clsx(
    'mx-auto mt-10 w-full max-w-md bg-gray-500',
    'hover:opacity-0.8',
    'disabled:opacity-0.7 disabled:cursor-default',
  )}
/>

개인적으로는 hover:after: 와 같이 두 개 이상의 variant를 섞어 쓰는 경우에는 애매해지기도 하고 사람이 코드 짜다 보면 실수할 여지도 크다고 생각해서 그리 선호하진 않습니다. 이런 걸 할 일이 생기는 경우 컴포넌트를 잘 분리해둔 다음 그냥 CSS Modules를 사용하는 것을 선호합니다.

import { Button } from '@/components/button';
...
  return <Button className="mt-10" color="gray" /> // Button은 내부적으로 CSS Modules

// 혹은

import styles from '@/utils/style.module.css'; // CSS Modules className
...
  return <button className={clsx(styles.overlay, 'mx-auto mt-10 w-full max-w-md bg-gray-500')} />

lint 를 통해 class 정렬을 해 주면 큰 도움이 됩니다.

공식문서에서 볼 수 있듯 Tailwind CSS는 자체적으로 권장하는 class들의 정렬 순서가 있고, prettier-plugin-tailwindcss 등을 통해 이를 자동 정렬하도록 권장합니다. 이를 통해 class 순서를 자동으로 정렬해두면 position이나 display 등 다른 요소와 관계가 있을 속성들은 앞으로 오고 background-color 등 다른 요소와 무관할 속성들은 뒤로 빠지기 때문에 코드가 한결 깔끔해집니다.

개인적으로는 prettier plugin 보다는 eslint-plugin-better-tailwindcss를 더 추천하는데요, eslint plugin에는 prettier plugin에 더해 잘못된 class들을 잡아주는 기능까지 있기 때문입니다. 가령 class="felx" 처럼 오타가 나면 린트 에러가 납니다. Tailwind CSS를 사용하다 보면 의외로 오타 때문에 무의미한 class 문자열이 들어가는 경우가 종종 생기는데요, 이런 케이스를 잡아주기 때문에 꽤 큰 도움이 됩니다.



LLM과 궁합이 좋습니다

예전에 GeekNews에도 관련 글이 올라왔었는데요, 여러 LLM 모델들은 다른 어떤 방법론들보다도 Tailwind CSS 코드로 응답하는 것을 선호하고 잘 하는 경향이 있습니다. 몇 년 후엔 모델이 발전함에 따라 이런 차이가 사실상 무의미해질 수도 있겠으나 지금은 분명 Tailwind CSS를 선택할 이유가 되어 주는 장점입니다.



하지만, 우리 프로젝트를 믿고 맡길 수 있을 만큼 훌륭한 라이브러리는 아닙니다

Tailwind CSS를 사용하다 보면 이런저런 버그들을 많이 만나게 됩니다. 메인테이너 입장에 공감해보자면 라이브러리에 이것저것 기능을 추가하다 보면 생각지도 못한 엣지케이스들이 생기는 건 어쩔 수 없겠지만, 테스트도 어렵고 뭔가 잘못되었을 때 타입 에러도 나지 않는 스타일 라이브러리이기에 이 점이 우리 개발자들에게 꽤 크리티컬하게 다가옵니다. React 처럼 canary 배포라도 충분히 하면서 심사숙고해서 배포해야 하지 않나 싶은데, tailwindlabs 팀은 꽤 과감하게 배포하더라구요.

이에 더해 최근에 v3 -> v4 로 올리는 과정에서 config 파일에 대한 breaking change도 시원하게 만들어졌고 (여전히 js config를 지원하긴 하지만, 그리 큰 효용이 없는데 방향성이 변경되었다는 것이라는 점에서 아쉬웠습니다), v4의 브라우저 호환성은 Chrome 111+ / Safari 16.4+ 로 많은 회사들에서 채택을 꺼릴 만 한 수준입니다. 또한 경량 라이브러리라고 스스로 소개하면서도 자체 제공하는 스타일인 preflight는 10kB가 넘는 수준으로 성능 최적화 관점에서는 거슬리는 크기입니다. 마지막으로 Utility First라고 주장하면서도 너무 유틸라이브러리보다는 설계에 관여하는 프레임워크성이라 Tailwind CSS가 원하는 방식대로 코드를 짜 줘야 합니다.

이런 여러 측면을 고려했을 때 저는 Tailwind CSS가 React처럼 우리 프로젝트를 맡기고 전적으로 믿을 수 있는 훌륭한 라이브러리라고 생각하지는 않습니다. 언제든 이젠 아니라는 생각이 들면 unocss 와 같이 Tailwind CSS의 class들이 대부분 호환되는 다른 라이브러리로 옮길 수 있어야겠다고 생각하기에 Tailwind CSS가 제공하는 has-**: 같은 고급 기능들은 가급적 사용을 지양하고 있으며, 저런 선택자를 사용할 일이 있다면 그냥 CSS Modules 로 작업하고 있습니다.



결론

Utility First 방법론은 상황이 맞는다면 분명 도입했을 때 큰 장점이 있는 방법론이고, Tailwind CSS는 그 진영의 대표주자로 큰 생태계를 가지고 있습니다. 장점도 단점도 있지만 분명 현재 프론트엔드 생태계에서 상당한 비중을 차지하는 라이브러리입니다.

혹시 Tailwind CSS를 한 번도 사용해보지 않았고 도입을 고민 중이시라면, 한 번쯤 꼭 도입해보시는 걸 추천드립니다. 특히 flex flex-col gap-3 p-4 같은 걸 짤 때 생산성이 정말 좋아집니다.

Tailwind CSS를 바라보는 제 시선을 많이 바꿔주고 이 글에도 많은 영감이 되어 주었던, 테오가 작성해주신 블로그 글: 세상 귀여운 on-demand Atomic CSS: AdorableCSS를 소개합니다.를 추천하며 글을 마칩니다.

profile
프론트엔드 개발자입니다

2개의 댓글

comment-user-thumbnail
2025년 8월 5일

글 잘 봤습니다! 저도 :md , :lg 이런 문법 너무 별로라고 생각합니다..

1개의 답글