..

Search

17) CSS Module

CSS Module


CSS Module

외부 CSS 파일을 참조하는 방식은 React 애플리케이션의 규모가 커질수록 여러 컴포넌트에서 사용된 CSS 클래스명이 서로 중복될 가능성이 높아집니다. 만약 서로 다른 두 개의 CSS 파일에 동일한 이름의 CSS 클래스가 정의되어 있다면, 해당 클래스가 적용된 React 엘리먼트는 이 두 스타일이 모두 한꺼번에 적용됩니다.

 

이와 같은 문제점을 해결하기 위해 CSS Module을 사용할 수 있습니다. CSS Module은 CSS 클래스를 불러와 사용할 때 클래스명을 고유한 이름으로 자동 변환해줌으로써 CSS 클래스명이 서로 중첩되는 현상을 미연에 방지해 주는 기술입니다.

 

React에서 CSS Module을 적용하는 방법은 아주 간단합니다. 별도의 설정 없이 CSS Module을 적용하고 싶은 CSS 파일의 이름을 아래와 같은 방식으로 작성하기만 하면 자동으로 적용됩니다.

특정 모듈만을 위한 CSS 파일

[모듈명].module.css

 

CSS Module을 적용한 파일에 작성된 클래스는 아래 코드처럼 styles 객체를 활용하여 자바스크립트 객체의 프로퍼티(property) 형식으로 참조할 수 있습니다.

CSS Module 문법
import styles from "파일 경로";
//...
<div className="{styles.[클래스명]}">...</div>

 

React 컴포넌트에서 해당 CSS 파일을 불러올 때 선언된 CSS 클래스명은 모두 고유한 이름으로 자동 변환됩니다. 고유한 클래스명은 파일 경로, 파일 이름, 원래 작성한 클래스명, 해쉬값 등을 사용하여 자동 생성됩니다. 따라서 CSS Module을 사용하면 CSS 파일마다 고유한 네임스페이스를 자동으로 부여해 주기 때문에 각각의 React 컴포넌트는 완전히 분리된 스타일을 보장받게 됩니다.

 

css-modules-concept

 

다음 예제는 FirstModule과 SecondModule 컴포넌트에 각각 Wrapper라는 동일한 이름의 CSS 클래스를 다른 스타일로 작성하여 적용한 예제입니다.

FirstModule.css
.wrapper {
  color: yellow;
  background-color: black;
  text-align: center;
}
SecodeModule.css
.wrapper {
  color: white;
  background-color: blue;
  text-align: left;
}
FirstModule.js
import "./FirstModule.css";

const FirstModule = () => {
  return (
    <div className="wrapper">
      <h1>Hello, React!</h1>
    </div>
  );
};

export default FirstModule;
예제(SecondModule.js)
import "./SecondModule.css";

const SecondModule = () => {
  return (
    <div className="wrapper">
      <h1>Hello, React!</h1>
    </div>
  );
};

export default SecondModule;

 

하지만 최종 렌더링된 웹 페이지를 살펴보면 우리가 의도했던 것과는 달리 두 컴포넌트 모두 같은 스타일이 적용된 것을 확인할 수 있습니다. 이렇게 CSS 클래스명이 고유하지 않으면, 우리가 전혀 예상하지 못한 결과가 나타날 수 있습니다.

 

이제 두 CSS 파일의 코드는 변경하지 않고, CSS 파일의 이름과 컴포넌트에서 이를 불러와 사용하는 부분의 코드만을 수정하여 CSS Module이 적용되도록 하겠습니다.

FirstModule.js
import styles from "./FirstModule.module.css";

const FirstModule = () => {
  return (
    <div className={styles.wrapper}>
      <h1>Hello, React!</h1>
    </div>
  );
};

export default FirstModule;
예제(SecondModule.js)
import styles from "./SecondModule.module.css";

const SecondModule = () => {
  return (
    <div className={styles.wrapper}>
      <h1>Hello, React!</h1>
    </div>
  );
};

export default SecondModule;

 

컴포넌트에서 CSS Module을 불어와 저장할 때 사용한 styles라는 이름 대신에 어떠한 이름을 사용해도 괜찮습니다. 예제에서는 일반적으로 많이 사용하는 styles를 사용했을 뿐이며, 다른 이름을 사용해도 무방합니다.

위의 예제와 같이 CSS Module을 사용하면 이미 다른 CSS 파일에 wrapper 클래스가 정의되어 있더라도 해당 CSS 파일에 정의된 wrapper 클래스는 전혀 영향을 받지 않게 됩니다.

 

최종적으로 렌더링된 웹 페이지를 개발자 도구로 확인해 보면 각 컴포넌트에 적용된 클래스명이 우리가 작성한 wrapper라는 이름이 아닌 해시(hash) 값이 뒤에 붙은 고유한 클래스명으로 변경되어 있는 것을 확인할 수 있습니다.

자동으로 생성된 고유한 클래스명

[파일명]_[클래스명]__[해시값]

 

구현 방식에 따라 해시값을 사용하는 것이 아닌 클래스 등을 이용하여 고유한 클래스명으로 변경하는 경우도 있습니다.

 

css-modules-result


여러 개의 클래스 적용하기

만약 CSS Module이 적용된 파일로부터 여러 개의 CSS 클래스를 불러와 적용하고 싶다면 ES6 문법부터 제공되는 템플릿 리터럴(template literal)을 사용하여 여러 개의 클래스명을 하나의 문자열로 합하여 적용할 수 있습니다.

 

이 때 템플릿 리터럴의 시작과 끝을 알려주는 '` ' 문자를 백틱(backtick)이라고 부르며, '$' 문자와 중괄호({})가 합쳐진 문자열 인터폴레이션(string interpolation)은 '+' 연산자를 사용하지 않고도 손쉽게 자바스크립트 표현식을 문자열로 변환할 수 있게 해줍니다.

FirstModule.module.css
.wrapper {
  color: yellow;
  background-color: black;
  text-align: center;
}

.h1 {
  text-decoration: underline;
  text-shadow: 5px 2px gray;
}
예제(FirstModule.js)
import styles from "./FirstModule.module.css";

const FirstModule = () => {
  return (
    <div className={`${styles.wrapper} ${styles.h1}`}>
      <h1>Hello, React!</h1>
    </div>
  );
};

export default FirstModule;

 

만약 템플릿 리터럴을 사용하고 싶지 않다면 적용하고 싶은 CSS 클래스들의 이름을 하나의 배열로 만든 후에 자바스크립트의 join() 메소드를 사용하여 배열의 모든 요소를 하나의 문자열로 반환하여 적용할 수 있습니다.

join() 메소드를 이용한 방법
<div className={[styles.wrapper, styles.h1].join(" ")}>

CSS Module의 장점과 단점

CSS Module을 이용한 스타일링 방식은 다음과 같은 장점을 가지고 있습니다.

1. 동일한 클래스명의 재정의로 인한 스타일의 전역 오염을 미연에 방지할 수 있습니다.
2. 자동으로 고유한 클래스명으로 변환해주기 때문에 클래스명을 짓기 위한 개발자의 고민을 줄여줄 수 있습니다.
3. 컴포넌트 단위로 스타일을 관리할 수 있어서 스타일의 유지보수가 편해집니다.

 

다만 CSS Module은 모듈마다 별도의 CSS 파일을 작성해야 하기 때문에 별도로 많은 CSS 파일을 만들어 관리해야 한다는 단점을 가집니다. 또한, 클래스를 동적으로 추가할 경우 최종 렌더링된 결과물에서 자동 변환된 클래스명이 코드의 가독성을 어지럽히는 경우가 종종 발생합니다.

 

다음 예제는 논리 AND 연산자(&&)를 사용하여 isHighlighted 변수의 불리언값에 따라 styles.h1 스타일의 적용 유무를 제어하는 조건부 스타일 예제입니다.

예제(FirstModule.js)
import styles from "./FirstModule.module.css";

const FirstModule = () => {
  let isHovered = false;

  return (
    <div className={`${styles.wrapper} ${styles.h1 && isHovered}`}>
      <h1>Hello, React!</h1>
    </div>
  );
};

export default FirstModule;

 

이 예제를 렌더링한 웹 페이지를 확인해 보면 해당 컴포넌트에 적용되지 않은 CSS 클래스명이 불리언 값인 false와 함께 추가되어 있는 것을 확인할 수 있습니다. 이렇게 적용되지 않은 클래스명이 최종 렌더링된 웹 페이지에 추가됨으로써 코드의 가독성이 매우 좋지 않게 되었습니다.

 

css-modules-bad-result


classnames

이와 같은 문제점을 해결하기 위해 많은 개발자들이 CSS Module을 사용하는 경우에 classnames 라이브러리를 함께 사용하고 있습니다.

 

classnames 라이브러리는 CSS 클래스를 동적으로 설정하는 조건부 스타일링 작업에 매우 유용한 라이브러리이며, CSS Module에서 여러 개의 클래스를 동시에 적용할 때 매우 편리하게 사용할 수 있습니다.

 

React에서 classnames를 사용하기 위해서는 우선 classnames 라이브러리를 설치해야 합니다.

Shell

# npm인 경우
npm install classnames


# yarn인 경우
yarn add classnames

 

classnames의 기본적인 사용 방법은 다음과 같습니다.

classnames 문법
classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'

// 불리언 값인 false로 평가되는 값들은 모두 무시됩니다.
classNames(null, false, 'foo', undefined, 0, 1, { bar: null }, ''); // => 'foo 1'

 

위와 같이 여러 타입의 값들을 다양하게 조합하여 클래스명을 작성할 수 있기 때문에 CSS 클래스를 동적으로 설정할 때 매우 편리합니다. 또한, classnames 라이브러리에서 제공하는 bind() 메소드를 사용하면 매번 styles.[클래스명] 형태로 클래스를 호출하지 않고도 바인드한 이름을 사용하여 여러 클래스를 한 번에 불러올 수 있습니다.

 

bind() 메소드를 사용하기 위해서는 우선 classnames/bind 패키지에서 classNames를 불어와야 합니다.

이를 활용하여 CSS Module에서 동적으로 추가되는 클래스명의 좋지 않은 가독성 문제를 해결할 수 있습니다.

 

다음 예제를 렌더링한 웹 페이지를 확인해 보면 조건에 의해 적용되지 않은 클래스명은 최종 결과물에 추가되지 않은 것을 확인할 수 있습니다.

예제(FirstModule.js)
import styles from "./FirstModule.module.css";
import classNames from "classnames/bind";

const FirstModule = () => {
  const cx = classNames.bind(styles);
  let isHovered = false;

  return (
    <div className={cx("wrapper", { h1: isHovered })}>
      <h1>Hello, React!</h1>
    </div>
  );
};

export default FirstModule;

 

classnames 라이브러리에 대해 좀 더 자세히 알고 싶다면 공식 문서를 참고하시기 바랍니다.

◎ classnames 라이브러리 공식 홈페이지 (https://www.npmjs.com/package/classnames)


연습문제