CSS-in-JS 없이 스타일링 — CSS Modules + Tailwind 실전 조합

게시일: 2025년 12월 5일 · 13분 읽기

styled-components의 런타임 오버헤드가 눈에 보이기 시작했다. 페이지가 약간 느려지는 것을 느꼈고, 번들 크기도 너무 컸다. 그래서 CSS Modules로 돌아왔다. 하지만 CSS Modules도 문제가 있다. Tailwind와의 조합으로 양쪽의 장점을 모두 가질 수 있다.

CSS-in-JS의 문제

런타임 오버헤드

CSS-in-JS는 자바스크립트가 실행되면서 CSS를 생성한다. 이는 다음을 의미한다:


// styled-components - 런타임에 실행됨
import styled from 'styled-components';

const Button = styled.button\`
  background: #007bff;
  padding: 10px 20px;
  border-radius: 4px;
\`;

// 이 코드가 렌더링될 때마다 스타일이 계산되고 DOM에 주입됨

번들 크기

styled-components는 약 15KB의 번들 크기를 차지한다. 작은 프로젝트에서는 큰 오버헤드다.

디버깅 어려움

CSS-in-JS는 generated class name을 사용하므로, DevTools에서 디버깅이 어렵다.

CSS Modules의 장점

CSS Modules는 파일 단위로 CSS 범위를 분리한다.


/* Button.module.css */
.button {
  background: #007bff;
  padding: 10px 20px;
  border-radius: 4px;
  border: none;
  cursor: pointer;
}

.button:hover {
  background: #0056b3;
}

.primary {
  background: #28a745;
}

.danger {
  background: #dc3545;
}

import styles from './Button.module.css';

export function Button({ variant = 'primary', children }) {
  return (
    
  );
}

장점:

CSS Modules의 한계

하지만 CSS Modules는 너무 저수준이다. 반복되는 코드가 많아진다.


/* 비슷한 패턴들이 계속 반복됨 */
.box1 {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px;
}

.box2 {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 24px;
}

.box3 {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px;
}

Tailwind CSS의 해답

Tailwind는 미리 정의된 유틸리티 클래스를 제공한다.


export function Button({ variant = 'primary', children }) {
  const baseStyles = 'px-5 py-2 rounded border-none cursor-pointer';
  const variants = {
    primary: 'bg-blue-500 hover:bg-blue-600',
    danger: 'bg-red-500 hover:bg-red-600'
  };

  return (
    
  );
}

장점:

단점:

CSS Modules + Tailwind 조합

두 가지를 조합하면 양쪽의 장점을 얻을 수 있다.

전략: Tailwind는 아토믹 레벨에서, CSS Modules는 컴포넌트 레벨에서 사용


/* Button.module.css */
.button {
  @apply px-5 py-2 rounded border-none cursor-pointer;
  transition: all 0.2s ease;
}

.primary {
  @apply bg-blue-500 hover:bg-blue-600;
}

.danger {
  @apply bg-red-500 hover:bg-red-600;
}

.size-small {
  @apply px-3 py-1 text-sm;
}

.size-large {
  @apply px-6 py-3 text-lg;
}

import styles from './Button.module.css';

export function Button({
  variant = 'primary',
  size = 'medium',
  children
}) {
  const classNames = [
    styles.button,
    styles[variant],
    size !== 'medium' && styles[`size-${size}`]
  ].filter(Boolean).join(' ');

  return (
    
  );
}

Vite와의 통합

vite.config.js


import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  css: {
    modules: {
      localsConvention: 'camelCase',
      generateScopedName: '[name]__[local]--[hash:base64:5]'
    }
  }
});

또는 더 간단하게, camelCase로 접근 가능하게 설정할 수 있다.


// Button.module.css
.primaryButton { ... }

// 사용
import styles from './Button.module.css';
styles.primaryButton  // 가능

보다 구조화된 예제

디자인 시스템과 함께


/* design-tokens.css */
:root {
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --spacing-xl: 32px;

  --color-primary: #007bff;
  --color-primary-hover: #0056b3;
  --color-danger: #dc3545;

  --radius-sm: 2px;
  --radius-md: 4px;
  --radius-lg: 8px;
}

/* Card.module.css */
.card {
  @apply rounded-lg p-6 shadow-md;
  background: white;
  border: 1px solid #e0e0e0;
}

.card--elevated {
  @apply shadow-lg;
  border: none;
}

.cardHeader {
  @apply mb-4 pb-4 border-b border-gray-200;
}

.cardTitle {
  @apply text-lg font-semibold text-gray-900;
}

.cardBody {
  @apply text-sm text-gray-700;
}

.cardFooter {
  @apply mt-4 pt-4 border-t border-gray-200 text-right;
}

import styles from './Card.module.css';

export function Card({ elevated = false, children }) {
  return (
    
{children}
); } export function CardHeader({ children }) { return
{children}
; } export function CardTitle({ children }) { return

{children}

; } export function CardBody({ children }) { return
{children}
; } export function CardFooter({ children }) { return
{children}
; }

클래스명 합치기 유틸리티

클래스명을 합치는 로직이 반복되므로, 유틸리티 함수를 만들자.


// classNames.ts
export function cn(...classes: (string | boolean | undefined)[]) {
  return classes.filter(c => typeof c === 'string').join(' ');
}

// 사용
import styles from './Button.module.css';
import { cn } from '@/utils/classNames';

export function Button({ variant, disabled, children }) {
  return (
    
  );
}

또는 더 강력한 라이브러리를 사용할 수도 있다.


// clsx 라이브러리 사용
import clsx from 'clsx';
import styles from './Button.module.css';

export function Button({ variant, disabled, children }) {
  return (
    
  );
}

성능 비교

방식 번들 크기 런타임 오버헤드 개발 경험
styled-components +15KB 높음 좋음
CSS Modules +0KB 없음 보통
Tailwind +50KB (최적화 후 5KB) 없음 빠름
CSS Modules + Tailwind +30KB (최적화 후 3KB) 없음 매우 좋음

언제 무엇을 사용할까?

결론

styled-components는 강력하지만 비싸다. CSS Modules + Tailwind의 조합은 성능, 유지보수성, 개발 경험의 좋은 균형을 제공한다. 특히 중대형 프로젝트에서 강력한 선택지다.

iL
ian.lab

실무 개발자입니다. 현장에서 겪은 문제와 해결 과정을 기록합니다. 오류 제보는 연락처로 보내주세요.