React CSS 모듈 완전 가이드
목차
3.
4.
5.
6.
7.
CSS 모듈이란?
CSS 모듈(CSS Modules)은 CSS 클래스명을 로컬 스코프로 제한하여 클래스명 충돌을 방지하는 기술입니다. React에서 컴포넌트별로 독립적인 스타일을 적용할 때 매우 유용합니다.
일반 CSS vs CSS 모듈
일반 CSS의 문제점:
/* styles.css */
.button {
background-color: blue;
color: white;
}
CSS
복사
→ 전역 스코프에서 .button 클래스가 모든 컴포넌트에 영향을 미침
CSS 모듈의 해결책:
/* Button.module.css */
.button {
background-color: blue;
color: white;
}
CSS
복사
→ 클래스명이 Button_button__hash123 형태로 변환되어 충돌 방지
CSS 모듈의 장점
1. 스코프 격리
•
컴포넌트별로 독립적인 CSS 스코프 제공
•
클래스명 충돌 완전 방지
2. 유지보수성 향상
•
스타일과 컴포넌트의 1:1 매핑
•
코드 리팩토링 시 안전성 보장
3. 명시적 의존성
•
import 문을 통한 명확한 스타일 의존성 표현
•
사용하지 않는 스타일 쉽게 식별 가능
4. 빌드 타임 최적화
•
사용되지 않는 CSS 자동 제거
•
클래스명 최적화로 번들 크기 감소
기본 사용법
1. CSS 모듈 파일 생성
파일명 규칙: ComponentName.module.css
/* Button.module.css */
.primary {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
.secondary {
background-color: #6c757d;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
.large {
padding: 15px 30px;
font-size: 18px;
}
.small {
padding: 5px 10px;
font-size: 12px;
}
CSS
복사
2. React 컴포넌트에서 사용
// Button.jsx
import React from 'react';
import styles from './Button.module.css';
const Button = ({ variant = 'primary', size = 'medium', children, ...props }) => {
return (
<button
className={`${styles[variant]} ${styles[size]}`}
{...props}
>
{children}
</button>
);
};
export default Button;
JavaScript
복사
3. 컴포넌트 사용 예제
// App.jsx
import React from 'react';
import Button from './components/Button';
function App() {
return (
<div>
<Button variant="primary" size="large">
Primary Large Button
</Button>
<Button variant="secondary" size="small">
Secondary Small Button
</Button>
</div>
);
}
export default App;
JavaScript
복사
고급 사용법
1. 동적 클래스명
// Card.jsx
import styles from './Card.module.css';
const Card = ({ isActive, hasError, children }) => {
const cardClasses = [
styles.card,
isActive && styles.active,
hasError && styles.error
].filter(Boolean).join(' ');
return (
<div className={cardClasses}>
{children}
</div>
);
};
JavaScript
복사
2. classnames 라이브러리 활용
먼저 설치:
npm install classnames
Shell
복사
사용 예제:
import classNames from 'classnames/bind';
import styles from './Card.module.css';
const cx = classNames.bind(styles);
const Card = ({ isActive, hasError, size }) => {
return (
<div className={cx('card', {
active: isActive,
error: hasError,
[`size-${size}`]: size
})}>
Content
</div>
);
};
JavaScript
복사
3. CSS 변수와 함께 사용
/* Theme.module.css */
.container {
--primary-color: #007bff;
--secondary-color: #6c757d;
--spacing: 1rem;
}
.button {
background-color: var(--primary-color);
margin: var(--spacing);
}
.darkTheme {
--primary-color: #0056b3;
--secondary-color: #545b62;
}
CSS
복사
import styles from './Theme.module.css';
const ThemedComponent = ({ isDark }) => {
return (
<div className={`${styles.container} ${isDark ? styles.darkTheme : ''}`}>
<button className={styles.button}>Themed Button</button>
</div>
);
};
JavaScript
복사
4. 중첩 선택자와 의사 클래스
/* Navigation.module.css */
.nav {
display: flex;
list-style: none;
padding: 0;
}
.navItem {
margin-right: 1rem;
}
.navItem:hover {
background-color: #f8f9fa;
}
.navItem:last-child {
margin-right: 0;
}
.navLink {
text-decoration: none;
color: #333;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: all 0.2s ease;
}
.navLink:hover {
background-color: #e9ecef;
}
.navLink.active {
background-color: #007bff;
color: white;
}
CSS
복사
실전 예제
완전한 카드 컴포넌트 구현
/* Card.module.css */
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.header {
padding: 1.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.subtitle {
margin: 0.5rem 0 0 0;
opacity: 0.9;
font-size: 0.875rem;
}
.body {
padding: 1.5rem;
}
.footer {
padding: 1rem 1.5rem;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
}
.actions {
display: flex;
gap: 0.5rem;
}
.button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.buttonPrimary {
background: #007bff;
color: white;
}
.buttonPrimary:hover {
background: #0056b3;
}
.buttonSecondary {
background: transparent;
color: #6c757d;
border: 1px solid #dee2e6;
}
.buttonSecondary:hover {
background: #e9ecef;
}
/* 반응형 디자인 */
@media (max-width: 768px) {
.card {
margin: 0.5rem;
}
.header,
.body,
.footer {
padding: 1rem;
}
.footer {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.actions {
justify-content: center;
}
}
CSS
복사
// Card.jsx
import React from 'react';
import styles from './Card.module.css';
const Card = ({
title,
subtitle,
children,
onPrimaryAction,
onSecondaryAction,
primaryActionText = "확인",
secondaryActionText = "취소"
}) => {
return (
<div className={styles.card}>
{(title || subtitle) && (
<div className={styles.header}>
{title && <h3 className={styles.title}>{title}</h3>}
{subtitle && <p className={styles.subtitle}>{subtitle}</p>}
</div>
)}
<div className={styles.body}>
{children}
</div>
{(onPrimaryAction || onSecondaryAction) && (
<div className={styles.footer}>
<div className={styles.actions}>
{onSecondaryAction && (
<button
className={`${styles.button} ${styles.buttonSecondary}`}
onClick={onSecondaryAction}
>
{secondaryActionText}
</button>
)}
{onPrimaryAction && (
<button
className={`${styles.button} ${styles.buttonPrimary}`}
onClick={onPrimaryAction}
>
{primaryActionText}
</button>
)}
</div>
</div>
)}
</div>
);
};
export default Card;
JavaScript
복사
사용 예제
// App.jsx
import React from 'react';
import Card from './components/Card';
function App() {
const handleSave = () => {
console.log('저장됨');
};
const handleCancel = () => {
console.log('취소됨');
};
return (
<div style={{ padding: '2rem', background: '#f5f5f5', minHeight: '100vh' }}>
<Card
title="사용자 정보"
subtitle="프로필 정보를 확인하고 수정하세요"
onPrimaryAction={handleSave}
onSecondaryAction={handleCancel}
primaryActionText="저장"
secondaryActionText="취소"
>
<p>이름: 홍길동</p>
<p>이메일: hong@example.com</p>
<p>가입일: 2024-01-15</p>
</Card>
</div>
);
}
export default App;
JavaScript
복사
트러블슈팅
1. 클래스명이 적용되지 않는 경우
문제: 스타일이 적용되지 않음
// ❌ 잘못된 방법
<div className="button">버튼</div>
JavaScript
복사
해결:
// ✅ 올바른 방법
<div className={styles.button}>버튼</div>
JavaScript
복사
2. 하이픈이 포함된 클래스명
CSS:
.nav-item {
color: blue;
}
CSS
복사
사용:
// ❌ 에러 발생
<div className={styles.nav-item}>
// ✅ 대괄호 표기법 사용
<div className={styles['nav-item']}>
// ✅ 또는 카멜케이스로 변경
.navItem {
color: blue;
}
<div className={styles.navItem}>
JavaScript
복사
3. 전역 스타일과 모듈 스타일 충돌
/* styles.module.css */
:global(.global-class) {
/* 전역 스타일 */
}
.local-class {
/* 로컬 스타일 */
}
/* 특정 전역 클래스 내부의 로컬 스타일 */
:global(.container) .localContent {
color: red;
}
CSS
복사
4. 빌드 환경 문제
Create React App이 아닌 경우 webpack 설정:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]___[hash:base64:5]'
}
}
}
]
}
]
}
};
JavaScript
복사
대안 및 비교
1. CSS Modules vs Styled Components
특징 | CSS Modules | Styled Components |
러닝 커브 | 낮음 | 중간 |
번들 크기 | 작음 | 큼 |
런타임 성능 | 좋음 | 보통 |
개발자 도구 | 보통 | 좋음 |
테마 지원 | 수동 | 내장 |
2. CSS Modules vs CSS-in-JS
// CSS Modules
import styles from './Button.module.css';
const Button = () => <button className={styles.primary}>버튼</button>;
// CSS-in-JS (Emotion)
import { css } from '@emotion/react';
const Button = () => (
<button css={css`background: blue; color: white;`}>
버튼
</button>
);
JavaScript
복사
3. 언제 CSS Modules를 사용해야 할까?
CSS Modules 권장 상황:
•
기존 CSS에 익숙한 팀
•
빠른 개발 속도가 필요한 경우
•
번들 크기 최적화가 중요한 경우
•
정적 스타일이 주를 이루는 경우
다른 방법 고려 상황:
•
동적 스타일링이 많이 필요한 경우 → Styled Components
•
복잡한 테마 시스템이 필요한 경우 → CSS-in-JS
•
매우 큰 규모의 프로젝트 → CSS-in-JS + 디자인 시스템
마무리
CSS Modules는 React에서 컴포넌트 기반 스타일링을 구현하는 강력하고 효율적인 방법입니다. 기존 CSS 지식을 그대로 활용하면서도 모던 웹 개발의 요구사항을 충족할 수 있어, 많은 프로젝트에서 채택되고 있습니다.
핵심은 컴포넌트와 스타일의 1:1 매핑을 통해 유지보수하기 쉬운 코드를 작성하는 것입니다. 이 가이드의 예제들을 참고하여 프로젝트에 적용해보시기 바랍니다.