Next.js: CSS Modules의 클래스명 조합(clsx, classnames) 최적화 적용기

Next.js CSS Modules 클래스명 조합(clsx, classnames) 최적화 적용 가이드
3초 요약: 이렇게 고치세요
  • 원인: 템플릿 리터럴로 조건부 클래스를 조합하면 삼항 연산자가 중첩되고, 조건이 꺼졌을 때 빈 문자열이나 false 문자열이 그대로 클래스에 섞여 들어감
  • 해결: clsx(또는 classnames)에 객체 문법 {[styles.클래스]: 조건}을 넘겨 조건이 참일 때만 클래스가 병합되게 함
  • 주의: CSS Module 클래스명은 리터럴 문자열이 아니라 반드시 대괄호로 계산된 속성(computed property) 형태로 넘겨야 함
clsx(styles.button, { [styles.active]: isActive })
↓ 왜 이런 문제가 생기는지 원리 보기

Next.js 프로젝트에서 컴포넌트 상태에 따라 CSS Modules 클래스를 조건부로 붙이다 보면, 템플릿 리터럴 안에 삼항 연산자가 두세 개만 겹쳐도 코드가 금방 지저분해집니다. 예전에 외주로 맡았던 어드민 대시보드 프로젝트에서 처음엔 그냥 && 단축 패턴으로 조건을 하나씩 욱여넣는 식으로 땜질하다가, 조건이 꺼졌을 때 클래스에 false라는 문자열이 그대로 찍혀서 스타일이 깨진 걸 보고 나서야 clsx 객체 문법으로 정리하는 패턴을 쓰게 됐습니다. 그 과정에서 정리한 실전 코드와, 처음엔 놓치기 쉬운 함정 몇 가지를 공유합니다.


템플릿 리터럴(Template Literals) 방식의 한계와 고통

상태에 따라 스타일을 다르게 주어야 하는 흔한 예시인 '버튼(Button)' 컴포넌트를 예로 들어보겠습니다. CSS Modules를 연동한 뒤, 템플릿 리터럴로 다중 조건부 스타일을 조합하면 보통 아래와 같이 구현하게 됩니다.

// 템플릿 리터럴을 사용한 기존의 조건부 스타일 조합
<button
  className={`${styles.button} ${isPrimary ? styles.primary : ''} ${isActive ? styles.active : ''} ${isDisabled ? styles.disabled : ''}`}
>
  전송하기
</button>

가독성 저하 및 공백 부작용

  • 복잡한 3항 연산자: 조건이 하나씩 추가될 때마다 코드 가독성이 기하급수적으로 저하됩니다.
  • 지저분한 DOM: 위 예시처럼 삼항 연산자에 빈 문자열('')을 폴백으로 쓰면 조건이 꺼졌을 때 class="button "처럼 불필요한 공백이 여러 개 남습니다. && 단축 패턴({isPrimary && styles.primary})을 쓰면 조건이 거짓일 때 false라는 문자열이 그대로 클래스에 삽입되는 실수도 흔합니다.
  • 휴먼 에러 유발: 문자열 사이의 띄어쓰기 한 칸 실수로 인해 스타일이 완전히 깨지는 부작용이 발생합니다.

해결사 등장: clsx와 classnames

이러한 문자열 지옥(String Concatenation Hell)을 해결하기 위해 클래스 조합 유틸리티 라이브러리를 도입하는 것이 표준으로 자리 잡았습니다. 대표적인 라이브러리로 classnamesclsx가 있습니다.

이 유틸리티들은 문자열, 객체, 배열 등 다양한 형태의 입력을 받아 유효한 클래스명만 필터링한 후 하나의 문자열로 결합해 줍니다. 특히 clsx는 classnames와 호환되는 API를 제공하면서도 훨씬 가볍고 빨라, classnames의 대체재로 널리 쓰이는 편입니다.


Next.js 실전 코드 적용 예시

실제 Next.js의 컴포넌트에서 CSS Modules와 clsx를 어떻게 조합하는지 구체적인 코드 예시를 통해 살펴보겠습니다.

Button.module.css

.button {
  padding: 10px 20px;
  font-size: 16px;
  border-radius: 8px;
  border: none;
  cursor: pointer;
  transition: background-color 0.2s ease;
}

.primary {
  background-color: #0070f3;
  color: white;
}

.active {
  box-shadow: 0 0 0 3px rgba(0, 112, 243, 0.5);
}

.disabled {
  background-color: #eaeaea;
  color: #999;
  cursor: not-allowed;
}

Button.jsx

import React from 'react';
import clsx from 'clsx';
import styles from './Button.module.css';

export default function Button({ isPrimary, isActive, isDisabled, children }) {
  return (
    <button
      type="button"
      disabled={isDisabled}
      className={clsx(
        styles.button,
        {
          [styles.primary]: isPrimary,
          [styles.active]: isActive,
          [styles.disabled]: isDisabled,
        }
      )}
    >
      {children}
    </button>
  );
}
Key Point: clsx 안에 객체 형태로 {[styles.className]: 조건}을 전달하면, 조건이 참(true)일 때만 해당 CSS Module 클래스명이 병합됩니다. 코드 가독성이 개선된 것을 확인할 수 있습니다.

이때 객체의 키는 반드시 [styles.primary]처럼 대괄호로 감싼 계산된 속성(computed property)이어야 합니다. 대괄호 없이 {'{ primary: isPrimary }'}처럼 쓰면 primary라는 문자열 그대로가 클래스명으로 붙는데, CSS Modules는 .primaryButton_primary__xY3kq 같은 해시 클래스명으로 바꿔버리기 때문에 이 리터럴 문자열은 실제 CSS와 연결되지 않아 스타일이 먹지 않습니다. [styles.primary]로 감싸야 CSS Modules가 실제로 생성한 해시 클래스명이 키로 들어갑니다.


도구 비교 (어떤 것을 선택해야 할까?)

프로젝트 특성에 맞는 기술 의사결정을 돕기 위한 번들 크기, 가독성, 성능 측면의 비교표입니다. 번들 크기는 Bundlephobia 기준 minified+gzip 수치입니다.

비교 항목 템플릿 리터럴 (Vanilla) classnames clsx (추천)
번들 크기 (Bundle Size) 0B (내장 기능) ~474B ~228B
가독성 (Readability) 복잡함 (3항 연산자 중첩) 우수 (객체/배열 지원) 우수 (객체/배열 지원)
성능 (Performance) 빠름 (단순 문자열 연산) 보통 (객체 순회 오버헤드) 빠름 (가벼운 만큼 실행 오버헤드도 적음)
의존성 (Dependency) 없음 외부 패키지 외부 패키지

호마다의 웹개발 팁

Next.js에서 CSS Modules와 clsx로 조건부 스타일을 깔끔하게 관리하는 패턴은 프론트엔드 유지보수성의 기본기입니다. 자바스크립트/CSS 기초부터 차근차근 정리한 글은 [호마다의 IT 개발 입문 블로그] 에서 확인하실 수 있습니다.

댓글

이 블로그의 인기 게시물

TypeScript: Unknown vs Any 타입 차이점과 안전한 타입 가드(Type Guard)

Next.js 하이드레이션 오류 원인과 해결법 완벽 정리

Next.js App Router에서 getServerSideProps 대체 및 데이터 페칭법