Life is connecting the dots .

Next.js에서 CSS module, Styled-components 사용하기 본문

Programming/Next.js

Next.js에서 CSS module, Styled-components 사용하기

soyeori 2023. 12. 2. 19:18

최근에 React와 Next.js 프로젝트에서 CSS module과 styled-components를 사용해 보았다. 어느 방법이던 스타일 코드를 작성할 때 비슷한 부분이 많았지만 설정이 필요한 부분이나, 조건에 따른 스타일링을 적용방법, 주의할 점 등에 대해 새롭게 알게 된 점이 있어서 나중에 필요할 때 꺼내보고자 정리해 두려고 한다.

CSS module

기본 사용법

CSS module의 기본 사용법은 다음과 같다. module.css 확장자를 사용하여 CSS 스타일 파일을 만들어 주고, 사용처에서는 해당 파일을 import해와서 사용하면 된다. import로 불러온 styles 객체 안에 있는 값인 CSS 파일에서 선언한 클래스 이름을 참조해서 className을 지정해 준다. 클래스이름이 .card-image처럼 하이픈(-)으로 연결되어 있을 때는 대괄호를 사용하여 올바르게 styles 객체의 속성을 참조할 수 있도록 처리해줘야 한다.

import styles from "./Card.module.css";

export default function CardItem() {
  ...
  return (
    <div className={styles.wrapper}>
      <div className={styles.container}>
        <div className={styles["card-image"]}>
          ...
        </div>
      </div>
    </div>
  );
}

여러 개의 클래스이름 사용

조건에 따른 클래스이름을 사용하거나 클래스이름을 조합해서 넣어줘야 하는 경우 백틱(``)을 사용해서 여러 개의 클래스이름을 문자열로 넣어줘야 한다. 간편하게 사용할 수 있지만 조건에 따라 클래스이름을 적용해야 하는 경우는 가독성이 떨어질 수 있다.

// 여러개 사용
<div className={`${styles.wrapper} ${styles.container}`}>

// 조건에 따른 클래스이름 적용
<div className={`${styles.wrapper} ${isImageUrl ? styles.container : ''}`}>

 

구글링 해보니 클래스이름을 좀 더 편하게 사용할 수 있게 해주는 classnames 라이브러리를 많이 사용한다고 한다. 자세한 사용법은 해당 깃헙에 들어가면 여러 가지 예시들을 볼 수 있다. 우선 bind를 사용하면 하나씩 styles 객체에서 꺼내오지 않고, 클래스이름만으로 사용할 수 있게 해 준다. 또한 classNames을 사용하여 여러 개의 클래스 이름을 묶어서 사용할 수 있고, 특정 key에 대한 value를 참/거짓으로 지정하여 클래스이름의 포함여부를 결정할 수 있다.

아래 코드처럼 cardStyle은 classNames를 사용해서 여러 클래스이름을 적용하되, 조건에 따른 클래스이름을 적용할 수 있게 해 준다. 여기서 주의할 점은 styles["card-image"]은 문자열이지만 객체 내에서 동적으로 속성을 지정할 때 올바르게 인식할 수 있도록 대괄호를 사용해야 한다.

import styles from "./Card.module.css";
import classNames from "classnames";

export default function CardItem() {
  ...
  // imageSourceUrl은 true or false
  const cardStyle = classNames(styles.default, { [styles["card-image"]]: imageSourceUrl });
  
  return (
    <div className={styles.wrapper}>
      <div className={styles.container}>
        <div className={styles["card-image"]}>
          <img src={imageUrl} alt="profile" className={cardStyle} /> // 추가된 부분
        </div>
      </div>
    </div>
  );
}

Styled-components

Next.js에서 사용하기

Next.js에서 styled-components를 사용하려면 추가 설정이 필요하다. 설정 없이 그냥 사용하면 스타일 컴포넌트에 Prop를 전달해서 다른 스타일을 적용해야 하는 경우처럼 클래스이름이 변경되는 상황에 대응할 수 없기 때문에 오류가 발생한다. React만 사용할 때는 문제없지만 Next.js와 사용하는 경우 프리렌더링 때 부여받은 클래스이름과 hydration과정에서 변경된 클래스이름이 다르기 때문에 prop 'className' did not match. 에러가 발생하는 것이다.

기존에는 babel-plugin-styled-components을 설치해야 했는데, 버전 12부터는 Next.js 컴파일러에 babel-plugin-styled-components을 내부에서 자동으로 관리해 주기 때문에 별도의 플러그인 설치 없이 next.config.js 파일을 업데이트하면 styled-components 사용이 가능하다. 

// next.config.js

const nextConfig = {
  compiler: {
    styledComponents: true,
  },
};

module.exports = nextConfig;

 

Transient Prop

styled-components의 기본 사용법은 Emotion과 같은 다른 CSS-in-JS와 유사하고, Prop로 받는 타입 지정 방법도 공식문서를 참고하여 적용할 수 있었다. 다만 Prop을 넘겨줄 때 prefix(접두사)로 '$'을 함께 사용해야 한다. 그렇지 않으면 다음과 같은 경고메시지가 발생하는데 경고문을 해석해 보면 styled-components에서 전달된 프로퍼티(isDelete)가 DOM으로 전달될 때, 리액트에서 해당 프로퍼티를 인식할 수 없기 때문에 발생한 메시지로 보인다. 이 부분은 Next.js를 사용하는 것과는 상관없이 React만 사용했을 때도 발생하는 문제이다.

// Button.tsx

import styled from "styled-components";

interface ButtonProps {
  action: string;
  label: string;
}

export default function Button({ action, label }: ButtonProps) {
  return <StyledButton isDelete={action === "delete"}>{label}</StyledButton>;
}

const StyledButton = styled.button<{ isDelete: boolean }>`
  ...
  background: ${({ isDelete }) => (isDelete ? "var(--color-red)" : "")};
`;

💡using transient props (`$` prefix for automatic filtering.)
lowercase를 적용해도 경고는 사라지지 않는다..🤔

 

여기서 문제는 isDelete라는 Prop은 StyledButton이라는 스타일드 컴포넌트에서만 쓰려고 만든 것인데, 의도치 않게 StyledButton이 스타일링하고 있는 Button 컴포넌트에 inDelete Prop이 전달되어 발생한 것이다. React는 DOM 엘리먼트에 기본적으로 알지 못하는 속성이나 프로퍼티가 전달되면 경고를 발생시킨다. 즉, 표준 HTML에서 사용하는 속성이나 이벤트에 대해서만 인식하고 처리하기 때문에 그 때 표준이 아닌 속성이나 프로퍼티가 DOM 엘리먼트에 전달되면 React는 해당 속성을 인식하지 못한다는 경고를 보여주는 것이다.

 

그렇다면 근본적으로 isDelete라는 Prop이 의도치 않게 전달되는 것을 막고 StyledButton에서만 사용하게 하는 것이 올바른 Prop전달 방법이 될 것이다. 이때, Transient Prop을 사용하여 원치 않는 Prop이 전달되는 것을 막아줄 수 있다.

Transient Prop을 사용하면 Styled Components로 스타일링하는 컴포넌트에서만 Prop을 사용하고, 스타일링의 대상이 되는 컴포넌트에는 Prop이 전달되지 않도록 할 수 있다. 경고메시지에도 나와있듯이 Transient Prop을 만들려면 접두사 '$'기호를 붙여서 사용할 수 있다.  prefix를 붙여서 코드를 수정했더니 더 이상 경고메시지가 나오지 않았다.

// Button.tsx
...

export default function Button({ action, label }: ButtonProps) {
  return <StyledButton $isDelete={action === "delete"}>{label}</StyledButton>;
}

const StyledButton = styled.button<{ $isDelete: boolean }>`
  ...
  background: ${({ $isDelete }) => ($isDelete ? "var(--color-red)" : "")};
`;

마무리

CSS-module과 Styled-components 이렇게 두 가지 방법을 최근에 사용했던 이유는 React + Styled-components를 사용했던 프로젝트를 Next.js로 마이그레이션하는 과정에 있었다. Next.js 최신버전에서 Styled-components의 사용이 불가능하거나 어려움이 많다는 점이 있다는 것을 들어서 Styled-components를 모듈로 변경하고자 했었기 때문이다. 또한 CSS module도 한번 사용해 보자 하는 마음도 있었던 것 같다. 결국 몇 개의 컴포넌트를 변경하다가 너무 번거로워서 설정파일을 추가하는 방법을 찾게 된 것이다.

 

한편으로 드는 생각은 리액트 자바스크립트로 된 프로젝트를 타입스크립트로 마이그레이션 할 때도 styled-components의 Prop타입 지정이나 파일 확장자를 변경하는 등 상황이 바뀔 때마다 무언가를 변경해줘야 하는 부분이 반드시 있는 것 같다. 그에 비해 CSS module을 사용하면 변화하는 상황에서도 모듈파일 자체는 변경해줘야 하는 부분이 거의 없다는 생각이 들었다. CSS module을 사용했을 때 여러 장점이 있지만 이 부분이 내가 느낀 CSS module을 사용했을 때 가장 좋은 점인 것 같다.

 

 


[ 참고 ]

Next.js 공식문서 - SWC