CSS 모듈 소개
Vite에서 CSS 모듈이 로컬 클래스 이름을 매핑하는 흐름과 React className 연결, 전역 경계, 캐스케이드, CSS 코드 분할의 책임을 익힙니다.
일반 CSS의 클래스 선택자는 문서 전역에서 서로 만날 수 있습니다. 프로젝트가 커지면 서로 다른 파일에 같은 .title이나 .button을 작성했을 때 의도하지 않은 규칙이 적용될 수 있습니다.
CSS 모듈(CSS Modules)은 React API가 아니라 빌드 도구가 CSS 클래스 이름을 모듈 파일 단위로 지역화하고, 원래 이름과 변환된 이름의 매핑을 JavaScript에 제공하는 방식입니다. 여기서 local은 “컴포넌트 인스턴스 전용”이 아니라 “그 CSS 모듈 파일에서 내보낸 클래스”라는 뜻입니다. 같은 모듈을 여러 컴포넌트가 import하면 같은 매핑과 규칙을 함께 사용합니다.
Local names, two build outputs
CSS Modules는 로컬 클래스 이름을 JavaScript 매핑과 처리된 CSS 선택자로 연결한다.
- 입력과 import
Component.jsx가Card.module.css를 import해 CSS 의존성을 모듈 그래프에 연결한다.- 로컬 이름 변환
- Vite가 선택한 변환기와 설정이 로컬 클래스 이름을 처리한다. 생성 문자열의 형식은 고정 규약이 아니다.
- JavaScript 출력
- 모듈 객체는 원래 이름을 클래스 문자열로 매핑한다.
composes결과는 여러 토큰일 수 있다. - CSS 출력
- 같은 토큰을 가진 처리된 선택자와 CSS 규칙이 개발 서버 또는 빌드 자산으로 전달된다.
- 브라우저에서 합류
className이 만든 DOM의 class 속성과 로드된 CSS 선택자가 같은 토큰으로 매칭된다.
- 로컬 클래스
- 모듈 파일의 로컬 클래스 이름만 변환한다. 같은 파일을 import하는 컴포넌트는 같은 매핑을 공유한다.
- 전역 경계
:global(...)과 일반 CSS는 전역 계약이다. 지역화가 모든 CSS를 격리하지는 않는다.- 정상 캐스케이드
- 사용자 정의 속성은 이름이 바뀌지 않으며, 일반 CSS처럼 캐스케이드하고 상속된다.
build.cssCodeSplit 같은 Vite 빌드 설정이 CSS 자산의 분할과 로딩 방식을 정한다.Vite에서 CSS 모듈 불러오기
Vite는 .module.css로 끝나는 파일을 CSS 모듈로 처리합니다. 이 파일을 import하면 각 로컬 클래스 이름을 처리된 클래스 문자열에 연결한 모듈 객체를 돌려줍니다.
.container {
--component-gap: 12px;
display: grid;
gap: var(--component-gap);
padding: 24px;
border: 1px solid #b7e4c7;
border-radius: 8px;
}
.title {
color: var(--brand-color, #287a3e);
font-size: 2rem;
}
.description {
color: #3d6f4a;
line-height: 1.6;
}
.myButton {
padding: 12px 24px;
border: 0;
border-radius: 6px;
background: #2f7d44;
color: white;
cursor: pointer;
}
.myButton:hover {
background: #286b3b;
}
.primary {
composes: myButton;
border: 2px solid #1f5d31;
font-weight: 700;
}import styles from './CssModulesExample.module.css';
export default function CssModulesExample({ gap = 12 }) {
return (
<section
className={styles.container}
style={{ '--component-gap': `${gap}px` }}
>
<h2 className={styles.title}>CSS 모듈 예제</h2>
<p className={styles.description}>
로컬 클래스 이름은 import한 매핑을 통해 선택합니다.
</p>
<button className={styles.myButton}>기본 버튼</button>
<button className={styles.primary}>강조 버튼</button>
</section>
);
}React의 className에는 최종 클래스 이름 문자열을 전달합니다. Vite가 반환한 styles.container도 문자열이며, styles.primary처럼 composes를 사용한 항목은 둘 이상의 클래스 토큰을 포함할 수 있습니다.
composes는 선언을 복사하거나 CSS 상속을 만드는 기능이 아닙니다. 내보내는 클래스 문자열에 다른 로컬 클래스 이름을 함께 구성하므로, 실제 값의 승패는 여전히 일반 CSS의 출처 순서, 명시도, 상속, 캐스케이드 규칙으로 결정됩니다.
생성 클래스 이름은 도구와 설정에 따라 달라진다
개발자 도구에서 다음과 비슷한 결과를 볼 수 있습니다.
{
container: "_container_k3h2m_1",
myButton: "_myButton_k3h2m_18",
primary: "_primary_k3h2m_31 _myButton_k3h2m_18"
}이 문자열은 형태를 설명하기 위한 예시입니다. 파일명, 로컬 이름, 해시가 조합되는 정확한 형식은 CSS Modules 변환기와 설정에 따라 달라집니다. Vite의 css.modules.generateScopedName 같은 옵션을 바꾸거나 Lightning CSS 변환기를 사용하면 결과 형식도 달라질 수 있으므로, 생성된 문자열을 직접 작성하거나 테스트의 고정값으로 삼지 않습니다.
소스 코드에서는 원래 로컬 이름을 매핑 객체로 읽습니다.
<button className={styles.primary}>강조 버튼</button>파일 경계와 전역 예외
로컬 스코프는 클래스 이름을 지역화합니다. CSS 전체를 격리하거나 다른 전역 규칙의 영향을 차단하지는 않습니다.
- 앱 전체의 reset, 기본 글꼴,
:root토큰은src/index.css같은 일반 CSS 파일에 둡니다. - 외부 라이브러리나 DOM 상태가 요구하는 전역 클래스는 필요한 범위만
:global(...)로 명시합니다. - 컴포넌트 모양과 상태를 나타내는 클래스는
.module.css의 로컬 이름으로 유지합니다.
:root {
--brand-color: #287a3e;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
}.dialog {
color: var(--brand-color);
}
/* 외부 스크립트와 합의한 전역 훅만 예외로 둡니다. */
:global(.body-scroll-lock) {
overflow: hidden;
}:global(...)은 해당 선택자를 전역에 남기는 명시적 경계입니다. 일반 reset까지 모듈 파일에 모으는 용도가 아니며, 전역 이름은 다시 충돌 가능성이 생기므로 작은 통합 지점에만 사용합니다.
CSS 사용자 정의 속성과 캐스케이드
CSS 사용자 정의 속성(--brand-color, --component-gap)의 이름은 CSS Modules가 지역화하지 않습니다. 사용자 정의 속성은 일반 CSS처럼 캐스케이드하고 상속됩니다.
따라서 다음 두 방식 모두 정상입니다.
:root나 상위 요소의 전역 규칙에서 테마 토큰을 정의하고 모듈 규칙에서var(...)로 읽습니다.- 모듈의 로컬 클래스에서 컴포넌트 하위 트리에만 적용할 기본값을 정의합니다.
실행 중 계산한 값만 바꿔야 한다면 앞의 예제처럼 React style 객체로 사용자 정의 속성 값을 전달할 수 있습니다. 이때 style은 동적 값 하나를 제공하고, 실제 레이아웃·상태·반응형 규칙은 CSS 클래스에 남겨 둡니다.
import 그래프와 CSS 코드 분할의 책임
import CssModulesExample from './components/CssModulesExample.jsx';
export default function App() {
return <CssModulesExample gap={16} />;
}CssModulesExample.jsx가 CSS 모듈을 import하면 JavaScript 모듈과 CSS 사이의 의존성이 Vite의 모듈 그래프에 드러납니다. 하지만 CSS Modules 자체가 컴포넌트마다 별도 CSS 파일을 보장하지는 않습니다.
Vite 빌드에서 build.cssCodeSplit이 활성화되어 있으면 비동기 JavaScript 청크가 import하는 CSS를 연결된 CSS 청크로 보존해 함께 가져옵니다. 이 옵션을 끄면 프로젝트 CSS를 하나의 파일로 추출합니다. 정적 import, 공유 규칙, 라이브러리 모드와 빌드 설정에 따라 실제 출력 묶음이 달라질 수 있으므로 다음 책임을 구분합니다.
CSS Modules: 로컬 클래스 이름을 변환하고 JavaScript 매핑을 제공합니다.
import 그래프: 어떤 JavaScript 모듈이 어떤 CSS에 의존하는지 기록합니다.
Vite 빌드 설정: 비동기 청크와 CSS 자산을 어떻게 나누고 불러올지 결정합니다.
장점과 고려사항
장점- 서로 다른 모듈 파일의 같은 로컬 클래스 이름이 충돌할 가능성을 줄입니다.
- CSS 의존성이 import 문에 드러나 사용 위치를 추적하기 쉽습니다.
composes로 기존 로컬 클래스를 클래스 문자열 수준에서 재사용할 수 있습니다.- 일반 CSS의 선택자, 가상 상태, 미디어 쿼리, 사용자 정의 속성을 그대로 활용합니다.
- 생성 클래스 이름은 사람이 직접 의존할 공개 API가 아니어서 개발자 도구에서 원본 이름을 추적해야 할 수 있습니다.
- 전역 스타일과
:global(...)경계는 팀이 별도로 관리해야 합니다. - 여러 클래스를 조건부로 조합하려면 문자열을 만들거나
classnames같은 도우미를 사용할 수 있습니다. - 지역화는 클래스 이름 충돌을 줄일 뿐, 명시도·소스 순서·상속·사용자 정의 속성의 캐스케이드를 없애지 않습니다.
React는 CSS 파일을 프로젝트에 추가하는 방법을 규정하지 않습니다. className과 style의 DOM 전달 규칙은 React 공식 문서를, Vite의 CSS Modules와 코드 분할 동작은 Vite CSS 기능과 빌드 옵션을 기준으로 확인합니다.
CSS 모듈 소개는 여기까지입니다.
CSS 모듈은 React 컴포넌트를 자동으로 격리하는 기능이 아니라, 빌드 시 로컬 클래스 이름과 처리된 클래스 문자열을 연결하는 방식입니다. 로컬 클래스, 전역 예외, 사용자 정의 속성의 캐스케이드, 번들러의 청크 책임을 나누어 이해하면 규모가 커져도 스타일 경계를 예측할 수 있습니다.
다음 절에서는 자바스크립트 코드 안에서 CSS를 작성하는 CSS-in-JS 라이브러리를 다룹니다.