CSS Modules는 컴포넌트 단위로 스타일을 로컬 스코프화하여 클래스 충돌을 방지하고 유지보수를 쉽게 만드는 실전 친화적 방법입니다. CRA, Vite, Next.js에서 기본 지원되어 설정이 가볍고, CSS-in-JS 대비 빌드/런타임 오버헤드가 적은 편입니다. 아래에서 바로 적용 가능한 패턴과 주의점을 정리합니다.
1. 가장 빠른 시작 (Vite/CRA/Next.js 공통)
파일명은 반드시 *.module.css로 작성합니다. 컴포넌트에서 import하여 className에 매핑합니다.
/* src/components/Button.module.css */
.button {
padding: 8px 12px;
border-radius: 6px;
border: none;
cursor: pointer;
transition: background-color .2s ease, opacity .2s ease;
}
.primary {
composes: button;
background: #1f6feb;
color: #fff;
}
.secondary {
composes: button;
background: #eaeef2;
color: #24292f;
}
.disabled {
opacity: .6;
pointer-events: none;
}
// src/components/Button.tsx
import React from 'react';
import styles from './Button.module.css';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
}
export default function Button({ variant = 'primary', disabled, children, className, ...rest }: ButtonProps) {
const classes = [styles[variant], disabled ? styles.disabled : '', className].filter(Boolean).join(' ');
return (
<button className={classes} disabled={disabled} {...rest}>
{children}
</button>
);
}
- Vite/CRA: 바로 동작합니다. Next.js는 *.module.css만 컴포넌트에서 import할 수 있고, 전역 CSS는 app/layout.tsx 혹은 pages/_app.tsx에서만 import 가능합니다.
2. 조건부/동적 클래스: clsx로 깔끔하게
조건부 클래스 결합은 배열 join보다 clsx가 가독성이 좋습니다.
// 설치: npm i clsx
import { clsx } from 'clsx';
import styles from './Badge.module.css';
export function Badge({ tone = 'info', rounded = false }) {
return (
<span className={clsx(styles.badge, styles[tone], { [styles.rounded]: rounded })}>
Badge
</span>
);
}
3. 스타일 재사용: composes로 중복 제거
CSS Modules는 composes로 다른 클래스(같은 파일/다른 파일)에서 선언된 규칙을 조합할 수 있습니다.
/* src/styles/typography.module.css */
.baseText {
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
-webkit-font-smoothing: antialiased;
}
.title {
composes: baseText;
font-weight: 700;
font-size: 20px;
}
/* src/components/Card.module.css */
.card {
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 10px;
background: #fff;
}
.cardTitle {
composes: title from '../styles/typography.module.css';
margin: 0 0 8px;
}
4. 전역이 꼭 필요할 때: :global 최소 사용
대부분은 로컬 스코프를 지키는 것이 좋습니다. 다만 리셋, 서드파티 오버라이드, 테마 속성 등은 전역이 필요합니다. 이때 :global(...)로 범위를 최소화합니다.
/* src/styles/globals.css (전역) */
:root {
--brand: #1f6feb;
}
html[data-theme='dark'] {
--brand: #0d4fd6;
color-scheme: dark;
}
/* src/components/Nav.module.css */
.nav {
display: flex;
gap: 8px;
}
:global([data-theme='dark']) .nav {
background: #0b1220;
}
/* 서드파티 예: .ant-btn 오버라이드 */
:global(.ant-btn).primary {
background: var(--brand);
}
- 주의: :global로 지정된 선택자는 해시가 적용되지 않아 범위가 넓습니다. 특정 컨테이너나 속성 선택자로 범위를 좁혀 누수와 우선순위 문제를 피합니다.
5. 다크 모드/테마를 CSS 변수로
전역에서 CSS 변수를 선언하고, 모듈에서는 변수만 소비합니다. 토글은 루트에 data-theme 속성을 교체하는 방식이 단순하고 접근성이 좋습니다.
/* src/components/Tag.module.css */
.tag {
padding: 4px 8px;
border-radius: 9999px;
background: var(--tag-bg, #eef2ff);
color: var(--tag-fg, #1e3a8a);
}
:global([data-theme='dark']) .tag {
--tag-bg: #111827;
--tag-fg: #93c5fd;
}
6. TypeScript: CSS Module 타입 선언
TS 프로젝트에서 클래스 키 자동 완성/오타 방지를 위해 최소 선언을 추가합니다. 더 엄격한 타입은 typed-css-modules/ts-plugin을 검토합니다.
// src/types/cssmodule.d.ts
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
7. 폴더 구조/네이밍 팁
- 스타일은 컴포넌트와 같은 폴더에 공존시키고 파일명을 Component.module.css로 통일합니다.
- 로컬 스코프가 기본이므로 BEM의 장황함은 줄이고, 역할 중심 이름(button, primary, disabled 등)으로 간결하게 작성합니다.
- 재사용 가능한 토큰(타이포, 그리드, 유틸)은 src/styles 아래 모듈로 분리하고 composes로 결합합니다.
8. 빌드/성능/SSR 체크포인트
- 코드 분할: 라우트/컴포넌트 경계에 따라 CSS도 함께 분할되어 초기 로드를 줄입니다. Next.js/Vite는 동적 import 시 CSS도 청크화됩니다.
- SSR: Next.js는 CSS Modules를 자동으로 추출/주입합니다. 렌더 시 FOUC를 줄이려면 중요한 스타일은 컴포넌트 모듈에 그대로 두고 전역 임포트를 최소화합니다.
- 셀렉터 최적화: 깊은 후손 선택자(.a .b .c)는 유지보수와 성능에 불리합니다. 최대 2~3단 레벨로 제한합니다.
9. 테스트 환경 설정(Jest)
테스트에서 클래스 매핑을 단순화하려면 identity-obj-proxy를 사용합니다.
// jest.config.js
module.exports = {
moduleNameMapper: {
'\\.(css|scss)$': 'identity-obj-proxy',
},
};
10. 현업 체크리스트
- *.module.css만 컴포넌트에서 임포트하고, 전역은 한 곳(globals.css)에서 관리합니다.
- 전역 오버라이드는 :global로 최소 범위만 허용합니다.
- 동적 클래스는 clsx로 관리하고, 상태 클래스(disabled, active 등)를 명시적으로 분리합니다.
- 재사용 로직은 composes로 조립하고, 공통 토큰은 styles 디렉토리로 모듈화합니다.
- 다크 모드는 data-theme + CSS 변수로 구현해 JS 의존을 줄입니다.
- TS 타입 선언을 추가해 빌드 안전성과 DX를 높입니다.
부록: Next.js와 SCSS Modules
Next.js에서 Sass를 쓰려면 npm i -D sass 후 *.module.scss를 동일한 방식으로 사용합니다. 전역 Sass는 globals.scss에 모읍니다.
/* app/page.module.scss */
.container {
margin: 0 auto;
max-width: 960px;
padding: 24px;
}
CSS Modules는 러닝 커브가 낮고, 기존 CSS 자산을 그대로 활용할 수 있어 팀 온보딩이 빠릅니다. 위의 규칙과 패턴을 적용하면 규모가 커져도 스타일 충돌 없이 일관된 UI를 유지할 수 있습니다.
'React' 카테고리의 다른 글
| React에서 Chart.js를 사용한 동적 차트 생성 (0) | 2026.06.05 |
|---|---|
| React 컴포넌트에서 메모리 누수 디버깅하기 (0) | 2026.06.05 |
| React 앱에서 Web Share API 사용하여 콘텐츠 공유하기 (0) | 2026.06.04 |
| React에서 Drag-and-Drop API로 파일 순차 업로드 구현하기 (1) | 2026.06.04 |
| React 앱에서 브라우저 성능 API 활용하여 로딩 속도 최적화하기 (1) | 2026.06.02 |