
어느덧 Tailwind CSS가 프론트엔드 생태계에서 주류 CSS 라이브러리로 올라온 지도 꽤 많은 시간이 지났습니다. 거부하시는 분들도 많고 추종하시는 분들도 많은 호불호가 정말 심한 기술인데요, 개인적으로는 추종할 만 한 포인트도 분명 있고 싫어할 만 한 포인트도 분명 있기에 잘 쓰면 매우 좋은 기술이라고 생각합니다. 이번 글에서는 Tailwind CSS에 대한 몇 가지 오해를 풀고, 이 기술을 잘 사용하는 법을 소개하려 합니다.
혹시 이 글을 읽는 분들 중 Tailwind CSS가 처음인 분이 계실까 하여 짤막하게 소개드리면 <div class="flex flex-col gap-2"> 와 같이 class에 직접 유틸성 스타일을 넣어 두는 기술입니다. 자세한 내용은 이 글에서 다루기엔 너무 길어서, 공식문서를 참고해 주세요.
Tailwind CSS는 스스로를 utility-first CSS framework 라고 소개합니다. 유틸성으로 사용할 수 있는 class들을 정의해두고 html tag에서 조립해서 사용한다는 것인데요, 위에서 봤던 이 div 태그는 아래와 같이 적용됩니다.
| HTML | tailwind가 빌드 시 자동 생성하는 CSS |
|---|---|
|
|
위와 같이,
1. 빌드 시 Tailwind CSS 가 코드를 문자열로 읽으면서
2. Tailwind CSS가 미리 정의해 둔 utility class에 해당하는 게 있다면
3. 해당 utility class를 빌드된 css에 넣어줍니다.
CSS Modules 나 styled 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 Modules 나 styled-components 등도 사실 이 관점에서 차이는 없습니다.
| HTML | CSS |
|---|---|
|
|
흔한 코드인데요, 잠시 시간을 들여 생각해보면 조금 어색합니다.
HTML의 div 와 h2 태그는 CSS 코드가 가진 지식인 modal__header 와 modal__title 이라는 선택자가 있다는 사실에 의존합니다.CSS의 modal__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-components 나 CSS 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를 사용하지 않을 이유가 매우 커집니다.
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')} />
공식문서에서 볼 수 있듯 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 문자열이 들어가는 경우가 종종 생기는데요, 이런 케이스를 잡아주기 때문에 꽤 큰 도움이 됩니다.
예전에 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를 소개합니다.를 추천하며 글을 마칩니다.
글 잘 봤습니다! 저도 :md , :lg 이런 문법 너무 별로라고 생각합니다..