CSS 모듈 활용
module.css의 지역화된 클래스 이름으로 스타일 충돌을 막고 전역 CSS·SCSS와의 책임 범위를 구분합니다.
웹 애플리케이션의 화면 구조와 사용자 경험은 스타일링(Styling) 방식에 영향을 받습니다.
React 기반의 Next.js 애플리케이션에서 스타일을 적용하는 방법은 다양하며, CSS 모듈(CSS Modules)은 클래스 이름을 파일 단위로 스코프 처리해 스타일 충돌을 줄입니다.
이 절에서는 CSS 모듈의 개념과 필요성, Next.js 프로젝트에서 활용하는 방법을 정리합니다.
CSS 모듈이란 무엇인가요?
CSS 모듈은 기본적으로 클래스 이름과 애니메이션 이름을 파일별 고유 이름으로 바꾸고, 컴포넌트가 그 이름을 가져와 사용하는 방식입니다.
서로 다른 모듈에서 같은 클래스 이름을 작성해도 이름 충돌을 줄일 수 있습니다.
핵심 특징- 로컬 스코프(Local Scope) 이름: 서로 다른 모듈의 같은 클래스 이름이 서로 다른 출력 이름으로 연결됩니다. 생성된 클래스는 이를 적용한 DOM 요소에 작용합니다.
- 고유한 클래스 이름 생성: 빌드 도구가 출력 클래스 이름을 생성합니다. 이름의 구체적인 형식은 도구와 개발·운영 모드에 따라 달라질 수 있으므로 직접 작성하지 않고
styles.btn처럼 참조합니다. - 파일 기반 모듈화: CSS 파일과 이를 가져오는 컴포넌트의 의존성을 드러냅니다. 같은 모듈을 여러 컴포넌트에서 공유할 수도 있습니다.
- JavaScript를 통한 스타일 임포트: CSS 파일을 JavaScript/TypeScript 코드에서 직접
import하여 사용합니다.
왜 CSS 모듈을 사용해야 할까요?
전통적인 CSS 작성 방식이나 다른 스타일링 방법에는 다음과 같은 문제점들이 있었습니다.
- 모든 CSS 클래스 이름은 기본적으로 전역 스코프를 가집니다.
- 다른 컴포넌트나 페이지에서 의도치 않게 동일한 클래스 이름을 사용하면 스타일이 덮어씌워지는 충돌(Collision)이 발생합니다.
- 이는 특히 규모가 큰 프로젝트나 여러 개발자가 협업하는 환경에서 디버깅을 어렵게 하고 유지보수를 복잡하게 만듭니다.
- 어떤 스타일이 어떤 HTML 요소에 적용될지 파악하기 어렵습니다.
- 컴포넌트 삭제 시 해당 스타일을 안전하게 삭제할 수 있는지 확신하기 어렵습니다. (데드 코드)
- 이름 충돌 감소: 다른 모듈의 클래스 이름을 전역에서 선점할 필요가 없습니다.
- 명확한 의존성: 컴포넌트 파일에서 CSS 파일을 직접 임포트하므로 스타일 사용처를 추적하기 쉽습니다.
Next.js에서 CSS 모듈 사용하기
Next.js는 CSS 모듈을 기본적으로 지원하며, 추가 설정 없이 바로 사용할 수 있습니다.
CSS 파일은 [name].module.css로 작성합니다. .module.scss와 .module.sass는 뒤에서 설명하는 sass 패키지를 설치한 후 사용할 수 있습니다.
간단한 버튼 컴포넌트를 만들고 CSS 모듈을 사용하여 스타일을 적용해 봅시다.
src/app/css-modules/page.tsx 파일 생성 (서버 컴포넌트):
이 페이지는 서버 컴포넌트이며, 우리가 만들 클라이언트 컴포넌트 StyledButton을 임포트하여 사용합니다.
// src/app/css-modules/page.tsx
import StyledButton from './StyledButton'; // 클라이언트 컴포넌트 임포트
import styles from './page.module.css'; // 페이지 요소에 적용할 CSS 모듈 임포트
export default function CssModulesPage() {
return (
<div className={styles.container}>
<h1 className={styles.title}>CSS 모듈 사용 예제</h1>
<p className={styles.description}>
아래 버튼은 CSS 모듈을 사용하여 고유한 스타일을 가집니다.
</p>
<div style={{ display: 'flex', gap: '20px', marginTop: '30px' }}>
<StyledButton label="클릭하세요" />
<StyledButton label="다른 버튼" primary={true} />
</div>
</div>
);
}src/app/css-modules/page.module.css 파일 생성:
page.tsx에 적용될 기본적인 레이아웃 스타일을 정의합니다.
/* src/app/css-modules/page.module.css */
.container {
padding: 40px;
max-width: 800px;
margin: 20px auto;
background-color: #f8f8f8;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
text-align: center;
}
.title {
color: #333;
margin-bottom: 15px;
font-size: 2.5em;
}
.description {
color: #666;
font-size: 1.1em;
line-height: 1.6;
}src/app/css-modules/StyledButton.tsx 파일 생성 (클라이언트 컴포넌트):
버튼 컴포넌트와 해당 스타일을 정의합니다.
"use client"; // 클라이언트 컴포넌트임을 명시
import React from 'react';
import buttonStyles from './StyledButton.module.css'; // 🚨 CSS 모듈 임포트
interface StyledButtonProps {
label: string;
primary?: boolean;
onClick?: () => void;
}
export default function StyledButton({ label, primary = false, onClick }: StyledButtonProps) {
// CSS 모듈의 클래스 이름을 조합합니다.
return (
<button
className={`${buttonStyles.btn} ${primary ? buttonStyles.primary : ''}`}
onClick={onClick}
>
{label}
</button>
);
}src/app/css-modules/StyledButton.module.css 파일 생성:
StyledButton 컴포넌트에 적용될 스타일을 정의합니다.
.btn {
padding: 12px 25px;
font-size: 1.1em;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.1s ease;
font-weight: bold;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.btn:hover {
transform: translateY(-2px);
}
.btn:active {
transform: translateY(0);
box-shadow: none;
}
/* 기본 버튼 스타일 */
.btn {
background-color: #007bff;
color: white;
}
/* primary prop이 true일 때 적용될 스타일 */
.primary {
background-color: #28a745;
color: white;
}
.primary:hover {
background-color: #218838;
}실습 확인:
개발 서버(npm run dev)를 실행한 후, http://localhost:3000/css-modules로 접속합니다.
- 두 개의 버튼이 서로 다른 배경색을 가지고 있는 것을 확인할 수 있습니다.
- 브라우저 개발자 도구(Elements 탭)에서 버튼의 클래스 이름이 원래의
btn,primary와 어떻게 매핑되는지 확인합니다. 구체적인 출력 이름은 실행 환경에 따라 달라질 수 있습니다. page.module.css의 클래스 이름도 유사하게 고유한 이름으로 변환됩니다.
CSS 모듈과 일반 CSS의 차이점
일반 CSS와 CSS 모듈의 적용 범위을 비교합니다.
| 비교 기준 | 일반 CSS | CSS 모듈 |
|---|---|---|
| 클래스 이름 | 작성한 전역 이름을 여러 파일이 함께 사용 | 파일별 고유 이름으로 변환하여 import 객체로 참조 |
| 불러오기 | App Router의 JS/TS import 또는 스타일시트 연결 | styles 객체를 import하고 className에 매핑된 이름 지정 |
| 적용 범위 | 일치하는 선택자에 따라 여러 화면에 영향 | 생성된 클래스를 적용한 요소에 영향; 상속과 전역 규칙은 남음 |
| 공유와 삭제 | 전역 선택자의 사용처 확인 | 같은 모듈을 여러 컴포넌트가 공유할 수 있으므로 import 사용처 확인 |
- 클래스 이름
- 일반 CSS: 작성한 전역 이름을 여러 파일이 함께 사용CSS 모듈: 파일별 고유 이름으로 변환하여 import 객체로 참조
- 불러오기
- 일반 CSS: App Router의 JS/TS import 또는 스타일시트 연결CSS 모듈: styles 객체를 import하고 className에 매핑된 이름 지정
- 적용 범위
- 일반 CSS: 일치하는 선택자에 따라 여러 화면에 영향CSS 모듈: 생성된 클래스를 적용한 요소에 영향; 상속과 전역 규칙은 남음
- 공유와 삭제
- 일반 CSS: 전역 선택자의 사용처 확인CSS 모듈: 같은 모듈을 여러 컴포넌트가 공유할 수 있으므로 import 사용처 확인
CSS 모듈은 Shadow DOM을 만들지 않습니다. 클래스 이름 지역화와 컴포넌트의 독점 소유는 서로 다른 개념입니다.
SCSS/SASS와 CSS 모듈 함께 사용하기
Next.js는 CSS 모듈과 함께 SCSS/SASS도 기본적으로 지원합니다.
현재는 sass(Dart Sass) 패키지를 설치한 후, 파일 확장자를 .module.scss 또는 .module.sass로 변경하여 사용하면 됩니다.
아래 원문은 darken()을 사용합니다. 이 함수는 Dart Sass에서 폐기 예정이며, 새 코드에서 같은 HSL 명도 감소를 표현하려면 @use "sass:color"와 color.adjust($color, $lightness: -10%, $space: hsl)를 사용할 수 있습니다. 비례 조정인 color.scale()과는 계산이 다릅니다.
npm install sass
# 또는
yarn add sass파일 이름 변경:
StyledButton.module.css를 StyledButton.module.scss로 변경하고, SCSS 문법(중첩, 변수 등)을 사용할 수 있습니다.
$primary-color: #28a745;
$default-color: #007bff;
.btn {
padding: 12px 25px;
font-size: 1.1em;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.1s ease;
font-weight: bold;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
&:hover { // SCSS 중첩 문법
transform: translateY(-2px);
}
&:active {
transform: translateY(0);
box-shadow: none;
}
/* 기본 버튼 스타일 */
background-color: $default-color; // SCSS 변수 사용
color: white;
}
/* primary prop이 true일 때 적용될 스타일 */
.primary {
background-color: $primary-color;
color: white;
&:hover {
background-color: darken($primary-color, 10%); // SCSS 함수 사용
}
}import buttonStyles from './StyledButton.module.scss'; // 🚨 확장자를 .scss로 변경
// ...이제 SCSS의 변수, 중첩, 믹스인을 CSS 모듈의 파일 단위 스코프 안에서 사용할 수 있습니다.