리액트 컴포넌트 스타일링 – CSS Module / Sass / styled-components


이 자료는 오래되었습니다! 최신화된 자료는 제가 벨로그에 올린

다양한 방식의 컴포넌트 스타일링 방식 CSS, Sass, CSS Module, styled-components  를 읽어주세요.

 

리액트 컴포넌트 스타일링 – CSS Module / Sass / styled-components

리액트에서 컴포넌트 스타일링에 있어서는, 동일화된 방식이 없습니다. 개발자마다, 그리고 회사들마다, 요구하는 스펙이 다르고, 취향도 많이 타기 때문에 정말 다양한 방식으로 컴포넌트를 스타일링을 할 수 있습니다. 가장 기본적인 방식으로는, CSS 파일을 사용하는 방식이지요.

create-react-app 으로 프로젝트를 만들었을때, 이 방식으로 컴포넌트 스타일링을 하는데요, 먼저 한번 다음 예제를 살펴볼까요?

.App {
  text-align: center;
}

.App-logo {
  animation: App-logo-spin infinite 20s linear;
  height: 80px;
}

.App-header {
  background-color: #222;
  height: 150px;
  padding: 20px;
  color: white;
}

.App-intro {
  font-size: large;
}

@keyframes App-logo-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

일단, 위 방식처럼, 일반 CSS 를, 웹팩의 css-loader 를 통하여 불러오는 방식이 있습니다. CSS 를 작성하다보면 발생 할 수 있는 문제점이, 클래스네임이 중복 될 수 있는 가능성인데요. 이를 방지하기 위해 위 코드에서는 각 클래스네임에 컴포넌트 이름을 접두사로 붙여주었습니다. 예: App-header, App-intro 등

접두사를 붙이는 방식외에도, 다음과 같은 방식으로도 해결 할 수 있겠지요.

.App { ... }
.App .header { ... }
.App .logo { ... }
.App .intro { ... }

CSS 를 좀 더 용이하게 작성하기 위해서 우리는 Sass, LESS, Stylus 등의 CSS 전 처리기를 사용하기도 합니다. 만약에 Sass 를 사용한다면 다음과 같이 작성 할 수 있겠죠.

.App {
  .header { ... }
  .logo { ... }
  .intro { ... }
}

이번 포스트에서는, 리액트 프로젝트에서 컴포넌트 스타일링을 할 때 자주 사용되는 방법들을 알아보겠습니다.

  • CSS Module: 모듈화된 CSS 로서, CSS 클래스를 만들면 자동으로 고유적인 클래스네임을 만들어서 스코프를 지역적으로 제한하는 방식입니다.
  • Sass: 자주 사용되는 CSS 전처리기 중 하나이며, 확장된 CSS 문법들을 통하여 CSS 코드를 더욱 용이하게 작성하고, 추가적으로 이를 CSS Module 처럼 사용하는 방법도 알아보겠습니다.
  • styled-components: 요즘 인기있는 컴포넌트 스타일링 방식 중 하나인, JS 코드 내부에서 스타일을 정의하는 방식입니다.

준비하기

우선 create-react-app 을 통하여 리액트 프로젝트를 생성하세요.

$ create-react-app styling-react --scripts-version=1.1.5

 

(이 포스트는 CRA v1 에 맞춰서 작성되었기 때문에 1.1.5 를 사용해야됩니다. 포스트 상단에도 언급되어있지만 최신버전을 쓰려면 제가 새로 작성한 글을 읽어주세요.)

 

그 다음에, 해당 디렉토리로 들어가서 yarn eject 명령어를 실행하세요. 만약에 yarn 이 설치되어있지 않다면, npm run eject 를 실행하세요.

위 명령어를 입력하면, node_modules/react-scripts 경로에 내장되어있는 리액트 프로젝트의 환경설정 파일들이 프로젝트 루트 경로로 이동됩니다.

CSS Module

CSS Module 은, CSS 를 모듈화하여 사용하는 방식입니다. CSS 클래스를 만들면 자동으로 고유적인 클래스네임을 만들어서 스코프를 지역적으로 제한하게 됩니다. 모듈화된 CSS 를 웹팩을 통해서 불러오면, 다음과 같이 사용자가 정의했던 클래스네임과 고유화된 클래스네임이 이뤄진 객체가 반환됩니다.

{
  box: 'src-App__box--mjrNr'
}

그리고 클래스를 적용하게 될 때에는 className={styles.box} 이런식으로 사용을 하게 되죠.

우선, 웹팩 설정으로 가서 CSS Module 을 활성화 해볼까요? 일단, create-react-app 에 이미 css-loader 가 이미 적용이 되어있으니, 이 로더의 옵션만 조금 수정해주면 된답니다.

config/webpack.config.dev.js 를 열어서 css-loader 를 찾아보세요. 그럼 다음과 같은 설정들이 보일거에요.

{
        test: /\.css$/,
        use: [
          require.resolve('style-loader'),
          {
            loader: require.resolve('css-loader'),
            options: {
              importLoaders: 1,
            },
          },
          {
            loader: require.resolve('postcss-loader'),
            options: {
              // Necessary for external CSS imports to work
              // https://github.com/facebookincubator/create-react-app/issues/2677
              ident: 'postcss',
              plugins: () => [
                require('postcss-flexbugs-fixes'),
                autoprefixer({
                  browsers: [
                    '>1%',
                    'last 4 versions',
                    'Firefox ESR',
                    'not ie < 9', // React doesn't support IE8 anyway
                  ],
                  flexbox: 'no-2009',
                }),
              ],
            },
          },
        ],
      },

CSS 를 불러오는 과정에서, 총 3가지의 로더가 사용되었는데요, style-loader 가 스타일들을 불러와서 페이지에서 활성화 해주는 역할을 하고, css-loader 는 css 파일에서 importurl(...) 문들을 webpack 의 require 기능을 통하여 처리해주는 역할을 한답니다. postcss-loader 의 경우에는, 우리가 입력한 CSS 구문이 모든 브라우저에서 제대로 작동할 수 있도록 자동으로 -webkit, -mos, -ms 등의 접두사를 붙여주지요.

여기서, css-loader 의 options에서 CSS Module 을 사용 하도록 설정을 해주면 됩니다. 이를 설정하기 위해서는 두가지의 속성을 설정해야합니다:

       modules: true,
       localIdentName: '[path][name]__[local]--[hash:base64:5]'

첫번째 속성은 CSS Module 을 활성화 하는 속성이고, 두번째 속성 localIdentName 은 CSS Module 에서 고유적으로 생성되는 클래스네임의 형식을 정해줍니다.

위 두 속성을 삽입해주고 나면 설정코드가 다음과 같이 됩니다:

  {
        test: /\.css$/,
        use: [
          require.resolve('style-loader'),
          {
            loader: require.resolve('css-loader'),
            options: {
              importLoaders: 1,
              modules: true,
              localIdentName: '[path][name]__[local]--[hash:base64:5]'
            },
          },
          {
            loader: require.resolve('postcss-loader'),
            options: {
              (...)
            },
          },
        ],
      },

그리고, webpack.config.prod.js 파일에서도 동일하게 설정을 해주세요. webpack.config.dev.js 는 우리가 개발 할 때 사용하는 웹팩 개발 서버 전용 설정이며, webpack.config.prod.js 는 나중에 리액트 프로젝트를 완성하고 배포하게 될 때 빌드하는 과정에서 사용되는 환경설정 파일입니다.

사용해보기

우선, src/App.css 파일에 있는 내용을 비워주고 box 라는 클래스를 만들어보겠습니다.

src/App.css

.box {
  display: inline-block;
  width: 100px;
  height: 100px;
  border: 1px solid black;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

그리고, 이를 App.js 에서 적용을 해보겠습니다.

src/App.js

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

console.log(styles); // 콘솔에 뭐가 프린트되는지 확인해보세요!

class App extends Component {
  render() {
    return (
      <div className={styles.box}>

      </div>
    );
  }
}

export default App;

이제 yarn start 를 해서 웹팩 개발 서버를 실행하세요. 그 다음엔 브라우저에서 페이지에 들어가보세요. 페이지의 정 가운데에 흰 페이지가 보여졌나요? 그리고, 테스트 삼아 styles 가 어떤 객체인지 확인해보고자 console.log 도 했었습니다. 개발자 도구의 콘솔란도 확인해보세요.

개발자도구의 Elements 탭을 열어서 코드를 보면 다음과 같이 되어있습니다

<div data-reactroot="" class="src-App__box--mjrNr"></div>

이렇게, 클래스네임 고유적으로 설정되어있으니, CSS 클래스가 중복되서 충돌이 날 일은 없겠지요?

클래스네임이 여러개일땐?

여러개의 클래스네임이 있을땐 어떻게 할까요? styles.box 도 결국 문자열형태이기 때문에, 사이에 공백을 두고 합쳐주면 됩니다.

만약에 다음과 같이 blue 라는 클래스를 새로 만들어 주었다면,

src/App.js

.box {
  display: inline-block;
  width: 100px;
  height: 100px;
  border: 1px solid black;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.blue {
  background: blue;
}

이를 사용 할 때에는 다음과 같이 사용하면 됩니다:

src/App.js

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

class App extends Component {
  render() {
    return (
      <div className={[styles.box, styles.blue].join(' ')}>

      </div>
    );
  }
}

export default App;

그런데 이것보다 훨씬 더 편하게 하는 방법이 있습니다. 바로, classnames 라는 라이브러리를 사용하는것입니다.
해당 라이브러리를 설치하세요

$ yarn add classnames

classnames 를 설치하고나면 다음과 같이 여러개의 클래스를 적용 할 수 있답니다.

src/App.js

import React, { Component } from 'react';
import classNames from 'classnames';
import styles from './App.css';

class App extends Component {
  render() {
    return (
      <div className={classNames(styles.box, styles.blue)}>

      </div>
    );
  }
}

export default App;

아직까지는, 그렇게 편한지 모르겠죠? classNames 의 bind 기능을 사용하고 나면, 우리가 스타일을 넣을때마다 styles. 를 붙여주는걸 생략 할 수 있답니다.

src/App.js

import React, { Component } from 'react';
import classNames from 'classnames/bind';
import styles from './App.css';
const cx = classNames.bind(styles);


class App extends Component {
  render() {
    return (
      <div className={cx('box', 'blue')}>

      </div>
    );
  }
}

export default App;

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'
classNames(['foo', 'bar']); // => 'foo bar'

// 동시에 여러개의 타입으로 받아올 수 도 있습니다.
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'

// false, null, 0, undefined 는 무시됩니다.
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'

객체형식으로 사용을 한다면, 조건부 스타일링을 하게 될 때 매우 편합니다.
다음 코드에서는, isBlue 값이 true 일 때에만 blue 클래스를 적용합니다.

src/App.js

import React, { Component } from 'react';
import classNames from 'classnames/bind';
import styles from './App.css';
const cx = classNames.bind(styles);


class App extends Component {
  render() {
    const isBlue = true;

    return (
      <div className={cx('box', {
        blue: isBlue
      })}>

      </div>
    );
  }
}

export default App;

파란색 박스가 나타났나요? 이제 isBlue 값을 false 로 설정해보고 다시 브라우저를 열어보세요. 파란색이 사라졌죠? 지금은 isBlue 값을 직접 설정했지만, 이 값을 props 로 받아와서 사용하면, 손쉽게 props 에 따라 다른 스타일을 줄 수 있게 되겠지요?

이제 CSS Module 을 어떻게 사용하는지 잘 알았지요? CSS Module 을 프로젝트에서 사용하게 될때는, 각 컴포넌트마다 스타일 파일을 만들어 불러와서 사용하면 됩니다.

Sass

Sass 를 사용하면, CSS 에서 사용 할 수 있는 문법을 확장하여 중복되는 코드를 줄이고, 더욱 보기 좋게 코드를 작성 할 수 있게됩니다. Sass 에 익숙하지 않다면 https://sass-guidelin.es/ko/ 를 읽어보시면 아주 큰 도움이 될 것 입니다.

이를 사용하기 위해선 두가지 패키지를 설치해야합니다.

$ yarn add node-sass sass-loader

sass-loader 는 webpack 에서 sass 파일들을 읽어오는 역할을 하고, node-sass 는 sass 로 작성된 코드들을 CSS 로 변환해줍니다.

설치를 하고 나면, 웹팩환경설정을 수정해주어야 합니다.

css-loader 설정 부분 하단에 css-loader 설정을 그대로 복사를 하고 test 부분을 .scss 로 변경 후, use 배열의 하단에 sass-loader 를 불러와서 설정하세요.

config/webpack.config.dev.js

      {
         test: /\.css$/,
         (...)
      },
      {
        test: /\.scss$/,
        use: [
          require.resolve('style-loader'),
          {
            loader: require.resolve('css-loader'),
            options: {
              importLoaders: 1,
              modules: true,
              localIdentName: '[name]__[local]__[hash:base64:5]'
            },
          },
          {
            loader: require.resolve('postcss-loader'),
            options: {
              // Necessary for external CSS imports to work
              // https://github.com/facebookincubator/create-react-app/issues/2677
              ident: 'postcss',
              plugins: () => [
                require('postcss-flexbugs-fixes'),
                autoprefixer({
                  browsers: [
                    '>1%',
                    'last 4 versions',
                    'Firefox ESR',
                    'not ie < 9', // React doesn't support IE8 anyway
                  ],
                  flexbox: 'no-2009',
                }),
              ],
            },
          },
          {
            loader: require.resolve('sass-loader'),
            options: {
              // 나중에 입력
            }
          }
        ],
      },

위 코드처럼 sass-loader 설정을 postcss-loader 하단에 위치시키면 됩니다. options 의 경우엔 이번엔 비워두고, 나중에 입력을 하도록 하겠습니다.

webpack.config.prod.js 파일에서도 file-loader 에서 scss 를 제외시키고, css-loader 부분 코드를 동일하게 복사 후 수정하세요

config/webpack.config.prod.js

      {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract(
          Object.assign(
            {
              fallback: require.resolve('style-loader'),
              use: [
                {
                  loader: require.resolve('css-loader'),
                  options: {
                    importLoaders: 1,
                    minimize: true,
                    sourceMap: true,
                    modules: true,
                    localIdentName: '[name]__[local]__[hash:base64:5]'
                  },
                },
                {
                  loader: require.resolve('postcss-loader'),
                  options: {
                    // Necessary for external CSS imports to work
                    // https://github.com/facebookincubator/create-react-app/issues/2677
                    ident: 'postcss',
                    plugins: () => [
                      require('postcss-flexbugs-fixes'),
                      autoprefixer({
                        browsers: [
                          '>1%',
                          'last 4 versions',
                          'Firefox ESR',
                          'not ie < 9', // React doesn't support IE8 anyway
                        ],
                        flexbox: 'no-2009',
                      }),
                    ],
                  },
                },
                {
                  loader: require.resolve('sass-loader'),
                  options: {
                     // 나중에 입력
                  }
                }
              ],
            },
            extractTextPluginOptions
          )
        ),
        // Note: this won't work without `new ExtractTextPlugin()` in `plugins`.
      },

현재 우리는, CSS Module 을 적용했었던 css-loader 설정을 복사해서 사용했기 때문에, Sass 를 사용 할 때에도 CSS Module 이 적용되어있습니다. 만약에 CSS Module 을 사용하지 않고자 한다면, modules 값과 localIdentName 을 지워주어야 합니다.

이제 웹팩 개발서버를 종료하고 재시작해보세요.

이제 프로젝트에 Sass 가 적용되었습니다. 한번 Sass 가 제공하는 멋진것들 몇가지를 사용해볼까요?

현재 선택자 참조 (&)

특정 클래스에 마우스가 올라갔을때, 그리고 클릭했을때 스타일을 주기 위해서는 CSS 를 사용한다면 다음과 같이 작성하게됩니다.

.box:hover {
  background: red;
}

.box:active {
  background: yellow;
}

Sass 에서 사용하는 현재 선택자 참조 기능을 사용한다면 다음과 같이 작성할수 있게 되지요.

.box {
  /* 스타일 설정.. */
  &:hover {
    background: red;
  }
  &:active {
    background: yellow;
  }
}

이런식으로 감싸진 구조로 CSS 를 작성 할 수 있기 때문에 가독성이 훨씬 높아집니다. 그럼 한번 바로 적용을 해볼까요? 우선 기존의 App.css 파일을 App.scss 로 파일 이름을 변경하세요.

그 다음엔, App.js 에서 import 할 때 사용하는 파일 이름도 App.scss 로 변경하세요.

그리고 App.scss 파일을 다음과 같이 수정하세요.

.box {
  display: inline-block;
  width: 100px;
  height: 100px;
  border: 1px solid black;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);

  &.blue {
    background: blue;
  }

  &:hover {
    background: yellow;
  }

  &:active {
    background: red;
  }
}

.blue 클래스도 현재 선택자 참조 기능을 사용하여 .box 안에 넣어주었습니다.

네스트된 구조

Sass 를 사용하면 네스트된 구조, 즉 감싸진 구조로 코드를 입력 할 수 있습니다. 우선 App.js 코드를 다음과 같이 수정해보세요.

src/App.js – render 메소드

  render() {
    const isBlue = true;

    return (
      <div className={cx('box', {
        blue: isBlue
      })}>
        <div className={cx('box-inside')}/>
      </div>
    );
  }

box 내부에 box-inside 라는 클래스를 가진 div 엘리먼트를 만들어주었습니다.

이제 .box-inside 라는 클래스를 만들건데요, 만약에 이 클래스가 box 클래스 내부에 있을때만 작동을 하게끔 하고 싶다면 CSS 로는 다음과 같이 작성합니다.

.box .box-inside {
    /* ... */
}

Sass 를 사용하면, 다음과 같이 작성 할 수 있게 됩니다.

.box {
  .box-inside {
      /* ... */
  }
}

그럼 한번, 우리의 App.scss 에서 위 문법을 사용해서 box-inside 클래스를 스타일링 해보겠습니다.

src/App.scss

.box {
  display: inline-block;
  width: 100px;
  height: 100px;
  border: 1px solid black;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);

  &.blue {
    background: blue;
  }

  &:hover {
    background: yellow;
  }

  &:active {
    background: red;
  }

  .box-inside {
    background: black;
    width: 50px;
    height: 50px;
  }
}

어떤가요? 이렇게 하니까 편하기도 하고, App 컴포넌트의 코드를 함께 읽지 않아도 .box-inside 클래스가 .box 엘리먼트 내부에 있다는것을 알 수 있으니 가독성도 높아지는것같지 않나요?

변수 사용하기

Sass 에서는 자주 사용되는 값은 변수에 넣어서 사용 할 수 있습니다. 한번 .box 의 width 와 height 을 설정된 100px 를 $size 라는 변수 안에 넣어서 사용해보겠습니다.

$size: 100px;

.box {
  display: inline-block;
  width: $size;
  height: $size;
  (...)
}

믹스인 사용하기

자주 사용 되는 값은 변수에 넣어서 사용하고, 자주 사용되는 구문들은 믹스인이라는것을 만들어서 재사용 할 수 있습니다. 한번 가운데에 위치시키는 CSS 구문들을 place-at-center 라는 믹스인에 만들어서 호출해보겠습니다.

$size: 100px;

@mixin place-at-center() {
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.box {
  display: inline-block;
  width: $size;
  height: $size;
  border: 1px solid black;
  position: fixed;

  @include place-at-center();

  (...)
}

전역적으로 사용하기

변수와 믹스인은, 여러곳에서 재사용하자고 만드는것인데, 현재 우리는 CSS Module 이 적용되어있는 상태이기 때문에, 믹스인과 변수들이 파일마다 공유되고있지 않습니다. 따라서, 우리는 이를 전역적으로 사용 할 수 있는 방법을 고안해야하는데요, 그 방법중 하나는, 스타일 디렉토리를 만들어서 전역적으로 사용되는 코드를 따로 구분시킨다음에 스타일에서 불러와서 사용하는 방법입니다.

src 디렉토리에 styles 라는 디렉토리를 만들고, utils.scss 라는 파일을 만드세요.

이 파일에 공용으로 사용할 코드를 잘라내서 넣으세요.

src/styles/utils.scss

$size: 100px;

@mixin place-at-center() {
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

이렇게 파일을 만들고 나면, App.scss 에서 다음과 같이 불러와서 사용 할 수 있답니다.

src/App.scss

@import './styles/utils';

.box {
  (...)

그런데, 앞으로 컴포넌트를 저장하는 디렉토리가 조금 깊어진다면, 저 파일을 불러와야 할 때마다 상위 디렉토리로 가야되서 이런 코드를 작성하게 될 지도 모릅니다.

scss

@import '../../../styles/utils'

조금 비효율적이지요? 헷갈리기도 하구요. 이런 복잡한 상대 경로를 작성하는걸 피하기 위해서, webpack 에서 sass-loader를 설정 할 때 includePaths 라는 설정을 통하여 경로를 간소화 할 수 있습니다.

이를 설정하기 위해선, 먼저 config/paths.js 파일을 수정해주어야 합니다.

config/paths.js – 하단부
// config after eject: we're in ./config/
module.exports = {
  (...),
  styles: resolveApp('src/styles')
};

해당 파일의 하단부를 보면 경로들이 들어있는 객체를 내보내는 코드가 있습니다. 이 부분에 styles 를 추가하세요.

그 다음엔, webpack.config.dev.js 와, webpack.config.prod.js 파일의 sass-loader 설정 부분을 다음과 같이 수정하세요.

config/webpack.config.dev.js, webpack.config.prod.js – sass-loader 설정 부분

          {
            loader: require.resolve('sass-loader'),
            options: {
              includePaths: [paths.styles]
            }
          }

이렇게 설정을 하고 나서 웹팩 개발서버를 재시작하면 새로운 설정이 적용됩니다. 위 설정을 하고 나면 다음과 같은 방식으로 util.scss 파일을 불러와서 사용 할 수 있게 됩니다.

src/App.scss

@import 'utils';

.box {
  (...)

어떤가요? 훨씬 더 쉽게 불러올수있겠죠?

Sass 라이브러리

Sass 를 사용하면 다양한 스타일 관련 라이브러리를 사용 할 수 있게 됩니다. 이번 Sass 공부 섹션을 마치기 전에, 반응형 디자인을 쉽게 할 수 있게 해주는 include-media 와, 손쉽게 색상을 골라서 사용 할 수 있는 색상 팔레트 open-color 라이브러리를 사용하여 멋진 버튼을 만들어보도록 하겠습니다.

우선 위 라이브러리를 npm 을 통해 설치하세요.

$ yarn add include-media open-color

설치를 하고 나면, utils.scss 파일쪽에서 불러와야 합니다.

src/styles/utils.scss

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

$breakpoints: (
  small: 376px, 
  medium: 768px, 
  large: 1024px, 
  huge: 1200px
);


$size: 100px;

@mixin place-at-center() {
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

npm 혹은 yarn 으로 설치한 패키지 내부에 있는 파일을 불러올때에는 ~ 문자를 통해서 node_modules 디렉토리에 접근 할수 있습니다.

그리고, $breakpoints 라는 변수를 설정해주었는데요, 여기에 있는 값은 추후 반응형 디자인 코드를 작성하게 될 때 사용하게 됩니다.

버튼 만들기

방금 설치한 라이브러리를 활용하여 멋진 버튼을 만들어봅시다.

버튼 컴포넌트는 src/components/Button/ 디렉토리에 저장을 하겠습니다. 해당 디렉토리에 다음 파일들을 만드세요.

  • Button.js
  • Button.scss
  • index.js

우선 컴포넌트 파일부터 작성을 해봅시다.

src/components/Button/Button.js

import React from 'react';
import styles from './Button.scss';
import classNames from 'classnames/bind';

const cx = classNames.bind(styles);

const Button = ({children, ...rest}) => {
  return (
    <div className={cx('button')} {...rest}>
      {children}
    </div>
  );
};

export default Button;

CSS Module 형식으로 스타일을 설정해주었구요, 함수형 컴포넌트로 만들어주었습니다. props 에서는 children 과 …rest 가 있는데요, 여기서 rest 는 나중에 이 컴포넌트가 받을 모든 props 를 칭합니다. 비구조화 할당 문법에서 ...foo 이런식으로 입력을 하면, 비구조화 할당을 할 때에 따로 지정되지 않은것들은 foo 에 객체형태로 담겨지게 됩니다.

예제

const object = {
  a:1,
  b:2,
  c:3
};

const { a, ...foo } = object;

console.log(a); // 1
console.log(foo); // { b: 2, c: 3 }

그리고, 컴포넌트를 렌더링 하는 부분에서 { …rest } 를 넣어주었는데요, 이는 객체 안에 있는 모든 값들을 props 로 지정한다는 의미입니다. 예를들어서 rest 객체 안에 onClick 이 들어있다면, <div onClick={onClick}> 이런식으로 설정이 됩니다.

이제 스타일을 작성해봅시다.

src/components/Button/Button.scss

@import 'utils';

.button {
  background: $oc-green-7;
  transition: all .2s ease-in;
  display: inline-block;
  padding-top: 2rem;
  padding-bottom: 2rem;
  text-align: center;
  color: white;
  position: fixed;
  font-size: 2rem;
  font-weight: 500;
  border-radius: 4px;
  cursor: pointer;

  @include place-at-center();

  width: 1200px;

  // 반응형 디자인
  @include media("<huge") {
    width: 1024px;
  }

  @include media("<large") {
    width: 768px;
  }

  @include media("<medium") {
    width: 90%;
  }

  // 마우스 상태에 따라 다른 효과 지정
  &:hover {
    background: $oc-green-6;
  }
  &:active {
    margin-top: 3px;
    background: $oc-green-8;
  }
}

open-color 를 적용하고나면 변수를 사용하여 여러가지 색깔을 쉽게 사용 할 수 있습니다. 변수의 형식은 $oc-색상이름-밝기 입니다. 색상의 종류는 https://yeun.github.io/open-color/ 에서 참고 할수 있습니다.

예를 들어, 초록색을 사용한다면 $oc-green-6 이런식으로 사용하면 되겠습니다.

그리고 include-media 의 경우엔 믹스인으로 구성된 라이브러리인데요, 사용을 할 때는 이전에 설정했던 breakpoints 에서 지정한 값들을 참조하여 @include media('<huge') { ... } 와 같은 형식으로 작성하면 됩니다.

위 코드에서는, 기본 넓이를 1200px 로 설정하고, 창의 크기가 1200px 미만으로 되면 1024px 로 설정하고, 창의 크기가 그것보다 작아지면 768px, 그리고 그 다음에는 90% 의 크기로 설정하도록 했습니다.

이제 컴포넌트 관련 코드를 다 작성했는데요, Button 디렉토리에 index.js 를 마지막으로 만들어주겠습니다. 우리는, 컴포넌트 자바스크립트 파일과 스타일 파일을 보기 쉽게 정리하기 위해서 Button 디렉토리를 따로 만들어주었는데요, 만약에 이대로 한다면 나중에 컴포넌트 파일을 불러오게 될 때에 './components/Button/Button' 형식으로 불러와야 합니다. Button 이 두번이나 중복되는 것은 보기에 깔끔하지 않으므로, index.js 파일을 만들어서, 컴포넌트를 불러온뒤 바로 내보내주겠습니다.

src/components/Button/index.js

import Button from './Button';
export default Button;

위 코드를 한 줄로 작성하면 다음과 같습니다:

export { default } from './Button';

여러분이 편한 문법을 사용하시면 됩니다.

자, 이제 이 컴포넌트를 App 에서 랜더링해볼까요?

src/App.js

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

class App extends Component {
  render() {
    return (
      <div>
        <Button>버튼</Button>
      </div>
    )
  }
}

export default App;

모든 브라우저 사이즈에서 짤리지 않는 컴포넌트를 만들어주었습니다!

styled-components

styled-components 는 자바스크립트 파일안에 스타일 자체를 선언하는 방식으로 스타일을 관리합니다. 이 라이브러리에서 제공되는 기능은 다양한데요, 이번에는 직접 사용해보면서 맛보기로만 익혀보고, 다음에 더욱 더 자세히 알아보도록 하겠습니다.

먼저 패키지를 설치하세요.

$ yarn add styled-components

그리고, 또 다른 버튼 컴포넌트를 만들어보겠습니다. components 디렉토리에 StyledButton.js 라는 파일을 만드세요.

src/components/StyledButton.js

import React from 'react';
import styled from 'styled-components';

const Wrapper = styled.div`
  border: 1px solid black;
  display: inline-block;
  padding: 1rem;
  border-radius: 3px;
  &:hover {
    background: black;
    color: white;
  }
`;

const StyledButton = ({children, ...rest}) => {
  return (
    <Wrapper {...rest}>
      {children}
    </Wrapper>
  );
};

export default StyledButton;

styled-components 를 불러온 다음에, styled 를 사용하여 Wrapper 라는 값에 스타일링된 div 엘리먼트를 만들어주었습니다. 이 과정에서 익숙하지 않은 문법이 사용됐지요?

styled`...`

위 문법은 ES6 의 Tagged Template Literals 라는 문법입니다.

한번, 크롬 개발자 코드를 열어서 다음 코드를 입력해보세요.

function myFunction(...args) {
   console.log(args);
}
myFunction`1+1 = ${1+1} and 2+2 = ${2+2}!`

그러면, 다음과 같은 결과물이 나타납니다.

[
  [
    "1+1 = ",
    " and 2+2 = ",
    "!"
  ],
  2,
  4
]

backquote (`) 사이에 ${자바스크립트표현} 을 사용하면 위와 같이 끊어서 함수의 인자로 전달해준답니다.

이 문법이 사용되는 이유는, 스타일링을 할 때, props 에 접근하기 위해서 다음과 같이 함수를 내부에 입력하기도 하는데요:

import React from 'react';
import styled from 'styled-components';

const Wrapper = styled.div`
  border: 1px solid black;
  display: inline-block;
  padding: 1rem;
  border-radius: 3px;
  font-size: ${(props)=>props.fontSize};
  &:hover {
    background: black;
    color: white;
  }
`;

const StyledButton = ({children, ...rest}) => {
  return (
    <Wrapper fontSize="1.25rem" {...rest}>
      {children}
    </Wrapper>
  );
};

export default StyledButton;

만약에 Tagged Template Literal 기능을 사용하지 않으면 다음과 같이 함수가 문자열 자체로 들어가게 되지만,

이 문법을 사용하면 자바스크립트 표현은 끊어서 처리하기 때문에, 함수가 함수 형태 그대로 남아지게 됩니다. 따라서, styled-components 라이브러리가 끊어진 값들을 참조해서 스타일을 정의 해 줄 수 있겠지요.

이제 이번에 만든 컴포넌트를 App 에서 렌더링 해보겠습니다.

src/App.js

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

class App extends Component {
  render() {
    return (
      <div>
        <StyledButton>버튼</StyledButton>
      </div>
    )
  }
}

export default App;

styled-components 의 최대 장점은, 자바스크립트 내부에서 스타일을 정의하다보니, 자바스크립트와 스타일링 사이의 벽이 허물어져서 동적 스타일링이 더욱 편해진다는 것 입니다.

한번 big 이라는 props 통해 버튼 크기를 동적으로 변경해보는 코드를 작성해보겠습니다.

src/components/StyledButton.js

import React from 'react';
import styled from 'styled-components';

const Wrapper = styled.div`
  border: 1px solid black;
  display: inline-block;
  padding: 1rem;
  border-radius: 3px;
  font-size: ${(props)=>props.fontSize};
  ${props => props.big && `
    font-size: 2rem;
    padding: 2rem;
  `}
  &:hover {
    background: black;
    color: white;
  }
`;

const StyledButton = ({children, big, ...rest}) => {
  return (
    <Wrapper fontSize="1.25rem" {...rest} big={big}>
      {children}
    </Wrapper>
  );
};

export default StyledButton;

위 코드에서

  ${props => props.big && `
    font-size: 2rem;
    padding: 2rem;
  `}

이 부분이, props 를 받아와서 이에따라 스타일을 설정하는 부분입니다.
그리고 props 를 받아오는 부분에서 big 이란 값을 불러와서 Wrapper 안에 그대로 넣어주었습니다.

자 이제, App 에서 big props 를 설정해줘봅시다.

src/App.js

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

class App extends Component {
  render() {
    return (
      <div>
        <StyledButton big>버튼</StyledButton>
      </div>
    )
  }
}

export default App;

여기서 <StyledButton big>, 이렇게 입력을 해주었는데요 이 표현은 <StyledButton big={true}> 와 동일한 표현입니다.

이번 포스트에서는 styled-components 는 여기까지만 알아보도록 하겠습니다. 이 포스트에서 제시된 기능 외에도 많은 기능을 제공해 주는데요, 예를들어 우리가 Sass 에서 사용한 include-media 믹스인처럼, 함수를 따로 만들어서 사용할수도있습니다. 또한, 우리가 Sass 에서 네스트된 구조로 스타일을 작성하는 것 처럼, styled-components 에서도 동일하게 네스트된 구조로 스타일을 작성 할수 있답니다. 이에 대해선 다음번에 다룰 강좌에서 더욱 더 자세히 알아보도록 하겠습니다.

정리

이번 포스트에서는, 다양한 방법으로 컴포넌트를 스타일링 하는 방법을 배웠습니다. 이 포스트에서 제시된 방법중에서, 어떤것이 가장 좋은지에 대한 해답은 없습니다. 일반 CSS 를 사용한다면, 이미 CSS 가 익숙한 개발자들에겐 매우 친숙할것이고, CSS Module 을 사용하면 스코프 이슈가 해결 될 것이며, 여기서 Sass 를 사용하면 더욱 확장된 CSS 문법들을 통하여 깔끔하고 편안한 코드를 작성 할 수 있게 됩니다. Sass 가 아니더라도, LESS, Stylus 등을 사용 할 수도 있겠지요.

styled-components 의 경우엔 자바스크립트와 CSS 의 벽을 허물어주고, JS 파일 자체에 스타일 코드를 입력하기 때문에 여러 파일을 만들 필요도 없고 동적 렌더링을 하게 될때에는 매우 효과적입니다. 하지만, 단점으로는 기존 CSS 파일을 통한 스타일링과 사뭇 다른 방식으로 관리를 하기 때문에, HTML/CSS 를 따로 작성하는 디자이너가 따로 있다면, 이를 일일히 포팅해야 되기 때문에 조금 힘들어질 수도 있습니다.

어떤 방법을 택할지는, 여러 방식으로 작업을 해보고 여러분에게 가장 편안한 방법으로 하는것이 가장 좋습니다.

이 다음에는 리액트 컴포넌트 스타일링, API 연동 실습 – NASA 에서 오늘의 우주 사진 가져오기 튜토리얼을 추천드립니다!

  • Jung Han

    감사합니다~

  • chun yum

    와우! 궁금했던 부분인데 감사합니다!!다음편도 빨리보고싶어요~!!

  • 류한경

    민준님 너무나 감사합니다. 늘 많이 배워요! 저도 언젠가는 제가 아는 걸 공유할 수 있을 정도로 고수가 되었으면 좋겠네요.

  • 정말 감사합니다. 알고 싶었던 정보였습니다. 감사드립니다.

  • Hyunsung Kim

    와…. 여기서 항상 눈팅하면서 리액트를 살짝살짝 공부하던 중인데 엄청 궁금했던 분야를 한방에 해결해주시네요 ㅋㅋㅋ 정말 감사합니다~!!!

  • 김세영

    npm install을 해도 되는데 yarn을 사용하는 이유가 따로 있나요?

  • 이상원

    매번 강의 잘보고있습니다. Sass 에 대해 질문이 있습니다.
    scss 파일의 위치가 src 밖에 있는 경우에는 import 할수없는데 src안에 존재해야하는건가요?
    만약에 src 밖에 존재한다면 어떻게 사용할 수 있을까요?

  • Seungwoo Lee

    npm run yarn 했는데도 yarn command not fount 나오네요

    • An

      yarn도 npm처럼 패키지를 관리하는 툴입니다. npm install –save [dependency_name] 혹은 yarn add [dependency_name] 명령어를 쓰시면 됩니다.

  • Seungwoo Lee

    config/webpack.config.dev.js 파일에 css-loader 라는단어가 전혀 없는데, 버전이 달라서그런건가요 ? ;ㅠㅠ

  • An

    안녕하세요. 강좌 잘 보고 있습니다. Sass를 사용하는 방법을 테스트하고 있었는데 test: /.scss$/, 이걸 빼먹어서 모듈 로딩이 안됐었네요. test가 파싱할 확장자들을 테스트한다는 뜻인가요?

  • JOOHYUNG LEE

    정말 감사합니다. 항상 이 사이트를 보면서 react에 대해 많이 공부하게 되네요

  • 주홍철

    어디에다 질문을 할지 몰라서욤.. 벨로퍼트님의 bitmulate 코드를 보고 있는데요.
    let parseJSON = str =>{
    let parsed = null;
    try {
    parsed = JSON.parse(str);
    } catch(e){
    return null;
    }
    return parsed;
    }
    let test1 = parseJSON({ “name”:”John”, “age”:30, “city”:”New York”});
    console.log(test1)
    parseJSON부분이요.. 이거하면 obj가 리턴되야 되는데 자꾸 null이 리턴되는데 왜그럴까요?

    • Object String을 넘겨야하는데, Object 자체를 넘기셔서 그렇습니다.

      let test1 = parseJSON(‘{ “name”:”John”, “age”:30, “city”:”New York”}’);

      이렇게 해주셔야합니다.

  • Jimmy Kim (jimmy)

    고맙습니다!!

  • Sung Junyoung

    감사합니다 🙂

  • Web Dev

    짱!

  • 리희찬

    감사합니다 ~~ 😀

  • 방랑자

    훌륭한 강의 감사드립니다.

    수정해야할 부분이 있는것 같아 댓글로 말씀드립니다.

    const StyledButton = ({children, big}, …aa) => {
    return (

    {children}

    );
    };

    위의 StyledButton 처음에 컴파일하면 에러나는데요 위와 같이 코드 변경해주시면 에러 사라집니다.

  • limstrong85

    좋은 정보 감사합니다^^ㅎㅎ
    강좌대로 적용을 했는데, 에러가 나서요;;
    운영체제가 window7인데 아무리해도 안 되네요ㅜㅜ;;
    혹시 맥 기준으로 적용 되는 건지 해서요~

  • sj

    안녕하세요. 이번에 책도 구매해서 강의 잘 보고있습니다 🙂
    리액트 처음 접근하기 어려웠는데 덕분에 빠른속도로 습득중입니다
    근데 하다가 조금 막히는 부분이 있어서..
    절대경로를 상대경로로 설정하는 부분에서 paths.js 설정과 config.dev.js config.prod.js에 설정을 모두 하였는데 상대경로를 찾지 못하여서 혹시 제가 뭔가를 잘 못했는데 잘 모르겠네요 ㅠㅠ 오류검증은 제가해야될 부분이고 혹시 저셋팅을 하면 원래 되야되는거죠??

    • sj

      자체해결하여 답변 공유드립니다.
      절대경로 설정하는 부분에서 셋팅은 저대로하면 되지만,
      vscode를 사용하실 경우 [tsconfig.json] 파일을 생성하여 baseUrl,paths 활성화 하여 path 잡아주셔야합니다 모두 참고하세요^^

  • kyungmin

    버튼 생성에서
    const cx = classNames.bind(styles);
    const Button = ({children, …rest})=>{
    return(

    {children}

    );
    };
    이 있는데 children과 rest에 어떠한 값이 들어가서 저러한 결과가 나오는지 궁금합니다. 자세히 알고싶어요.(책으로 공부하다가 이해가 잘안되네요.)

    • 상욱 이

      저도 여기서 children에 대한 설명이 없어서 좀 헤멨네요. 제가 생각했을 땐, 태그 사이에 들어가는 값이 children으로 들어가는 것 같긴한데 확실하게 무엇인지 궁금합니다.

  • john

    yarn eject 안되시는분 있나요? yarn eject를하면 Remove untracked files, stash or commit any changes, and try again.
    error Command failed with exit code 1.라는 에러가 뜨는데 어떻게하면 좋을까요? config folder가없어서 sass를 못쓰고있습니다.

    • JunHyeok Ha

      git add .
      git commit -m “abc”
      하신 뒤에 하시면 될 듯 합니다.

  • WLion

    안녕하세요. 현재 책보면서 수업 듣고 있습니다 .
    현재 open-color를 적용하는 부분에서 적용이 안되서 이렇게 글을 올립니다.
    style에 utils.scss에 @import ‘~open-color/open-color.scss’ 하니
    밑줄 그이면서 Unable to open ‘open-color.scss’: File not found (file:///c:/project/todo-list/src/styles/~open-color/open-color.scss). 라고 뜨네요.
    open-color도 적용이 안되구요. 뭐가 문제일까요?

    • Github 이슈에도 남겨주셨는데
      현재 프로젝트를 압축해서 저한테 보내주시면 확인해보겠습니다 ^^