본문으로 건너뛰기

안동민 개발노트

본문 시작

CSS 모듈 활용

module.css의 지역화된 클래스 이름으로 스타일 충돌을 막고 전역 CSS·SCSS와의 책임 범위를 구분합니다.

웹 애플리케이션의 화면 구조와 사용자 경험은 스타일링(Styling) 방식에 영향을 받습니다.

React 기반의 Next.js 애플리케이션에서 스타일을 적용하는 방법은 다양하며, CSS 모듈(CSS Modules)은 클래스 이름을 파일 단위로 스코프 처리해 스타일 충돌을 줄입니다.

이 절에서는 CSS 모듈의 개념과 필요성, Next.js 프로젝트에서 활용하는 방법을 정리합니다.

같은 .title 도 파일 경계가 다르면 충돌하지 않는다

.module.css 를 import하면 빌드가 로컬 클래스와 고유한 출력 이름의 매핑을 만듭니다. 컴포넌트는 원문 문자열 대신 그 매핑을 사용합니다.

  1. Card

    컴포넌트 A source Card.module.css · .title import styles.title DOM Card_title__a1

  2. Modal

    컴포넌트 B source Modal.module.css · .title import styles.title DOM Modal_title__b9

  3. 로컬

    *.module.css 한 컴포넌트와 함께 이동하고 삭제되는 스타일

  4. 전역

    globals.css reset·기본 요소·앱 공통 토큰처럼 전체에 필요한 규칙

  5. 탈출

    global(...) 외부 라이브러리 등 반드시 전역 이름을 겨냥할 때만 제한적으로 사용


CSS 모듈이란 무엇인가요?

CSS Modules는 작성한 이름을 파일별 고유 class로 바꾼다

사람은 styles 객체를 사용하고 빌드는 충돌 없는 실제 class 이름을 DOM에 연결한다.

  1. 1
    Module file

    Button.module.css에 지역 class 정의

  2. 2
    Import

    컴포넌트가 styles.primary 속성으로 참조

  3. 3
    Build

    파일·class·hash를 조합한 고유 이름 생성

  4. 4
    DOM

    다른 모듈의 같은 이름과 충돌 없이 적용

CSS 모듈은 CSS 파일을 불러올 때, 모든 클래스 이름과 애니메이션 이름을 자동으로 고유하게 만들어 컴포넌트 범위로 스타일을 한정하는 방식입니다.

이는 전역적인 스타일 충돌 문제를 해결하고, CSS의 스코프를 명확히 하여 유지보수성을 높이는 데 기여합니다.

핵심 특징
  • 로컬 스코프(Local Scope) 스타일: CSS 모듈로 작성된 클래스 이름은 해당 모듈(파일) 내에서만 유효합니다. 다른 컴포넌트의 동일한 클래스 이름과 충돌하지 않습니다.
  • 고유한 클래스 이름 생성: 빌드 시 [filename]\_[classname]\_\_[hash]와 같은 형태로 고유한 클래스 이름이 자동으로 생성됩니다. (예: button\_module\_\_btn\_\_abc123)
  • 파일 기반 모듈화: 각 CSS 파일이 독립적인 모듈처럼 작동하여, 특정 컴포넌트의 스타일은 해당 컴포넌트의 CSS 파일에만 존재합니다.
  • JavaScript를 통한 스타일 임포트: CSS 파일을 JavaScript/TypeScript 코드에서 직접 import하여 사용합니다.

왜 CSS 모듈을 사용해야 할까요?

전통적인 CSS 작성 방식이나 다른 스타일링 방법에는 다음과 같은 문제점들이 있었습니다.

전역 스코프 문제 (Global Scope Pollution)
  • 모든 CSS 클래스 이름은 기본적으로 전역 스코프를 가집니다.
  • 다른 컴포넌트나 페이지에서 의도치 않게 동일한 클래스 이름을 사용하면 스타일이 덮어씌워지는 충돌(Collision)이 발생합니다.
  • 이는 특히 규모가 큰 프로젝트나 여러 개발자가 협업하는 환경에서 디버깅을 어렵게 하고 유지보수를 복잡하게 만듭니다.
스타일 간의 의존성 및 관리의 어려움
  • 어떤 스타일이 어떤 HTML 요소에 적용될지 파악하기 어렵습니다.
  • 컴포넌트 삭제 시 해당 스타일을 안전하게 삭제할 수 있는지 확신하기 어렵습니다. (데드 코드)
CSS 모듈은 이러한 문제들을 다음과 같이 해결합니다
  • 스타일 고립: 각 컴포넌트의 스타일이 해당 컴포넌트에만 영향을 미치도록 합니다. 클래스 이름 충돌 걱정 없이 자유롭게 클래스 이름을 지을 수 있습니다.
  • 모듈화된 CSS: CSS 파일 자체가 컴포넌트에 종속적인 모듈이 되어, 컴포넌트와 스타일 간의 강한 연관성을 확보합니다. 컴포넌트 삭제 시 관련 CSS 파일을 함께 삭제해도 다른 곳에 영향을 주지 않습니다.
  • 명확한 의존성: 컴포넌트 파일에서 CSS 파일을 직접 임포트하므로, 어떤 컴포넌트가 어떤 스타일을 사용하는지 명확하게 알 수 있습니다.

Next.js에서 CSS 모듈 사용하기

Next.js는 CSS 모듈을 기본적으로 지원하며, 추가 설정 없이 바로 사용할 수 있습니다.

파일 이름을 [name].module.css 또는 [name].module.scss, [name].module.sass와 같이 .module. 확장자를 사용하여 작성하면 됩니다.

실습: 버튼 컴포넌트에 CSS 모듈 적용하기

간단한 버튼 컴포넌트를 만들고 CSS 모듈을 사용하여 스타일을 적용해 봅시다.

src/app/css-modules/page.tsx 파일 생성 (서버 컴포넌트): 이 페이지는 서버 컴포넌트이며, 우리가 만들 클라이언트 컴포넌트 StyledButton을 임포트하여 사용합니다.

src/app/css-modules/page.tsx
// 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
/* 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 파일 생성 (클라이언트 컴포넌트): 버튼 컴포넌트와 해당 스타일을 정의합니다.

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 컴포넌트에 적용될 스타일을 정의합니다.

src/app/css-modules/StyledButton.module.css
.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 탭)를 열어 버튼의 클래스 이름을 확인해 보세요. StyledButton_module__btn__... 와 같이 고유한 해시값이 붙어 있는 것을 볼 수 있습니다. 이는 스타일 충돌을 방지하기 위해 CSS 모듈이 자동으로 생성한 고유한 클래스 이름입니다.
  • page.module.css의 클래스 이름도 유사하게 고유한 이름으로 변환됩니다.

CSS 모듈과 일반 CSS의 차이점

특징일반 CSS (.css)CSS 모듈 (.module.css)
스코프전역 (Global Scope)로컬 (Local Scope)
클래스 이름작성된 이름 그대로 사용자동으로 고유한 이름으로 변환
충돌 방지수동으로 관리 (BEM, 네이밍 컨벤션 등)자동 처리
임포트 방식<link> 태그 또는 @import (전역)JavaScript/TypeScript 파일에서 import styles from './styles.module.css'
재사용성전역적 재사용컴포넌트 기반 재사용 (모듈별)
유지보수성규모가 커질수록 어려움명확한 의존성, 쉬운 삭제
모듈 class는 import 객체를 통해서만 안정적으로 이어진다

작성 이름과 브라우저 이름이 다르므로 동적 문자열보다 명시적 속성 참조를 사용한다.

  1. Define
    .title

    module.css 안에서 사람이 읽을 이름 작성

  2. Import
    styles.title

    컴포넌트가 객체 속성으로 의존성 표현

  3. Hash
    title_x9a

    빌드가 파일별 고유 이름으로 변환

  4. Render
    className

    실제 DOM에는 생성된 이름이 들어감


SCSS/SASS와 CSS 모듈 함께 사용하기

Next.js는 CSS 모듈과 함께 SCSS/SASS도 기본적으로 지원합니다.

현재는 sass(Dart Sass) 패키지를 설치한 후, 파일 확장자를 .module.scss 또는 .module.sass로 변경하여 사용하면 됩니다.

레거시 Sass 구현은 신규 프로젝트에서 사용하지 않는 것이 좋습니다.

SCSS/SASS 설치
npm install sass
# 또는
yarn add sass

파일 이름 변경: StyledButton.module.cssStyledButton.module.scss로 변경하고, SCSS 문법(중첩, 변수 등)을 사용할 수 있습니다.

src/app/css-modules/StyledButton.module.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 함수 사용
  }
}
컴포넌트에서 임포트 경로 변경
src/app/css-modules/StyledButton.tsx (수정)
import buttonStyles from './StyledButton.module.scss'; // 🚨 확장자를 .scss로 변경
// ...

이제 SCSS의 변수, 중첩, 믹스인을 CSS 모듈의 파일 단위 스코프 안에서 사용할 수 있습니다.

CSS 모듈은 컴포넌트 기반 개발에서 스타일 관리의 복잡성을 크게 줄여주는 효과적인 방법입니다.

Next.js에서 기본적으로 지원하므로, 새로운 프로젝트를 시작할 때 스타일링 방식으로 고려하는 것이 좋습니다.

CSS Module은 class 이름을 컴포넌트 계약으로 만든다

파일 작성부터 import·빌드·유지보수까지 스타일 의존성이 컴포넌트 가까이에 남는다.

  1. write
    Button.module.css

    컴포넌트 단위 class와 상태 스타일 정의

  2. import
    styles object

    문자열 대신 styles.btn으로 명시적 참조

  3. build
    Unique class

    고유 이름을 만들어 다른 화면 충돌 차단

  4. maintain
    Local dependency

    삭제·변경 영향이 컴포넌트 범위에 남음

CSS 모듈을 적용할 때는 클래스 충돌 방지뿐 아니라 스타일 책임이 컴포넌트 경계 안에 머무는지도 함께 확인해야 합니다.

CSS 모듈은 컴포넌트 단위 스타일 계약을 만든다

클래스 이름 충돌을 피하는 것뿐 아니라 어느 컴포넌트가 어떤 스타일 책임을 갖는지 파일 구조로 드러냅니다.

  1. 범위
    파일 스코프

    범위 클래스 이름은 빌드 시 고유해지므로 다른 컴포넌트와 우연히 충돌하지 않습니다.

  2. compose
    조합

    compose 공통 토큰은 변수와 유틸 클래스로 빼고 화면별 스타일은 모듈 안에 둡니다.

  3. global
    전역 예외

    global reset, 폰트, body처럼 전역 규칙만 남깁니다.

  4. scss
    확장

    scss SCSS도 파일 경계와 클래스 책임을 유지합니다.

같은 클래스 이름을 파일 경계에서 분리한다

작성자는 짧은 의미 이름을 쓰고, 빌드 결과는 충돌하지 않는 이름으로 연결된다. 지역 스타일과 전역 규칙의 책임만 명확히 나누면 된다.

  1. 파일마다 같은 이름 사용

    · AUTHOR 파일마다 같은 이름 사용 Card → .title Profile → .title 이름을 전역에서 선점할 필요가 없다.

  2. · AUTHOR
  3. · BUILD
  4. · USE
  5. 구조·상태

    MODULE 구조·상태 card, title, selected처럼 컴포넌트 내부에서 쓰는 클래스

  6. 앱 공통 기반

    GLOBAL 앱 공통 기반 reset, typography, theme token처럼 여러 경계가 공유하는 규칙

  7. 동적 값

    RUNTIME 동적 값 상태 class를 조합하고 연속 값은 CSS variable로 전달

마지막으로 CSS Modules가 일반 CSS와 달리 클래스 충돌을 줄이는 방식을 Next.js 컴포넌트 기준으로 정리합니다.

CSS Modules는 컴포넌트 가까이에서 지역 scope를 만든다

작성 파일·사용 객체·전역 규칙을 분리하면 스타일 변경 범위가 선명해진다.

  1. file
    Button.module.css

    한 컴포넌트에 종속된 시각 규칙

  2. use
    styles.name

    import 객체를 통한 명시적 class 참조

  3. build
    Local hash

    같은 title 이름도 파일별 고유하게 변환

  4. global
    Tokens · reset

    앱 전체에 필요한 규칙만 별도 유지

  5. scss
    module.scss

    필요하면 Sass 문법과 지역 scope 결합