다양한 방식의 리액트 컴포넌트 스타일링 방식 CSS, Sass, CSS Module, styled-components

Minjun Kim·2018년 10월 21일
134
post-thumbnail

리액트에서는 컴포넌트를 스타일링 할 때 다양한 방식을 사용 할 수 있습니다. 이 튜토리얼에서는 어떤 방식이 있는지, 자주 사용되는 것들을 하나하나 사용해보겠습니다.

저는 개인적으로 컴포넌트 하나마다 Sass 파일 하나씩 만들어서 관리를 하는것을 선호하고, 최근 만드는 프로젝트에서는 styled-components 를 사용하기도 합니다 :) CSS Module 은 작년에 자주 사용했었는데 사용하기가 요즘은 조금 불편하다고 느껴져서 잘 사용하고 있지 않고 있습니다.

이 포스트는 옛날에 제 블로그에 작성한 리액트 컴포넌트 스타일링 을 최신화한 포스트 입니다. CRA 버전이 달라져서 webpack 설정 부분에 변화가 있었습니다. 추가적으로, 이전에 집필한 리액트를 다루는 기술 에서 다룬 내용에서도 CRA 가 업데이트됨에 따라 설정방식이 달라졌는데 9장 컴포넌트 스타일링 대신 이 튜토리얼을 진행하시면 도움이 될 것입니다. 만약에 책이랑 똑같은 방식으로 진행을 하시려면 CRA 1.1.5 버전을 사용하셔야됩니다 (참고)


이 튜토리얼에선 create-react-app 을 사용하여 프로젝트를 직접 만들어서 진행하도록 하겠습니다.

$ npx create-react-app styling-react

가장 흔한 방식, 그냥 CSS

기존의 CSS 스타일링에 있어서 딱히 불편함을 느끼지 않고, 새로운 기술을 배우는게 불필요하다고 생각한다면 그냥 CSS 를 사용하시면 됩니다.

실제로, 그냥 작은 프로젝트라면 다른 시스템을 도입하는것 조차 고민하기도 귀찮습니다. 그런 상황에는 프로젝트에 기본적으로 적용되어있는 CSS 시스템을 사용하는것만으로도 충분합니다.

create-react-app 으로 만든 프로젝트를 보면, src 디렉토리에 App.js 와 App.css 가 있습니다.

App.js

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    );
  }
}

export default App;

App.css

.App {
  text-align: center;
}

.App-logo {
  animation: App-logo-spin infinite 20s linear;
  height: 40vmin;
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

우선, CSS 를 작성하게 될 때 가장 중요한점은 중복되는 클래스명 생성하지 않기 입니다. 클래스명이 중복되는것을 방지하기 위해선 꽤 다양한 방식이 있는데, 하나는 네이밍이고 하나는 CSS Selector 의 활용입니다.

create-react-app 으로 만든 App 컴포넌트를 보면, 클래스명이 컴포넌트명-클래스 으로 네이밍이 되어있습니다.

이런 형식 외에도, BEM Naming 이란 방식도 있는데, 컴포넌트마다 CSS 파일을 하나하나 만드는 상황에선 그렇게 권장되지는 않습니다. BEM 은 스타일을 하나의 파일에 몰아서 작성 할 때는 편리하지만, 클래스 파일이 여러개일때는 딱히 이렇게까지 이름을 지을 필요는 없다고 생각합니다.

다른 방식으로는, CSS Selector 를 활용하는 것 입니다. 예를들어서 위 CSS 를 다음과같이 변환을 하면:

App.css

.App {
  text-align: center;
}

/*.App 안에 들어있는 .logo*/
.App .logo {
  animation: App-logo-spin infinite 20s linear;
  height: 40vmin;
}

/* .App 안에 들어있는 header */
.App header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

/* .App 안에 들어있는 a 태그 */
.App a {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

App.js 에서 사용된 클래스명들은 다음과 같이 수정해주면 됩니다.

App.js

import React, { Component } from "react";
import logo from "./logo.svg";
import "./App.css";

class App extends Component {
  render() {
    return (
      <div className="App">
        <header>
          <img src={logo} className="logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    );
  }
}

export default App;

이런식으로, 컴포넌트의 최상위 html 요소에는 컴포넌트 이름과 동일한 이름으로 클래스명을 지으시고, 그 하단에서는 소문자로 입력하거나, 딱히 클래스명이 불필요한 경우엔 아예 생략을 할 수도 있죠.

Sass 사용하기

Sass (Syntactically Awesome Style Sheets: 문법적으로 짱 멋진 스타일시트) 는 CSS pre-processor 로서, 복잡한 작업을 쉽게 할 수 있게 해주고, 코드의 재활용성을 높여줄 뿐 만 아니라, 코드의 가독성을 높여주어 유지보수를 쉽게해줍니다.

이전 버전의 create-react-app 으로 만든 프로젝트에서는 Sass 를 사용하기위하여 별도의 작업을 추가적으로 해줬어야 하는데, v2 다음 버전부터는 그냥 바로 사용하실 수 있습니다.

Sass 가 익숙하지 않은 분들은 제가 쓴 포스트Sass 가이드 를 참고해보시는것을 권장드립니다.

Sass 에서는 두가지의 확장자 (.scss/.sass) 를 지원합니다. Sass 가 처음 나왔을떈 sass 만 지원되었고, sass 는 문법이 아주 다른데요:

.sass

$font-stack:    Helvetica, sans-serif
$primary-color: #333

body
  font: 100% $font-stack
  color: $primary-color

.scss

$font-stack:    Helvetica, sans-serif;
$primary-color: #333;

body {
  font: 100% $font-stack;
  color: $primary-color;
}

더 많은 차이점들은 여기 서 비교해볼 수 있습니다. 보통 scss 문법이 더 많이 사용되므로, 우리는 .scss 확장자로 스타일을 작성하겠습니다.

한번 새 컴포넌트를 만들어서 Sass 를 사용해볼게요!

우선, node-sass 라는 라이브러리를 설치해야합니다. 이 라이브러리는 Sass 를 CSS 로 변환해줍니다.

$ yarn add node-sass

다음, src 디렉토리에 SassComponent.scss 파일을 생성하세요.

SassComponent.scss

// 변수 사용하기
$red: #fa5252;
$orange: #fd7e14;
$yellow: #fcc419;
$green: #40c057;
$blue: #339af0;
$indigo: #5c7cfa;
$violet: #7950f2;
// mixin 만들기 (재사용되는 스타일 블록을 함수처럼 사용 할 수 있음)
@mixin square($size) {
  $calculated: 32px * $size;
  width: $calculated;
  height: $calculated;
}

.SassComponent {
  display: flex;
  .box {
    background: red; // 일반 CSS 에선 .SassComponent .box 와 마찬가지
    cursor: pointer;
    transition: all 0.3s ease-in;
    &.red {
      // .red 클래스가 .box 와 함께 사용 됐을 때
      background: $red;
      @include square(1);
    }
    &.orange {
      background: $orange;
      @include square(2);
    }
    &.yellow {
      background: $yellow;
      @include square(3);
    }
    &.green {
      background: $green;
      @include square(4);
    }
    &.blue {
      background: $blue;
      @include square(5);
    }
    &.indigo {
      background: $indigo;
      @include square(6);
    }
    &.violet {
      background: $violet;
      @include square(7);
    }
    &:hover {
      // .box 에 마우스 올렸을 때
      background: black;
    }
  }
}

SassComponent.js 컴포넌트 파일도 만드세요.

SassComponent.js

import React from 'react';
import './SassComponent.scss';

const SassComponent = () => {
  return (
    <div className="SassComponent">
      <div className="box red" />
      <div className="box orange" />
      <div className="box yellow" />
      <div className="box green" />
      <div className="box blue" />
      <div className="box indigo" />
      <div className="box violet" />
    </div>
  );
};

export default SassComponent;

그리고 해당 컴포넌트를 App.js 에서 렌더링하세요:

import React, { Component } from "react";
import SassComponent from "./SassComponent";

class App extends Component {
  render() {
    return <SassComponent />;
  }
}

export default App;

utils 함수 분리하기

자주 사용 될 수도 있는 Sass 변수 및 믹스인을 따로 파일로 분리해보겠습니다.

src 디렉토리 안에 styles 라는 디렉토리를 생성하고, 그 안에 utils.scss 파일을 만들어서, 기존 SassComponent 스타일 코드에 작성됐던 변수들과 믹스인을 잘라내서 이동시키겠습니다.

src/styles/utils.scss

// 변수 사용하기
$red: #fa5252;
$orange: #fd7e14;
$yellow: #fcc419;
$green: #40c057;
$blue: #339af0;
$indigo: #5c7cfa;
$violet: #7950f2;
// mixin 만들기 (재사용되는 스타일 블록을 함수처럼 사용 할 수 있음)
@mixin square($size) {
  $calculated: 32px * $size;
  width: $calculated;
  height: $calculated;
}

이제, SassComponent.scss 에서 위 파일에서 선언한 것들을 사용해보겠습니다. 다른 scss 파일을 불러올 땐 @import 구문을 사용합니다.

src/SassComponent.scss

@import './styles/utils.scss';
.SassComponent {
  display: flex;
  .box {
    background: red; // 일반 CSS 에선 .SassComponent .box 와 마찬가지
    cursor: pointer;
    transition: all 0.3s ease-in;
    &.red {
      // .red 클래스가 .box 와 함께 사용 됐을 때
      background: $red;
      @include square(1);
    }
    &.orange {
      background: $orange;
      @include square(2);
    }
    &.yellow {
      background: $yellow;
      @include square(3);
    }
    &.green {
      background: $green;
      @include square(4);
    }
    &.blue {
      background: $blue;
      @include square(5);
    }
    &.indigo {
      background: $indigo;
      @include square(6);
    }
    &.violet {
      background: $violet;
      @include square(7);
    }
    &:hover {
      // .box 에 마우스 올렸을 때
      background: black;
    }
  }
}

동일하게 작동하는지 확인해보세요!

sass-loader 설정 커스터마이징하기

이 부분은 Sass 를 사용 할 때 필수적인 작업은 아니지만, 해두면 좋을 수도 있는 작업입니다! 예를들어서, 방금 우리가 SassComponent 에서 styles/utils.scss 를 불러올 때, @import '../styles/utils.scss';

이런식으로 구현을 했었는데요, 만약에 프로젝트에 디렉토리를 깊숙한 구조로 만든다면, (예: src/components/somefeature/ThisComponent.js) import '../../../styles/utils.scss' 이런식으로 한참 거슬러올라가야 한다는 단점이 있습니다.

이 문제점를 핵려하는 방법은 Webpack에서 Sass 를 처리하는 sass-loader 의 설정을 커스터마이징하여 해결 할 수 있습니다. 그렇지만, create-react-app 으로 만든 프로젝트는 프로젝트 구조의 복잡도를 낮추기 위하여 세부 설정들이 모두 숨어있습니다. 이를 커스터마이징 하기 위해서는 yarn eject 명령어를 통하여 세부설정을 다시 밖으로 꺼내주어야 합니다.

create-react-app 으로 만든 프로젝트는 기본적으로 .git 설정도 되어있는데요, yarn eject 는 아직 git 에 커밋되지 않은 변화가 있다면 진행이 되지 않으니, 먼저 커밋을 해주어야 합니다.

VSCode 의 git UI 를 사용하셔서 현재까지 한 작업을 커밋하시거나, 다음 명령어를 통해서 커밋을 하세요.

$ git add .
$ git commit -m'Commit before yarn eject'

그리고 나서, yarn eject 명령어를 실행합니다.

$ yarn eject
yarn run v1.12.0
warning ../package.json: No license field
$ react-scripts eject
? Are you sure you want to eject? This action is permanent. (y/N) y

이제 config 경로가 프로젝트에 생겼을텐데요, 그 안에 webpack.config.js 를 열어보세요.

그리고, sassRegex 를 찾아보시면

            {
              test: sassRegex,
              exclude: sassModuleRegex,
              use: getStyleLoaders(
                {
                  importLoaders: 2,
                  sourceMap: isEnvProduction && shouldUseSourceMap,
                },
                'sass-loader'
              ),
              sideEffects: true,
            },

이런 블록이 보일텐데, 여기서 use: 부분을 문자열을 다음과 같이 교체하세요:

            {
              test: sassRegex,
              exclude: sassModuleRegex,
              use: getStyleLoaders({
                importLoaders: 2,
                sourceMap: isEnvProduction && shouldUseSourceMap
              }).concat({
                loader: require.resolve('sass-loader'),
                options: {
                  includePaths: [paths.appSrc + '/styles'],
                  sourceMap: isEnvProduction && shouldUseSourceMap
                }
              }),
              // Don't consider CSS imports dead code even if the
              // containing package claims to have no side effects.
              // Remove this when webpack adds a warning or an error for this.
              // See https://github.com/webpack/webpack/issues/6571
              sideEffects: true
            },

getStylesLoaders 에서 두번째 파라미터로는 문자열형태로밖에 받아오지 못하니, 위와 같은 형태로 getStyleLoaders 로 만든 배열 뒷부분에다가 우리가 사용할 loader를 options 와 함께 적용을 해주었습니다.

작성 후, 서버를 껐다가 재시작하세요.

이제, utils.scss 파일을 불러올 때, 현재 수정하고 있는 scss 파일 경로가 어디에 위치하더라도, 앞부분에 상대경로를 입력 할 필요 없이 styles 디렉토리 기준 절대경로로 불러올 수 있습니다.

@import 'utils.scss';

한번, SassComponent.scss 파일에서 기존 import 구문을 위와 같이 수정해보고 적용이 잘 되는지 확인해보세요.

이제 utils 를 사용 하는 컴포넌트가 있다면 위 한줄만 붙여넣어주시면 됩니다.

하지만! 이렇게 utils.scss 를 포함시키는것 조차 귀찮을 수도 있을 것 입니다. 그럴땐, sass-loader 의 data 설정을 사용하시면 됩니다. data 속성은, Sass 파일을 읽을 때 마다 앞부분에 특정 코드를 포함시켜줍니다.

webpack.config.js 를 열어서 data 를 포함시켜보세요:

            {
              test: sassRegex,
              exclude: sassModuleRegex,
              use: getStyleLoaders({
                importLoaders: 2,
                sourceMap: isEnvProduction && shouldUseSourceMap
              }).concat({
                loader: require.resolve('sass-loader'),
                options: {
                  includePaths: [paths.appSrc + '/styles'],
                  sourceMap: isEnvProduction && shouldUseSourceMap,
                  data: `@import 'utils';`
                }
              }),
              // Don't consider CSS imports dead code even if the
              // containing package claims to have no side effects.
              // Remove this when webpack adds a warning or an error for this.
              // See https://github.com/webpack/webpack/issues/6571
              sideEffects: true
            },

이렇게 하고 나면, 모든 scss 파일에서 utils 를 사용 할 수 있게 되므로, SassComponent 에서 맨 위 import 구문을 지워도 정상적으로 작동 할 것입니다.

node_modules 에서 불러오기

yarn혹은 npm 을 통하여 설치한 Sass 라이브러리를 불러오는 방법을 알아봅시다. 가장 기본적인 방식으로는

@import '../../../node_modules/library/styles';

과 같은 형식으로 위로 거슬러 올라가주어야 합니다. 하지만, 이것보다 더욱 쉬운 방법이 있는데요, 바로 물결 ~ 를 사용 하는 것 입니다.

@import '~library/styles';

제가 자주 사용하는 Sass 라이브러리중에서는 반응형 디자인을 쉽게 해주는 include-media 와 색상 팔레트인 open-color 가 있는데요, 만약 이를 사용하게 된다면, 다음과 같이 불러오면 됩니다.

$ yarn add open-color include-media 
@import '~include-media/dist/include-media';
@import '~open-color/open-color';

패키지 디렉토리 안에 있는 scss 파일을 불러와야 하므로, node_modules 안으로 들어가서 디렉토리를 확인 후 필요한 파일을 불러오시면 됩니다.

CSS Module

CSS Module 은 CSS 클래스를 불러와서 사용 할 때 [파일이름]_[클래스이름]__[해쉬값] 형태로 클래스네임을 자동으로 고유한 값으로 만들어주어서 컴포넌트 스타일 중첩현상을 방지해주는 기술입니다. 이를 사용하기 위해선, [파일이름].module.css 이런식으로 파일을 저장하셔야 합니다.

한번, CSSModule.module.css 라는 스타일을 먼저 작성해봅시다.

src/CSSModule.module.css

/* 자동으로 고유해질 것이므로 흔히 사용되는 단어를 클래스 이름으로 마음대로 사용가능*/

.wrapper {
  background: black;
  padding: 1rem;
  color: white;
  font-size: 2rem;
}

/* 글로벌 CSS 를 작성하고 싶다면 */

:global .something {
  font-weight: 800;
  color: aqua;
}

자바스크립트로 컴포넌트도 작성해볼까요?

src/CSSModule.js

import React from 'react';
import styles from './CSSModule.module.css';
const CSSModule = () => {
  return (
    <div className={styles.wrapper}>
      안녕하세요, 저는 <span className="something">CSS Module!</span>
    </div>
  );
};

export default CSSModule;

위 코드처럼 styles 를 불러오면 하나의 객체를 전달받게 되는데 그 안에는 CSS Module 에서 사용한 클래스 이름과, 해당 이름을 고유화시킨 값이 key-value 형태로 들어있습니다.

한번 console.log(styles) 를 하면 이런 결과가 나타납니다:

{
  wrapper: "CSSModule_wrapper__CUMkx"
}

그리고, 이걸 사용하기 위해선 className={styles.[클래스이름]} 형태로 설정을 해주시면 됩니다.

다 작성하셨으면 App 컴포넌트에서 CSSModule 컴포넌트를 렌더링해봅시다.

src/App.js

import React, { Component } from 'react';
import CSSModule from './CSSModule';
class App extends Component {
  render() {
    return <CSSModule />;
  }
}

export default App;

이런식으로, 잘 작동하나요?

만약에 CSS Module 을 사용한 클래스이름을 두개 이상 적용 할 때는 이렇게 하시면 됩니다:

src/CSSModule.module.css

/* 자동으로 고유해질 것이므로 흔히 사용되는 단어를 클래스 이름으로 마음대로 사용가능*/

.wrapper {
  background: black;
  padding: 1rem;
  color: white;
  font-size: 2rem;
}

.inverted {
  color: black;
  background: white;
}

/* 글로벌 CSS 를 작성하고 싶다면 */

:global .something {
  font-weight: 800;
  color: aqua;
}

src/CSSModule.js

import React from 'react';
import styles from './CSSModule.module.css';

const CSSModule = () => {
  return (
    <div className={`${styles.wrapper} ${styles.inverted}`}>
      안녕하세요, 저는 <span className="something">CSS Module!</span>
    </div>
  );
};

export default CSSModule;

CSS Module 이 잘 작동하고 있나요?

classNames

classNames 는 CSS 클래스를 조건부로 설정 할 때 매우 유용한 라이브러리입니다. 그리고, CSS Module 을 사용 할 때 이 라이브러리를 함께 사용 한다면 여러 클래스를 적용할 때 편해집니다.

우선, classNames 를 설치하세요:

$ yarn add classnames

한번, classNames 의 기본적인 사용법을 훑어보겠습니다.

import classNames from 'classnames';

classNames('one', 'two'); // 'one two'
classNames('one', { two: true }); // 'one two'
classNames('one', { two: false }); // 'one'
classNames('one', ['two', 'three']); // 'one two three'

const myClass = 'hello';
classNames('one', myClass, { myCondition: true }); //'one hello myCondition'

이런식으로 여러가지 종류의 파라미터를 조합해 CSS 클래스를 설정 할 수 있게 되기 때문에, 컴포넌트에서 조건부로 클래스를 설정할때 굉장히 편합니다. 예를 들어서 props 의 값에 따라 다른 스타일을 주게 하는게 쉬워지죠:

const MyComponent = ({ highlighted, theme }) => {
  <div className={classNames('MyComponent', { highlighted }, theme)}>Hello</div>
}

이렇게하면 위 엘리먼트의 클래스로는 highlighted 값이 true 이나 false 에 따라 highlighted 라는 클래스가 적용 될 것이고, 추가적으로 theme 으로 전달받는 문자열이 그대로 클래스에 적용 될 것입니다.

만약에 이런 라이브러리의 도움을 받지 않는다면 이런 형식으로 처리를 하셔야 될 겁니다:

const MyComponent = ({ highlighted, theme }) => {
  <div className={`MyComponent ${theme} ${highlighted ? 'highlighted' : ''}`}>Hello</div>
}

classNames 를 쓰는 것이 훨씬 가독성이 높지요?

추가적으로, CSS Module 과 함께 쓸 땐 어떻게 편해질 수 있는지 알아보겠습니다.

classNames 를 불러올때 classnames/bind 를 사용하면 클래스를 넣어줄 때 마다 styles.[클래스] 형식으로 할 필요 없이, 사전에 미리 styles 에서 받아와서 사용하게끔 설정해두고 cx('class1', 'class2') 형태로 사용 할 수 있게 됩니다.

한번 CSSModule.js 컴포넌트를 다음과 같이 작성해보세요:

src/CSSModule.js

import React from 'react';
import classNames from 'classnames/bind';
import styles from './CSSModule.module.css';

const cx = classNames.bind(styles); // 미리 styles 에서 클래스를 받아오도록 설정하고

const CSSModule = () => {
  return (
    <div className={cx('wrapper', 'inverted')}>
      안녕하세요, 저는 <span className="something">CSS Module!</span>
    </div>
  );
};

export default CSSModule;

classnames/bind 를 사용하면, CSS Module 을 사용 할 때 클래스를 여러개 설정하거나 또는 조건부로 설정을 하게 될 때, 훨씬 편하게 작성 할 수 있겠죠?

Sass 와 함께 사용하기

Sass 를 사용할때도 파일이름 뒤에 .module.scss 을 입력해주면 CSS Module 로 사용 할 수 있습니다.
한번 파일 이름을 변경해보세요. 스타일 코드도 조금 바꾸겠습니다.

CSSModule.module.scss

/* 자동으로 고유해질 것이므로 흔히 사용되는 단어를 클래스 이름으로 마음대로 사용가능*/

.wrapper {
  background: black;
  padding: 1rem;
  color: white;
  font-size: 2rem;
  &.inverted {
    // inverted 가 .wrapper 와 함께 사용 됐을 때만 적용
    color: black;
    background: white;
    border: 1px solid black;
  }
}

/* 글로벌 CSS 를 작성하고 싶다면 */
:global { // :global {} 로 감싸기
  .something {
    font-weight: 800;
    color: aqua;
  }
  // 여기에 다른 클래스를 만들 수도 있겠죠?
}

그리고 CSSModule.js 에서도 상단에 .css 파일 대신 .scss 파일을 불러오세요.

import styles from './CSSModule.module.scss';

이전과 똑같이 보여지고 있나요?

CSS Module 이 아닌 파일에서 CSS Module 사용하기

우리가 CSS Module 에서 글로벌 클래스를 정의 할 때 :global 을 썼었던 것 처럼, CSS Module 이 아닌 일반 .css/.scss 파일에서도 :local 을 통하여 CSS Module 을 사용 할 수도 있습니다.

:local .wrapper {
  /* 스타일 */
}
:local {
  .wrapper {
    /* 스타일 */
  }
}

styled-components

styled-components 는 현존하는 리액트 CSS-in-JS 관련 라이브러리 중에서 가장 잘나가는 라이브러리입니다. CSS-in-JS 는 이름이 그렇듯, 자바스크립트 파일 안에 CSS 를 작성하는 형태입니다. 해외의 큰 기업 - Atlassian, Reddit, coinbase 등에서도 사용되고 있고, 국내에서도 사용하는곳이 꽤 있습니다 - Channel.io, Huiseoul, Tumblebug 등..

styled-components 의 대체제는 현재 대표적으로 emotion 이 있습니다. 작동 방식은 꽤나 비슷합니다.

취향에 따라 갈릴수도 있지만, 분명히 알아두면 좋은 라이브러리인건 확실합니다.

한번 사용해볼까요?

$ yarn add styled-components

이 라이브러리를 통해 한번 예제 컴포넌트를 만들어보겠습니다. 그냥 하나의 자바스크립트 파일안에 스타일까지 작성 할 수 있기 때문에 .css/.scss 파일 같은걸 만들 고민은 안하셔도 된다는게 큰 이점입니다.

한번 예제 컴포넌트 코드를 작성해보세요:

src/StyledComponent.js

import React from 'react';
import styled, { css } from 'styled-components';

const Box = styled.div`
  /* props 로 넣어준 값을 직접 전달해줄 수 있습니다. */
  background: ${props => props.color || 'blue'};
  padding: 1rem;
  display: flex;
`;

const Button = styled.button`
  background: white;
  color: black;
  border-radius: 4px;
  padding: 0.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  font-size: 1rem;
  font-weight: 600;

  /* & 문자를 사용하여 Sass 처럼 자기 자신 선택 가능 */
  &:hover {
    background: rgba(255, 255, 255, 0.9);
  }

  /* 다음 코드는 inverted 값이 true 일 때 특정 스타일을 부여해줍니다. */
  ${props =>
    props.inverted &&
    css`
      background: none;
      border: 2px solid white;
      color: white;
      &:hover {
        background: white;
        color: black;
      }
    `};
  & + button {
    margin-left: 1rem;
  }
`;

const StyledComponent = () => (
  <Box color="black">
    <Button>안녕하세요</Button>
    <Button inverted={true}>테두리만</Button>
  </Box>
);

export default StyledComponent;

Tagged Template Literal

styled-components 에서는 스타일을 입력 할 때 Tagged 템플릿 리터럴(Template Literal) 이라는 ES6 문법을 사용합니다. 이 문법을 사용하는 이유는, `` 를 사용할 때 내부에 JavaScript 객체나 함수가 전달 될 때 이를 따로 추출하기 위함입니다.

예를들어서:

`hello ${{foo: 'bar' }} ${() => 'world'}!`
// 결과: "hello [object Object] () => 'world'!"

위 코드는 [object Object] 이런식으로 문자열로 들어가게되면서 형태를 잃어버리게 되는데요, 만약에 함수를 다음과 같이 만들어서 사용하면 이 템플릿 리터럴 안에 넣어준
값들을 온전히 알아낼 수 있게 됩니다.

function tagged(...args) {
	console.log(args);
}
tagged`hello ${{foo: 'bar' }} ${() => 'world'}!`

이렇게, 사이사이에 들어가는 JavaScript 값의 원본 값을 그대로 추출 할 수 있습니다.

스타일링 된 엘리먼트 만들기

스타일링 된 엘리먼트를 만들 땐, 상단에서 styled 를 불러오고 styled.태그명 을 사용하여 구현합니다:

import styled from 'styled-components';

const MyComponent = styled.div`
  font-size: 2rem;
`;

저 자리에 button 이던, input 이던, 원하는걸 넣으시면 됩니다.

하지만, 만약에 보여줘야 할 태그 형식이 유동적이거나, 아니면 특정 컴포넌트에 스타일링을 해야 하는 상황이라면 다음과 같은 형태로 구현 할 수 있습니다:

// 문자열로 styled 의 인자로 전달
const MyInput = styled('input')`
  background: gray;
`
// 아예 컴포넌트 형식의 값을 넣어줌
const StyledLink = styled(Link)`
  color: blue;
`

스타일에서 props 조회하기

스타일링 한 컴포넌트에 전달하는 props 값을 스타일쪽에서 그대로 사용 하실 수 도 있습니다.

const Box = styled.div`
  /* props 로 넣어준 값을 직접 전달해줄 수 있습니다. */
  background: ${props => props.color || 'blue'};
  padding: 1rem;
  display: flex;
`;

props 에 따른 조건부 스타일링

일반 CSS 클래스를 사용했더라면 주로 클래스이름으로 조건부 스타일링을 해왔었을텐데요, styled-components 에서는 그냥 props 로도 처리 가능합니다. 이렇게 말이죠:

import styled, { css } from 'styled-components';
/* 단순 변수의 형태가 아니라 여러줄의 스타일 구문을 조건부로 설정해야 하는 경우엔
css 를 불러와야합니다. 
*/
const Button = styled.button`
  background: white;
  color: black;
  border-radius: 4px;
  padding: 0.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  font-size: 1rem;
  font-weight: 600;

  /* & 문자를 사용하여 Sass 처럼 자기 자신 선택 가능 */
  &:hover {
    background: rgba(255, 255, 255, 0.9);
  }

  /* 다음 코드는 inverted 값이 true 일 때 특정 스타일을 부여해줍니다. */
  ${props =>
    props.inverted &&
    css`
      background: none;
      border: 2px solid white;
      color: white;
      &:hover {
        background: white;
        color: black;
      }
    `};
  & + button {
    margin-left: 1rem;
  }
`;

한번 App에서 렌더링해볼까요?

src/App.js

import React, { Component } from 'react';
import StyledComponent from './StyledComponent';

class App extends Component {
  render() {
    return <StyledComponent />;
  }
}

export default App;

반응형 디자인

styled-components 에서 반응형 디자인은 어떻게 하는지 알아봅시다. 일단, 일반 CSS 랑 똑같이 하면 되긴 하는데요:

src/StyledComponent.js 의 Box 컴포넌트

const Box = styled.div`
  /* props 로 넣어준 값을 직접 전달해줄 수 있습니다. */
  background: ${props => props.color || 'blue'};
  padding: 1rem;
  display: flex;
  /* 기본적으로는 1024px 에 가운데 정렬을 하고
    가로 크기가 작아짐에 따라 사이즈를 줄이고
    768px 미만으로 되면 꽉 채웁니다 */
  width: 1024px;
  margin: 0 auto;
  @media (max-width: 1024px) {
    width: 768px;
  }
  @media (max-width: 768px) {
    width: 100%;
  }
`;

이런 작업을 함수화하여 훨씬 더 쉽게 할 수도 있답니다:

src/StyledComponent.js 상단부

import React from 'react';
import styled, { css } from 'styled-components';

const sizes = {
  desktop: 1024,
  tablet: 768
};

// 위에있는 size 객체에 따라 자동으로 media 쿼리 함수를 만들어줍니다.
// 참고: https://www.styled-components.com/docs/advanced#media-templates
const media = Object.keys(sizes).reduce((acc, label) => {
  acc[label] = (...args) => css`
    @media (max-width: ${sizes[label] / 16}em) {
      ${css(...args)};
    }
  `;

  return acc;
}, {});

const Box = styled.div`
  /* props 로 넣어준 값을 직접 전달해줄 수 있습니다. */
  background: ${props => props.color || 'blue'};
  padding: 1rem;
  display: flex;
  width: 1024px;
  margin: 0 auto;
  ${media.desktop`width: 768px;`}
  ${media.tablet`width: 768px;`};
`;

어떤가요? 훨씬 간단해졌죠? 지금은 media 를 StyledComponent.js 에서 만들어주긴 했지만, 실제로 사용한다면 아예 다로 다른 파일로 분리시켜서 여기저기서 불러와서 사용하는게 훨씬 편할 것입니다.

기타

styled-components 는 정말 유용한 라이브러리이고 프로덕션 레벨에서도 사용가능한 충분히 성장된 라이브러리입니다.

글로벌 색상 관리를 도와줄 수 있는 Theme 이라는 기능도 있는데, 저는 딱히 엄청 선호하지는 않습니다. 단순히 자주사용되는 색상들을 담기 위한 것 이라면 그냥 객체형태로 저장을 해두고 불러와서 사용하는것이 훨씬 간단하며, 이 기능은 사용자가 서비스의 테마를 직접 변경 할 수도 있는 경우에 사용하면 조금 유용 할 수도 있습니다.

서버 사이드렌더링 도 지원이 되고있습니다.

정리

정말 다양한 리액트 컴포넌트 스타일링 방식을 배워보았습니다. 모두 쓸모있는 기술들이며, 이러한 방식들 중에서 뭘 사용 할 지 선택하는것은 여러분의 몫입니다.

profile
CEO @ Chaf Inc. 사용자들이 좋아하는 프로덕트를 만듭니다.

29개의 댓글

comment-user-thumbnail
2018년 10월 24일

감사합니다! 드디어 utils를 간단하게 임포트 할 수 있게되었네요 :D

답글 달기
comment-user-thumbnail
2018년 10월 26일

항상 잘 보고 있습니다 :) 혹시 sass 부분 CRAv2에서 eject 없이 react-app-rewire만으로 해결하는방법이 있나요?

2개의 답글
comment-user-thumbnail
2018년 11월 2일

git add . git commit -m 'Commit before yarn eject' 을 해도 에러가 나는 경우는 어떻게 해결을 해야할까요..?

1개의 답글
comment-user-thumbnail
2018년 12월 1일

감사합니다. styled components 적응만 되면 좋아 보이네요

답글 달기
comment-user-thumbnail
2018년 12월 12일

좋은 정보 감사합니다.

답글 달기
comment-user-thumbnail
2019년 1월 3일

좋은 글 감사합니다!! 예전부터 궁금한건데 상수 cx 는 무슨 뜻인가요? 정말 궁금해서요;;

1개의 답글
comment-user-thumbnail
2019년 1월 12일

todo-list 도 바뀐 버전으로 한번 포스팅해주시지요.

답글 달기
comment-user-thumbnail
2019년 1월 22일

감사합니다.

답글 달기
comment-user-thumbnail
2019년 3월 7일

감사합니다. 책보다가 막혀서 일러주신 부분 보고 어느정도 해결은 한 것 같습니다만, 사소한 미해결 궁금증이 남아서 https://github.com/velopert/learning-react/issues/96 에 질문 드렸습니다.

답글 달기
comment-user-thumbnail
2019년 5월 16일

좋은 글 감사합니다. ^^
그런데, styled components에는 open-color같은 라이브러리가 없나요?

답글 달기
comment-user-thumbnail
2019년 9월 16일

혹시 styled-components 에서 기존 사스 함수를 쓰려면 어떻게 해야될까요?
단위 변환하는 식이 있어서
const StyledLogo = styled(ReactLogo) height: 25rem; width: 25rem; display: inline-block; margin: auto; .stroke { stroke : ${props => props.stroke} } ;

여기에서 기존엔 height 와 width를
@include function-name(width, 25px); 이런식으로 썼었어요.

답글 달기
comment-user-thumbnail
2019년 10월 14일

많은 도움 되고 있습니다.
한가지 궁금한게 있는데요,
상황에 따라서 쓰고 싶은데 scss와 module.scss를 같이 쓰고 싶은데
scss와 module.scss을 같이 사용하는건 불가능한가요?

웹팩에서 css-loader modules설정을 켜면 className="" 이 안먹고
설정을 끄면 className={}이 작동하지 않습니다.

한가지 더 module.scss로 저장을 안해도 .scss에서 모듈 작동이 되던데 제가 설정을 잘못한걸까요?

답글 달기
comment-user-thumbnail
2019년 12월 19일

안녕하세요. 정말 잘 보고 배우고 있습니다.
잘 보고 따라하다가 sass-loader 설정에서 오류가 생겨서 댓글로 남깁니다.

==변경전==
loader: require.resolve('sass-loader'),
options: {
includePaths: [paths.appSrc + '/styles'],
sourceMap: isEnvProduction && shouldUseSourceMap
}

==변경후==
loader: require.resolve('sass-loader'),
options: {
sassOptions: {
includePaths: [paths.appSrc + '/styles'],
sourceMap: isEnvProduction && shouldUseSourceMap
}
}

options object 에서 includePaths property를 인식하지 못하는 문제 였습니다.
변경 후는 sassOptions object로 넣어 주시면 됩니다.

2개의 답글
comment-user-thumbnail
2020년 1월 22일

'sass-loader 설정 커스터마이징 하기' 에서 data 프로퍼티를 추가하여 특정 코드를 포함시켜주는 부분에 수정이 필요합니다.
현재는 'data'가 아닌 'prependData' 로 입력을 해줘야지 정상 동작합니다.
코드로 보면
options: {
prependData: "@import 'utils';",
sassOptions: { ... },
}

로 입력을 해줘야 제대로 동작 합니다.

1개의 답글
comment-user-thumbnail
2020년 4월 29일

1개의 github 저장소에서 3개의 프로젝트를 작업합니다......

next.js 를 사용한다고 하며 scss는 하나만 뱉는다구 하네요.... @_@;;; 어떡하지....

.pc {
@import './_pc.scss'
}

.mobile {
@import './_mobile.scss'
}

이렇게 해야하나 -ㅠ-

답글 달기
comment-user-thumbnail
2020년 8월 18일

<sass-loader 설정 커스터마이징하기> sass-loader 의 data 설정 부분에서 prependData로 바꿔주어도 현 버전에서는 안되는 것 같습니다.

저는 additionalData로 해서 해결하였습니다.

use: getStyleLoaders({
    importLoaders: 3,
    sourceMap: isEnvProduction && shouldUseSourceMap,
}).concat({
    loader: require.resolve('sass-loader'),
    options: {
        sassOptions: {
            includePaths: [paths.appSrc + '/style'],
            sourceMap: isEnvProduction && shouldUseSourceMap,
        },
        additionalData:"@import 'utils';",
    }
}),
    sideEffects: true,
},
답글 달기
comment-user-thumbnail
2020년 12월 12일

${props =>
props.inverted && // colon expected
css background: none; border: 2px solid white; color: white; &:hover { background: white; color: black; } };
& + button {
margin-left: 1rem; // semi-colon expected
} // at-rule or selector expected
`;
전 왜 여기서 에러가 3개나 뜰까요..ㅠㅠ 도와주세요 ㅠ0ㅠ

답글 달기
comment-user-thumbnail
2020년 12월 28일

좋은 정보 감사합니다. 한가지 질문 드려요
react (js) 로 module (library) 형태로하여 yarn build 후 dist/index.js를 배포합니다.
이때 exsample.css도 선언 되어 있지만 정작 dist/index.js에는 선언된 css가 없습니다.

bundlder({
cwd: process.cwd(),
define,
compress: false,
sourcemap: true,
smartAssetOption: {
url: 'inline',
// maxSize: 14 // Max file size to inline (in kb) default 14kb
},
format: 'cjs',
'css-modules': 'custom__[local]'
이 옵션으로 하게 되는데, 문제가 어디에 있을까요

  • build 시 webpack은 사용하지 않습니다. ("build": "cross-env NODE_ENV=prd node ./script/build.js",)
답글 달기
comment-user-thumbnail
2021년 8월 31일

저에게 꼭 필요했던 정보였어요, 감사합니다 ^^

답글 달기
comment-user-thumbnail
2021년 12월 9일

안녕하세요! sass-loader 커스터 마이징중 오류가 발생했는데 해결 방법을 몰라 여쭙니다 ㅠㅠ
분명 책에 나와있는대로 수정을 진행하고 서버를 재시작했는데
SassError: Can't find stylesheet to import. << 이런 에러메세지와 함께 컴파일 오류가 나네요ㅜㅜ
제가 뭘 잘못하고 있는걸까요??

도움을 주시면 정말 감사하겠습니다.

webpack.config.js 코드

{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders({
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
}).concat({
loader: require.resolve("sass-loader"),
options: {
sassOptions: {
includePaths: [path.appSrc + "/styles"],
},
sourceMap: isEnvProduction && shouldUseSourceMap,
additionalData: @import 'utils';,
},
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},

답글 달기
comment-user-thumbnail
2022년 12월 7일

단순히 자주 사용하는 색상을 저장하는 것이 목적이라면 색상을 오브젝트로 저장하고 불러와서 사용하는 것이 훨씬 쉽습니다. https://geeksnation.net/gmail-sign-up

답글 달기