CSS-in-JS 없이 스타일링 — CSS Modules + Tailwind 실전 조합
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 (
);
}
장점:
- 번들에 포함된다 (분리된 다운로드 없음)
- 런타임 오버헤드 없음
- 진정한 캡슐화
- DevTools에서 쉽게 찾을 수 있음
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 (
);
}
장점:
- 빠르게 프로토타입 가능
- 일관성 있는 디자인 시스템
- 최적화된 빌드 (사용하지 않는 클래스 제거)
- 반응형 디자인 쉬움
단점:
- HTML 속성이 길어짐
- 컴포넌트에 스타일 로직이 섞임
- 큰 프로젝트에서 복잡해짐
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) | 없음 | 매우 좋음 |
언제 무엇을 사용할까?
- 매우 동적인 스타일: CSS-in-JS (그래도 성능 고려)
- 소규모 프로젝트: Tailwind
- 중대형 프로젝트: CSS Modules + Tailwind
- 성능 중시: CSS Modules
결론
styled-components는 강력하지만 비싸다. CSS Modules + Tailwind의 조합은 성능, 유지보수성, 개발 경험의 좋은 균형을 제공한다. 특히 중대형 프로젝트에서 강력한 선택지다.