뒤로가기
Tailwind CSS vs CSS Modules, 6개월 사용 후기

September 2, 2024

cssfrontend

CSS Modules를 처음 도입한 건 2년 전, 커머스 프로젝트에서였다. 그 전까지는 글로벌 CSS에 BEM 네이밍을 쓰고 있었는데, 팀원이 6명으로 늘면서 클래스 이름 충돌이 슬슬 문제가 됐다. .card-title을 내가 쓰고 있는데 다른 팀원도 같은 이름을 쓰는 거다. 빌드해보면 스타일이 엉뚱하게 먹혀 있고, 원인을 찾으려면 CSS 파일 여러 개를 뒤져야 했다. CSS Modules는 그 문제를 깔끔하게 해결해줬다. 파일 단위로 스코프가 잡히니까 이름 충돌 걱정이 없었다.

6개월 동안 CSS Modules로 프로젝트를 마무리했고, 바로 다음 프로젝트에서 Tailwind를 도입했다. 이번 글은 기능 비교가 아니다. 두 프로젝트를 연속으로 경험하면서 느낀 것들을 시간 순서대로 풀어본다.

CSS Modules 6개월: 안정적이었지만 느렸다#

CSS Modules의 첫인상은 "편안하다"였다. CSS를 아는 사람이면 바로 쓸 수 있다. 별도의 문법을 배울 필요가 없다. .module.css 파일을 만들고 import하면 끝이다.

tsx
import styles from "./ProductCard.module.css";

function ProductCard({ product }: Props) {
  return (
    <div className={styles.card}>
      <img className={styles.image} src={product.thumbnail} />
      <div className={styles.content}>
        <h3 className={styles.title}>{product.name}</h3>
        <p className={styles.price}>{product.price.toLocaleString()}</p>
      </div>
    </div>
  );
}
css
.card {
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid #e5e7eb;
  transition: box-shadow 0.2s;
}
.card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.image {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}
.content {
  padding: 16px;
}
.title {
  font-size: 16px;
  font-weight: 600;
  color: #111827;
  margin-bottom: 4px;
}
.price {
  font-size: 14px;
  color: #6b7280;
}

깔끔하다. 컴포넌트 파일에는 구조만 보이고, 스타일은 CSS 파일에 따로 있다. 관심사의 분리. 교과서적인 패턴이다.

문제는 속도였다. "속도"라고 하면 런타임 퍼포먼스를 떠올릴 수 있는데, 여기서 말하는 건 개발 속도다. 컴포넌트 하나를 만들 때마다 파일을 두 개 왔다 갔다 해야 했다. JSX를 쓰다가 "아, padding을 넣어야지" 하면 CSS 파일로 탭을 전환하고, 클래스 이름을 짓고, 스타일을 쓰고, 다시 JSX로 돌아온다. 이 컨텍스트 스위칭이 쌓이면 하루에 30분은 날리는 것 같았다.

클래스 이름 짓기도 은근히 소모적이었다. .container? .wrapper? .inner? .content? 시맨틱하게 지으려고 하면 고민이 길어지고, 대충 지으면 나중에 뭐가 뭔지 모른다. 한 컴포넌트 안에 .top-section, .middle-area, .bottom-part 같은 이름이 늘어나는 걸 보면서 "이게 맞나" 싶었다.

그래도 CSS Modules가 빛나는 순간이 있었다. 디자이너와 함께 복잡한 애니메이션을 구현할 때였다. 상품 카드에 호버하면 이미지가 살짝 확대되면서 그라데이션 오버레이가 올라오고, 가격 태그가 슬라이드 인 되는 인터랙션이었다. @keyframes를 정의하고, pseudo-element로 오버레이를 만들고, transition-delay로 순서를 조절하고. 이런 세밀한 CSS 작업은 CSS 파일에서 하는 게 자연스러웠다.

css
.card {
  position: relative;
  overflow: hidden;
}
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(transparent 40%, rgba(0, 0, 0, 0.6));
  opacity: 0;
  transition: opacity 0.3s ease;
}
.card:hover::after {
  opacity: 1;
}
.card:hover .image {
  transform: scale(1.05);
}
.card:hover .priceTag {
  transform: translateY(0);
  opacity: 1;
  transition-delay: 0.15s;
}
.priceTag {
  transform: translateY(20px);
  opacity: 0;
  transition: transform 0.3s ease, opacity 0.3s ease;
}

이 정도 복잡한 인터랙션을 유틸리티 클래스로 표현하려면 상당히 고통스럽다. CSS의 cascade와 pseudo-element, @keyframes를 자유롭게 쓸 수 있는 환경이 필요한 순간이 분명 있다.

디자인 시스템과의 궁합도 좋았다. CSS Custom Properties로 토큰을 관리하고, 모듈에서 그걸 참조하는 패턴이 깔끔하게 동작했다.

css
:root {
  --color-primary: #2563eb;
  --spacing-md: 16px;
  --radius-md: 8px;
}
.button {
  background: var(--color-primary);
  padding: var(--spacing-sm) var(--spacing-md);
  border-radius: var(--radius-md);
}

디자이너가 피그마에서 정의한 토큰 이름을 그대로 CSS 변수로 옮기면 됐다. 디자인과 코드 사이의 번역 비용이 낮았다.

Tailwind 도입: 디자이너와의 라이브 세션에서 결정적 순간#

다음 프로젝트는 대시보드 서비스였다. 디자이너 한 명과 프론트 둘이서 빠르게 MVP를 만들어야 하는 상황이었다. 팀 리드가 "Tailwind 한번 써볼까"라고 제안했고, 나는 솔직히 회의적이었다. className에 유틸리티 클래스가 20개씩 붙어 있는 코드를 봤을 때 "유지보수가 된다고?" 싶었으니까.

첫 주는 적응 기간이었다. justify-betweenjustify-content: space-between이라는 걸 매번 문서에서 찾아봤다. p-4padding: 1rem이라는 것도 외워야 했다. 진입 장벽이 확실히 있었다.

그런데 2주차부터 흐름이 바뀌었다. 디자이너가 피그마에서 화면을 보여주면서 "이거 이렇게 바꿔보면 어때요?"라고 할 때, 내가 실시간으로 코드를 고칠 수 있었다. CSS 파일을 열 필요가 없었다. 클래스 이름을 고민할 필요도 없었다. p-4p-6으로 바꾸고, text-gray-500text-gray-700으로 바꾸면 바로 반영됐다.

tsx
function StatCard({ label, value, trend }: Props) {
  return (
    <div className="rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
      <p className="text-sm font-medium text-gray-500">{label}</p>
      <p className="mt-1 text-2xl font-bold text-gray-900">{value}</p>
      <span
        className={`mt-2 inline-flex items-center text-xs font-medium ${
          trend > 0 ? "text-green-600" : "text-red-600"
        }`}
      >
        {trend > 0 ? "+" : ""}
        {trend}%
      </span>
    </div>
  );
}

파일 하나. CSS 파일 없음. 디자이너 옆에서 "padding 좀 더 줄까요?" "그림자 좀 더 넣을까요?" 하면서 1~2초 만에 반영한다. 이 경험이 결정적이었다. 프로토타이핑 속도가 CSS Modules 때와 비교가 안 됐다.

2~3일이 지나니까 자주 쓰는 클래스는 거의 다 외웠다. 그리고 VSCode의 Tailwind 확장이 자동완성을 워낙 잘 해줘서, 정확한 이름을 몰라도 타이핑하다 보면 나왔다.

가독성에 대한 생각이 바뀌다#

CSS Modules를 쓸 때는 className={styles.title}className="text-base font-semibold text-gray-900 mb-1"보다 당연히 읽기 좋다고 생각했다.

6개월 동안 Tailwind를 쓰고 나니 생각이 달라졌다. CSS Modules의 styles.title은 "무엇을 하는지"를 숨긴다. CSS 파일을 열어봐야 비로소 "아, 16px이고 semibold이고 어두운 회색이구나"를 안다. 반면 Tailwind는 스타일이 바로 보인다. 코드 리뷰할 때 CSS 파일을 따로 열 필요 없이, JSX만 보면 이 요소가 어떻게 보이는지 바로 파악된다.

물론 클래스가 길어지면 고통스럽다.

tsx
<div className="flex items-center justify-between px-4 py-3 bg-white border-b border-gray-200 sticky top-0 z-10 shadow-sm">

이 정도 되면 구조보다 스타일이 먼저 눈에 들어와서 읽기 싫어진다. 해결법은 컴포넌트로 추출하는 거다.

tsx
function StickyHeader({ children }: { children: ReactNode }) {
  return (
    <div className="flex items-center justify-between px-4 py-3 bg-white border-b border-gray-200 sticky top-0 z-10 shadow-sm">
      {children}
    </div>
  );
}

스타일 때문에 컴포넌트를 쪼개게 되는데, 이게 사실 나쁘지 않다. 자연스럽게 컴포넌트 분리가 유도된다.

CSS를 "제대로" 아는 게 먼저라는 결론#

두 도구를 쓰면서 느낀 건, 결국 CSS 자체를 이해하고 있는 게 중요하다는 거다. 한 유명 프론트엔드 교육자가 이런 글을 쓴 적이 있다. CSS는 개별 속성을 외우는 게 아니라, 속성이 입력으로 들어가는 "레이아웃 알고리즘"을 이해해야 한다고. z-index가 왜 안 먹히는지 모르면, 그건 Tailwind를 쓰든 CSS Modules를 쓰든 똑같이 모르는 거다. 도구가 문제가 아니라 기초가 문제인 거다.

CSS를 진지하게 공부하는 사람이 드물다는 것도 공감가는 말이었다. 대부분의 프론트엔드 개발자가 JavaScript에 집중하고 CSS는 "대충 되니까 넘어가는" 영역으로 취급한다. 그러다 stacking context를 몰라서 모달 z-index 전쟁이 벌어지고, containing block을 몰라서 position: absolute가 왜 이상하게 동작하는지 디버깅에 1시간을 쓴다.

방어적 CSS라는 개념도 인상적이었다. 미래에 콘텐츠가 바뀌어도 깨지지 않는 CSS를 쓴다는 발상이다. 텍스트가 길어지면? 이미지 비율이 달라지면? 컨테이너가 좁아지면? 이런 엣지 케이스를 미리 고려해서 CSS를 작성하는 습관. 이건 Tailwind에서든 CSS Modules에서든 동일하게 적용되는 원칙이다.

팀 온보딩과 빌드 결과물#

새 팀원이 합류할 때 어느 쪽이 더 쉬운가. CSS Modules는 "그냥 CSS"라서 진입장벽이 낮다. 하지만 "이 프로젝트의 CSS 규칙"을 파악하는 데는 시간이 걸린다. 카드 컴포넌트의 padding은 보통 얼마인지, 색상 체계는 어떤지, CSS 파일을 이곳저곳 열어봐야 파악된다.

Tailwind는 초반 학습비용이 있지만, 일단 익히면 코드 자체에 스타일이 다 드러나 있으니까 프로젝트 파악이 빠르다. p-4, text-gray-500 같은 클래스가 프로젝트 전체에서 일관되게 쓰인다.

빌드 결과물 크기는 Tailwind이 유리하다. 사용하지 않는 클래스를 자동 제거해서 CSS 번들이 보통 10KB 이내다. CSS Modules는 컴포넌트가 늘면 CSS도 비례해서 늘어난다. 다만 CSS가 실제로 성능 병목이 되는 경우는 거의 없어서, 이건 결정적인 차이라기보다는 "알아두면 좋은" 정도다.

결론: 도구가 아니라 맥락이 결정한다#

지금 새 프로젝트를 시작한다면 나는 Tailwind을 고른다. 개발 속도의 차이가 크고, 특히 디자이너와 빠르게 이터레이션해야 하는 상황에서 Tailwind의 장점이 압도적이다.

하지만 모든 상황에서 그런 건 아니다. 복잡한 CSS 애니메이션이 많은 프로젝트, 디자인 시스템을 CSS Custom Properties 기반으로 정교하게 관리하는 프로젝트에서는 CSS Modules가 더 자연스럽다. @keyframes를 Tailwind config에 정의하는 건 CSS 파일에서 직접 쓰는 것보다 확실히 불편하다. pseudo-element를 쓸 때도 마찬가지다.

한 가지 더, 두 방식을 섞어 쓸 수도 있다. 대부분의 레이아웃과 간격은 Tailwind으로 빠르게 처리하고, 복잡한 애니메이션이나 pseudo-element가 필요한 부분만 CSS 파일을 쓰는 거다. 프로젝트에서 실제로 이렇게 했는데, 규칙만 잘 정해두면 혼란 없이 동작했다. "유틸리티 클래스 5개 이상이면 컴포넌트로 추출", "애니메이션은 CSS 파일에서" 정도의 팀 규칙이면 충분했다.

결국 팀의 성향과 프로젝트의 특성이 결정의 중심이어야 한다. CSS를 깊이 있게 다루는 걸 좋아하고, 복잡한 인터랙션이 많은 팀이라면 CSS Modules가 맞다. "CSS는 빨리 끝내고 로직에 집중하고 싶다"는 팀이라면 Tailwind이 맞다. 어떤 도구를 쓰든, CSS 자체를 제대로 이해하고 있는 게 전제 조건이다. 도구는 결국 도구일 뿐이고, 기초가 탄탄해야 어떤 도구든 제대로 쓸 수 있다.