리액트 - 컴포넌트 스타일링

호박이와 칼림바·2023년 10월 4일
0

React

목록 보기
10/16

📗목차

스타일링 방식

  • 일반 CSS:
    컴포넌트를 스타일링하는 가장 기본적인 방식이다.

  • Sass:
    자주 사용되는 CSS 전처리기(pre-processor) 중 하나로 확장된 CSS 문법을 사용하여 CSS 코드를 더욱 쉽게 작성할 수 있도록 해준다.

  • CSS Module:
    스타일을 작성할 때 CSS 클래스가 다른 CSS 클래스의 이름과 절대 충돌하지 않도록 파일마다 고유한 이름을 자동으로 생성해 주는 옵션이다.

  • styled-components:
    스타일을 자바스크립트 파일에 내장시키는 방식으로 스타일을 작성함과 동시에 해당 스타일이 적용된 컴포넌트를 만들 수 있게 해준다.

가장 흔한 방식, 일반 CSS

CSS를 작성할 때 가장 중요한 점은 CSS 클래스를 중복되지 않게 만드는 것이다. CSS 클래스가 중복되는 것을 방지하는 여러 가지 방식이 있는데, 그 중 하나는 이름을 자을 때 특별한 규칙을 사용하여 짓는 것이고, 또 다른 하나는 CSS Selector를 활용하는 것이다.

1. 이름 짓는 규칙

리액트 프로젝트를 생성하면 자동 생성된 App.css를 읽어 보면 클래스 이름이 컴포넌트-이름-클래스 형태로 지어져 있다(예: App-header). 클래스 이름에 컴포넌트를 이름을 포함시킴으로써 다른 컴포넌트에서 실수로 중복되는 클래스를 만들어 사용하는 것을 방지할 수 있다. 비슷한 방식으로 BEM 네이밍이라는 방식도 있다. BEM 네이밍은 CSS 방법론 중 하나로, 이름을 지을 때 일종의 규칙을 준수하여 해당 클래스가 어디에서 어떤 용도로 사용되는지 명확하게 작성하는 방식이다. 예를 들어 .card_title_primary처럼 말이다.

2. CSS Selector

CSS Selector를 사용하면 CSS 클래스가 특정 클래스 내부에 있는 경우에만 스타일을 적용할 수 있다. 예를 들어 .App 안에 들어 있는 .logo에 스타일을 적용하고 싶다면 다음과 같이 작성하면 된다.

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

Sass 사용하기

Sass(Syntactically Awesome Style Sheets)는 CSS 전처리기로 복잡한 작업을 쉽게 할 수 있도록 해주고, 스타일 코드의 재활용성을 높여 줄 뿐만 아니라 코드의 가독성을 높여 유지 보수를 더욱 쉽게 해준다.

create-react-app 구버전에서는 Sass를 사용하려면 추가 작업이 필요했는데, v2 버전부터는 별도의 추가 설정 없이 바로 사용할 수 있다.

Sass에서는 두 가지 확장자 .scss와 .sass를 지원한다. Sass가 처음 나왔을 때는 .sass 확장자만 지원되었으나 나중에 개발자들의 요청에 의해 .scss 확장자도 지원하게 되었다.

.scss의 문법과 .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;
}

주요 차이점을 살펴보면, sass 확장자는 중괄호({})와 세미콜론(;)을 사용하지 않는다.
반면 .scss 확장자는 기존 CSS를 작성하는 방식과 비교해서 문법이 크게 다르지 않다. 보통 .scss 문법이 더 자주 사용되므로 이 책에서는 .scss 확장자를 사용하여 스타일을 작성해보겠다.

새 컴포넌트를 만들어서 Sass를 한번 사용해보자. 우선 node-sass라는 라이브러리를 설치해줘야 한다. 이 라이브러리는 Sass를 CSS로 변환해준다.

다음 명령어를 입력하자. npm i node-sass
하지만, 설치 에러가 떴다. node.js 버전이 node-sass 버전이랑 호환이 안 돼서 생기는 문제라는대.

node-sass는 nodejs 버전에 영향을 받습니다.
그리고 node-sass 대신 sass 패키지 사용을 권장합니다.
sass는 node-sass를 흡수한 최신 버전의 패키지입니다.
npm i -D sass 로 설치하시면 됩니다.
node-sass 설치 오류 참조

구글링 결과 다음 명령어를 입력하면 되었다.
npm i -D sass

설치가 완료되면 다음과 같이 SassComponent.scss 파일을 작성해보자

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

.SassComponent {
  display: flex;
  .box { // 일반 CSS에서는 .SassComponent .box와 마찬가지
    background: red;
    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 스타일시트를 사용하는 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 컴포넌트에서 보여주자.

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

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

export default App;

작업한 뒤에는 개발 서버를 재시작시켜야 Sass가 성공적으로 적용된다.
개발 서버가 구동 중인 터미널 창에서 Ctrl+C를 누르고, 다시 npm start 명령어를 입력하자. 그러고 나면 아래와 같은 결과가 나타난다.


1. utils 함수 분리하기

여러 파일에서 사용될 수 있는 Sass 변수 및 믹스인은 다른 파일로 따로 분리하여 작성한 뒤 필요한 곳에서 쉽게 불러와 사용할 수 있다.

src 디렉터리에 styles 라는 디렉터리를 생성하고, 그 안에 utils.scss 파일을 만들어 보라.
그 다음에는 기존 SassComponent.scss에 작성했던 변수와 믹스인을 잘라내서 이동시켜 보자.

src/styles/utils.scss

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

다른 scss 파일을 불러올 때는 @import 구문을 사용한다.

SassComponent.scss

@import './styles/utils';

.SassComponent {
  display: flex;
  .box { // 일반 CSS에서는 .SassComponent .box와 마찬가지
    background: red;
    cursor: pointer;
    transition: all 0.3s ease-in;
    (...)
  }
}

utils.scss 파일을 분리하기 전의 결과와 같은 결과가 나타나는지 확인해보자.

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

이 작업은 Sass를 사용할 때 반드시 해야 하는 것은 아니지만, 해두면 유용하다. 예를 들어 방금 SassComponent에서 utils를 불러올 때 @import './styles/utils'; 형태로 불러왔는데, 만약 프로젝트에 디렉터리를 많이 만들어서 구조가 깊어졌다면 해당 파일에서 다음과 같이 상위 폴더로 한참 거슬러 올라가야 한다는 단점이 있다.

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

이 문제점은 웹팩에서 Sass를 처리하는 sass-loader의 설정을 커스터마이징하여 해결할 수 있다. create-react-app으로 만든 프로젝트는 프로젝트 구조의 복잡도를 낮추기 위해 세부 설정이 모두 숨겨져 있다. 이를 커스터마이징하려면 프로젝트 디렉터리에서 npm run eject명령어를 통해 세부 설정을 밖으로 꺼내줘야 한다.

create-react-app에서는 기본적으로 Git 설정이 되어 있는데, npm run eject는 아직 Git에 커밋되지 않은 변화가 있다면 진행되지 않으니, 먼저 커밋해야 한다.

VS Code 좌측에 있는 Git UI를 사용하거나,

다음 명령어를 통해 지금까지 한 작업을 커밋하자.

git add .
git commit -m "Commit before npm run eject"

그리고 나서 npm run eject 명령어를 실행한다.

이제 프로젝트 디렉터리에 config라는 디렉터리가 생성되었을 것이다. 그 디렉터리 안에 들어 있는 webpack.config.js를 열어보자.

그 파일에서 "sassRegex"라는 키워드를 찾아 보면(Ctrl + F) 두 번째 탐색 결과에서 아래와 같은 코드가 나타날 것이다.

여기서 use:에 있는 'sass-loader' 부분을 지우고, 뒷부분에 concat을 통해 커스터마이징된 sass-loader 설정을 넣어주자.

커스터마이징한 sass-loader

            {
              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,
                  }
                }
              }),
              sideEffects: true,
            },

리액트 버전 업데이트로 인해 위의 코드는 이러한 에러가 발생한다.
'options has an unknown property 'includePaths'. These properties are valid'
이 문제를 해결하기 위해 아래의 링크를 통해 참고하자.
커스터마이징 에러 글 참조

"options안에 또 다른 옵션들을 설정해 주어야 하기 때문에, 다짜고짜 sass 옵션을 써버리면 안되고, sassOptions 안에 정의를 해주어야 한다."

커스터마이징한 sass-loader 코드에서 options 안에 코드만 수정해준다.

options: {
  sassOptions: {
    includePaths: [paths.appSrc + '/styles'],
    sourceMap: isEnvProduction && shouldUseSourceMap,
  }
}

설정 파일을 저장한 후, 서버를 껐다가 재시작하자.
이제 utils.scss 파일을 불러올 때 현재 수정하고 있는 scss 파일 경로가 어디에 위치하더라도 앞부분에 상대 경로를 입력할 필요 없이 styles 디렉터리 기준 절대 경로를 사용하여 불러올 수 있다.

SassComponent.scss 파일에서 import 구문을 다음과 같이 수정해보자. 그리고 똑같이 적용되는지 확인해보아라.

@import './utils';

이제부터 utils.scss를 사용하는 컴포넌트가 있다면 위 한 줄만 넣어주면 된다.
하지만 새 파일을 생성할 때마다 utils.scss를 매번 이렇게 포함시키는 것도 귀찮을 수 있다. 그럴 때는 sass-loader의 data 옵션을 설정하면 된다.
data 옵션을 설정하면 Sass 파일을 불러올 때마다 코드의 맨 윗부분에 특정 코드를 포함시켜 준다.

webpack.config.js를 열어서 조금 전 수정했던 sass-loader의 옵션에 있는 data 필드를 다음과 같이 설정해보자.

{
  test: sassRegex,
  exclude: sassModuleRegex,
  use: getStyleLoaders({
    importLoaders: 2,
    sourceMap: isEnvProduction && shouldUseSourceMap
  }).concat({
    loader: require.resolve('sass-loader'),
    options: {
      sassOptions: {
        includePaths: [paths.appSrc + '/styles'],
        sourceMap: isEnvProduction && shouldUseSourceMap,
        data: `@import 'utils';`
      }
    }
  }),
  sideEffects: true,
},

이렇게 작성하면 모든 scss 파일에서 utils.scss를 자동으로 불러오므로, Sass에서 맨 윗줄에 있는 import 구문을 지워도 정상적으로 작동할 것이다.

라고 했는데 또 리액트 버전이 업데이트가 많이 된 것인지... sassOptions 객체에 data 키가 다른 걸로 바뀐 거 같다.
sass-loader 설정 글 참고

options: {
    additionalData: `@import 'utils';`,
    sassOptions: {
      includePaths: [paths.appSrc + "/styles"],
      sourceMap: isEnvProduction && shouldUseSourceMap,
    },
},

위의 링크에서 보았던 코드를 그대로 수정하였는데도...Sass에서 맨 윗줄에 있는 import 구문을 지우면 에러가 발생한다.

위의 에러는 SassComponent.scss 파일에서 생긴 에러인데,

주석 처리를 한 부분 때문에 utils.scss에서 $red를 정의한 것이 불러와지지 않아서이다.
구글링 해봐도 chatGPT로 검색을 해봐도 어디를 수정해야 될지 모르겠다.
이번 건 킵해둬야 될 거 같다.

3. node_modules에서 라이브러리 불러오기

Sass의 장점 중 하나는 라이브러리를 쉽게 불러와서 사용할 수 있다는 점이다. npm을 통해 설치한 라이브러리를 사용하는 가장 기본적인 방법에 대해 알아보자.
다음과 같이 상대 경로를 사용하여 node_modules까지 들어와서 불러오는 방법이다.

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

하지만 이런 구조는 스타일 파일이 깊숙한 디렉터리에 위치할 경우 ../를 매우 많이 적어야 하니 번거로울 것이다. 더 쉬운 방법은 바로 물결 문자(~)를 사용하는 방법이다.

import '~library/styles';

물결 문자를 사용하면 자동으로 node_modules에서 라이브러리 디렉터리를 탐지하여 스타일을 불러올 수 있다.

유용한 Sass 라이브러리 두 가지를 설치하고 사용해보겠다.
반응형 디자인을 쉽게 만들어 주는 include-media(https://include-media.com/)와 매우 편리한 색상 팔레트인 open-color(https://www.npmjs.com/package/open-color)를 npm 명령어를 사용해 설치해보자.
npm i include-media open-color

그 다음에는 utils.scss 파일을 열고 물결 표시를 사용하여 라이브러리를 불러오자. 다음 두 줄을 코드 상단에 넣어주면 된다.

utils.scss

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

위에서 불러온 include-media와 open-color를 SassComponent.scss에서 사용해보자. 해당 스타일 파일을 아래와 같이 수정해보자.

SassComponent.scss

.SassComponent {
  display: flex;
  background: $oc-gray-2;
  @include media('<768px') {
    background: $oc-gray-9;
  }
  .box {
    (...)
  }
}

이 코드는 SassComponent의 배경색을 open-colors 팔레트 라이브러리에서 불러온 후 설정하고, 화면 가로 크기가 768px 미만이 되면 배경색을 어둡게 바꿔 준다.


CSS Module

CSS Module은 CSS를 불러와서 사용할 때 클래스 이름을 고유한 값,
[파일 이름]_[클래스 이름]__[해시값] 형태로 자동으로 만들어서 컴포넌트 스타일 클래스 이름이 중첩되는 현상을 방지해 주는 기술이다.
CSS Module을 사용하기 위해 구버전(v1)의 create-react-app에서는 웹팩에서 css-loader 설정을 별도로 해줘야 했지만, v2 버전 이상부터는 따로 설정할 필요 없이 .module.css 확장자로 파일을 저장하기만 하면 CSS Module이 적용된다.

CSSModule.module.css라는 파일을 src 디렉터리에 생성하여 다음과 같이 한번 작성해보자.

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

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

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

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

CSS Module을 사용하면 클래스 이름을 지을 때 그 고유성에 대해 고민하지 않아도 된다. 해당 클래스는 방금 만든 스타일을 직접 불러온 컴포넌트 내부에서만 작동하기 때문이다.

만약 특정 클래스가 웹 페이지에서 전역적으로 사용되는 경우라면 :global을 앞에 입력하여 글로벌 CSS임을 명시해 줄 수 있다.

위의 CSS Module을 사용하는 리액트 컴포넌트도 작성해보자.

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;

CSS Module이 적용된 스타일 파일을 불러오면 객체를 하나 전달받게 되는데 CSS Module에서 사용한 클래스 이름과 해당 이름을 고유화한 값이 키-값 형태로 들어있다.
예를 들어 위 코드에서 console.log(styles)를 한다면 다음과 같은 결과가 나타난다.

{ wrapper: "CSSModule_wrapper__1SbdQ" }

우리가 지정한 클래스 이름 앞뒤로 파일 이름과 해시값이 붙어있는 것을 확인할 수 있다.
이 고유한 클래스 이름을 사용하려면 클래스를 적용하고 싶은 JSX 엘리먼트에
className={styles.[클래스 이름]} 형태로 전달해주면 된다.
:global을 사용하여 전역적으로 선언한 클래스의 경우 평상시 해왔던 것처럼 그냥 문자열을 넣어준다.

그 다음 App 컴포넌트에서 렌더링해보자.

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

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

export default App;

이렇게 화면에 출력된다.

CSS Module을 사용한 클래스 이름을 두 개 이상 적용할 때는 다음과 같이 코드를 작성하면 된다.

CSSModule.module.css

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

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

.inverted {
  color: black;
  background: white;
  border: 1px solid black;
}

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

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

CSSModule.js

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

export default CSSModule;

위 코드에서 ES6 문법 템플릿 리터럴을 사용하여 문자열을 합해주었다.
이 문법을 사용하면 문자열 안에 자바스크립트 레퍼런스를 쉽게 넣어 줄 수 있다.
만약 템플릿 리터럴 문법을 사용하고 싶지 않다면 다음과 같이 작성할 수도 있다.

className={[styles.wrapper, styles.inverted].join(' ')}

1. classnames

classnames는 CSS 클래스를 조건부로 설정할 때 매우 유용한 라이브러리이다.
또한 CSS Module을 사용할 때 이 라이브러리를 사용하면 여러 클래스를 적용할 때 매우 편리하다.

우선 해당 라이브러리를 설치하자.
npm i classnames --save

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이면 highlighted 클래스가 적용되고, false이면 적용되지 않는다. 추가로 theme으로 전달받는 문자열은 내용 그대로 클래스에 적용된다.

이런 라이브러리의 도움을 받지 않는다면 다음과 같은 형식으로 처리해야 될 것이다.

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

classnames를 쓰는 것이 가독성이 훨씬 좋아 보인다.

덧붙여 CSS Module과 함께 사용하면 CSS Module 사용이 훨씬 쉬워진다. classnames에 내장되어 있는 bind 함수를 사용하면 클래스를 넣어줄 때마다 styles.[클래스 이름] 형태를 사용할 필요가 없다. 사전에 미리 styles에서 받아 온 후 사용하게끔 설정해 두고 cx('클래스 이름', '클래스 이름2') 형태로 사용할 수 있다.

다음 코드는 우리가 만든 CSS Module 컴포넌트에 classnames의 bind 함수를 적용한 예이다.

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>
  );
};

CSS Module을 사용할 때 클래스를 여러 개 설정하거나, 또는 조건부로 클래스를 설정할 때 classnames의 bind를 사용하면 훨신 편리하게 작성할 수 있을 것이다.

2. Sass와 함께 사용하기

Sass를 사용할 때도 파일 이름 뒤에 .module.scss 확장자를 사용해주면 CSS Module로 사용할 수 있다.
CSSModule.module.css 파일의 이름을 CSSModule.module.scss로 한번 변경해보자. 스타일 코드도 이에 따라 조금 수정해보겠다.

CSSModule.module.scss

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

.wrapper {
  background: black;
  padding: 1rem;
  color: white;
  font-size: 2rem;
  &.inverted {
    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';

이전과 똑같은 화면을 볼 수 있을 것이다.

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

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

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

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

styled-components

컴포넌트 스타일링의 또 다른 패러다임은 자바스크립트 파일 안에 스타일을 선언하는 방식이다. 이 방식을 'CSS-in-JS'라고 부른다.
CSS-in-JS 라이브러리 중에서 개발자들이 가장 선호하는 styled-components를 알아보자.

styled-components를 대체할 수 있는 라이브러리로 현재 emotion이 대표적이다. 작동 방식도 styled-components와 비슷하다.

아래의 명령어를 입력하여 설치하도록 하자.
npm i styled-components

이 라이브러리를 통해 컴포넌트를 만들어 보겠다. styled-components를 사용하면 자바스크립트 파일 하나에 스타일까지 작성할 수 있기 때문에 .css 또는 .scss 확장자를 가진 스타일 파일을 따로 만들지 않아도 된다는 큰 이점이 있다.

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;

이제 이 컴포넌트를 App 컴포넌트에서 보이도록 하자.

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

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

export default App;

styled-components와 일반 classNames를 사용하는 CSS/Sass를 비교했을 때, 가장 큰 장점은 props 값으로 전달해 주는 값을 쉽게 스타일에 적용할 수 있다는 것이다.

VS Code를 사용할 때 styled-components를 위해 컴포넌트 내부에 작성한 스타일이 그저 문자열로 간주되어 코드 신택스 하이라이팅(문법에 따라 에디터 폰트 색상을 입히는 작업)이 제대로 이루어지지 않는다.

VS Code의 마켓플레이스에서 vscode-styled-components를 검색하여 설치하면 색상이 정상적으로 입혀진다.

1. Tagged 템플릿 리터럴

위에서 작성한 코드를 확인해보면, 스타일을 작성할 때 `을 사용하여 만든 문자열에 스타일 정보를 넣어 주었다. 이것을 Tagged 템플릿 리터럴이라고 부른다. CSS Module을 배울 때 나온 일반 템플릿 리터럴과 다른 점은 템플릿 안에 자바스크립트 객체나 함수를 전달할 때 온전히 추출할 수 있다는 것이다.

예를 들어 다음 코드와 실행 결과를 확인해보자.

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

템플릿에 객체를 넣거나 함수를 넣으면 형태를 잃어 버리게 된다. 객체는 "[object Object]"로 변환되고, 함수는 함수 내용이 그대로 문자열화되어 나타난다.

만약 다음과 같은 함수를 작성하고 나서 해당 함수 뒤에 템플릿 리터럴을 넣어주면, 템플릿 안에 넣은 값을 온전히 추출할 수 있다.

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

크롬 브라우저의 개발자 콘솔에다 코드를 붙여 넣으면 다음과 같은 결과가 나타난다.


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

styled-components를 사용하여 스타일링된 엘리먼트를 만들 때는 컴포넌트 파일의 상단에서 styled를 불러오고, styled.태그명을 사용하여 구현한다.

예시 코드

import styled from 'styled-components';

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

이렇게 styled.div 뒤에 Tagged 템플릿 리터럴 문법을 통해 스타일을 넣어주면, 해당 스타일이 적용된 div로 이루어진 리액트 컴포넌트가 생성된다. 그래서 나중에 Hello와 같은 형태로 사용할 수 있다.

div가 아닌 button이나 input에 스타일링을 하고 싶다면 styled.button 혹은 styled.input 같은 형태로 뒤에 태그명을 넣어주면 된다.

하지만 사용해야 할 태그명이 유동적이거나 특정 컴포넌트 자체에 스타일링해 주고 싶다면 다음과 같은 형태로 구현할 수 있다.

// 태그의 타입을 styled 함수의 인자로 전달
const MyInput = styled('input')`
  background: gray;
`

// 아예 컴포넌트 형식의 값을 넣어 줌
const StyledLink = styled(Link)`
  color: blue;
`

3. 스타일에서 props 조회하기

styled-components를 사용하면 스타일 쪽에서 컴포넌트에게 전달된 props 값을 참조할 수 있다. 이전에 작성했던 Box 컴포넌트를 다시 봐보도록 하자.

styledComponent.js - Box 컴포넌트

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

이 코드를 보면 background 값에 props를 조회해서 props.color의 값을 사용하게 했다. 그리고 color 값이 주어지지 않을 때는 blue를 기본 색상으로 설정했다.

이렇게 만들어진 코드는 JSX에서 사용될 때 다음과 같이 color 값을 props로 넣어 줄 수 있다.

<Box color="black">(...)</Box>

4. props에 따른 조건부 스타일링

일반 CSS 클래스를 사용하여 조건부 스타일링을 해야 할 때는 className을 사용하여 조건부 스타일링을 해왔는데, styled-components에서는 조건부 스타일링을 간단하게 props로도 처리할 수 있다.

다시 위에서 작성한 Button 컴포넌트를 확인해보자.

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

const Box = styled.div`
  (...) // 생략
`;

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

이렇게 만든 컴포넌트는 다음과 같이 props를 사용하여 서로 다른 스타일을 적용할 수 있다.

<Button>안녕하세요</Button>
<Button inverted={true}>테두리만</Button>

스타일 코드 여러 줄을 props에 따라 넣어 줘야 할 때는 CSS를 styled-components에서 불러와야 한다. CSS를 사용하지 않고 다음과 같이 바로 문자열을 넣어도 작동되기는 한다.

  ${props => 
    props.inverted && 
    `
 	  background: none;
 	  border: 2px solid white;
	  color: white;
    &:hover {
	  background: white;
	  color: black;
	}
  `};

하지만 이렇게 했을 때 해당 내용이 그저 문자열로만 취급되기 때문에 VS Code 확장 프로그램에서 신택스 하이라이팅이 제대로 이루어지지 않는다는 단점이 따른다.
더욱 치명적인 단점은 Tagged 템플릿 리터럴이 아니기 때문에 함수를 받아 사용하지 못 해 해당 부분에서는 props 값을 사용하지 못한다는 것이다. 만약 조건부 스타일링을 할 때는 넣는 여러 줄의 코드에서 props를 참조하지 않는다면 굳이 CSS를 불러와서 사용하지 않아도 상관없다. 하지만 props를 참조한다면, 반드시 CSS로 감싸줘서 Tagged 템플릿 리터럴을 사용해줘야 한다.

5. 반응형 디자인

이번엔는 style-components를 사용할 때 반응형 디자인을 어떻게 하는지 한번 알아보자. 브라우저의 가로 크기에 따라 다른 스타일을 적용하기 위해서는 일반 CSS를 사용할 때와 똑같이 media query를 사용하면 된다.

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%; 
  }
`;

일반 CSS에서 할 때랑 큰 차이는 없지만 이러한 작업을 여러 컴포넌트에서 반복해야 한다면 귀찮아질 것이다. 그럴 때는 이 작업을 함수화하여 간편하게 사용할 수 있다. styled-components 매뉴얼에서 제공하는 유틸 함수를 따라 사용해보자.

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: 100%;`};
`;

media를 한번 선언하고 나니까 이를 사용할 때 스타일 쪽의 코드가 훨씬 간단해졌다.
지금은 media를 StyledComponent.js에서 만들었지만, 실제로 사용한다면 아예 다른 파일로 모듈화한 뒤 여기저기서 불러와 사용하는 방식이 훨씬 편할 것이다.

참고문헌

김민준,「리액트를 다루는 기술 :실무에서 알아야 할 기술은 따로 있다!」, 길벗, 개정판[실은 2판] 2019 (개정판)

profile
프론트엔드 개발자입니다.

0개의 댓글