본문 바로가기

React

React에서 스타일링을 위한 CSS Modules 활용하기

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를 유지할 수 있습니다.