React/[책] 리액트를 다루는 기술

9장. 컴포넌트 스타일링

spring_sunshine 2021. 7. 28. 09:42
  • 일반 CSS: 컴포넌트를 스타일링 하는 가장 기본적인 방식
  • Sass: 자주 사용되는 CSS 전처리기 중 하나로, 확장된 CSS 문법을 사용하여 CSS 코드를 더욱 쉽게 작성할 수 있게 해준다.
  • CSS Module: 스타일을 작성할 때 CSS 클래스가 다른 CSS 클래스의 이름과 절대 충돌하지 않도록 고유한 이름을 자동으로 생성해주는 옵션
  • styled-components: 스타일을 자바스크립트 파일에 내장시키는 방식으로, 스타일을 작성함과 동시에 해당 스타일이 적용된 컴포넌트를 만들 수 있게 해준다. 

9.1 가장 흔한 방식, 일반 CSS

  • 프로젝트는 일반 CSS 방식으로 만들어져 있다. 
  • CSS를 작성할 때 가장 중요한 점은 CSS 클래스를 중복되지 않게 만드는 것 → 중복되는 것을 방지하는 여러가지 방식 중 하나는 이름을 지을 때 특별한 규칙을 이용하고, 또 다른 하나는 CSS Selector를 활용하는 것이다.

1) 이름 짓는 규칙

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

2) CSS Selector

  • CSS Selector를 사용하면 CSS 클래스가 특정 클래스 내부에 있는 경우에만 스타일을 적용할 수 있다. 
  • 특정 클래스 안에 있는 태그의 스타일을 적용할때는 .을 생략하고 Classname만 적는다.

.App안에 들어 있는 .logo에 스타일을 적용하고 싶다면 다음과 같이 작성한다.

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

 

기존 App.css와 App.js의 CSS 클래스 부분을 수정해보자.

 

App.css

.App {
  text-align: center;
}

/* .App 안에 들어 있는 .logo*/
.App .logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App .logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

/* .App 안에 들어 있는 header
	header는 클래스가 아닌 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

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

function App() {
  return (
    <div className="App">
      <header>
        <img src={logo} className="App-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 요소에는 컴포넌트의 이름으로 클래스 이름(.App)을 짓고, 그 내부에서는 소문자를 입력(.logo)하거나, header 같은 태그를 사용하여 클래스 이름이 불필요한 경우에는 아예 생략할 수 있다.


9.2 Sass 사용하기 (Syntatically Awesome Style Sheets) 

  • Sass는 CSS 전처리기로 복잡한 작업을 쉽게 할 수 있게 해준다.
  • 스타일 코드의 재활용성을 높여 줄 뿐만 아니라, 코드의 가독성을 높여서 유지 보수를 더욱 쉽게 해준다.
  • Sass에서는 두 가지 확장자 .scss와 .sass를 지원한다. (.scss가 더 자주 사용된다)

.sass

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

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

→ .sass 확장자는 중괄호와 세미콜론을 사용하지 않는다.

 

.scss (주로 쓰임)

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

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

 

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 {
    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;
    }
  }
}​

 

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 (
      <div>
        <SassComponent />
      </div>
    );
  }
}

export default App;

1) utils 함수 분리하기 

 

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

 

src/styles/utils.scss

$red: #fa5252;
$orange: #fd7214;
$yellow: #fcc419;
$green: #40c057;
$blue: #339af0;
$indigo: #5c7cfa;
$violet: #7950f2;

// 믹스인 만들기(재사용되는 스타일 블록을 함수처럼 사용할 수 있음)
@mixin square($size) {
  $calculated: 32px * $size;
  width: $calculated;
  height: $calculated;
}

 

SassComponent.scss

@import './styles/utils';
.SassComponent {
	display: flex;
    .box {
		background: red;
        cursor: pointer;
        (...)
    }
}

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


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

  • 프로젝트에 디렉터리를 많이 만들어서 구조가 깊어졌다면 해당 파일에서 상위 폴더로 한참 거슬러 올라가야 한다는 단점이 있다. 
  • 이 문제점은 웹팩에서 Sass를 처리하는 sass-loader의 설정을 커스터마이징하여 해결할 수 있다. → 프로젝트 디렉터리에서 yarn eject 명령어를 통해 세부 설정을 밖으로 꺼내 주어야 한다.

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

 

기본적인 방법: 상대 경로를 사용하여 noe_modules까지 들어가서 불러오는 방법이다.

@import '~library/styles';

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

 

반응형 디자인을 쉽게 만들어주는 include-media와 매우 편리한 색상 팔레트인 open-color를 yarn 명령어를 사용해 설치해 보자. 

$yarn add open-color include-media

 

이제 utils.scss 파일을 열고 물결 표시를 사용하여 라이브러리를 불러오자.

 

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

 

SassComponent.scss

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

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

 


9.3 CSS Module

CSS Module은 CSS를 불러와서 사용할 때 클래스 이름을 고유한 값, 즉 [파일 이름]_[클래스 이름]_[해시값] 형태로 자동으로 만들어서 컴포넌트 스타일 클래스 이름이 중첩되는 현상을 방지해 주는 기술이다. 

 

CSSModule.module.css

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

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

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

:global .something {
  font-weight: 800;
  color: aqua;
}
  • CSS Module을 사용하면 스타일을 직접 불러온 컴포넌트 내부에서만 해당 클래스가 작동하기 때문에 클래스 이름을 지을 때 고유성에 대해 고민하지 않아도 된다. 
  • 특정 클래스가 웹 페이지 전역적으로 사용되는 경우라면 :global을 앞에 입력하여 글로벌 CSS임을 명시해 줄 수 있다.

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.[클래스 이름]} 형태로 전달해 주면 된다. 

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 "./CSSModule.module.css";

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

export default CSSModule;

→ 위 코드에서는 ES6 문법 템플릿 리터럴을 사용하여 문자열을 합해 주었다.

 

위와 같이 템플리 리터럴 문법을 사용하고 싶지 않다면 다음과 같이 작성할 수 있다. 

<div className={[styles.wrapper, styles.invertes].join(' ')}>

 

1) classnames

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

우선 해당 라이브러리를 설치하자.

$ 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값에 따라 다른 스타일을 주기가 쉬워진다. 

 

example 코드 

const MyComponent = ({hightlighted, theme}) => (
	<div className={classNames('MyComponent', { highlighted }, theme)}>Hello</div>
);
  • 엘리먼트 클래스에 highlighted 값이 true이면 highlighted 클래스가 적용되고, false면 적용되지 않는다.
  • theme으로 전달받는 문자열은 내용 그대로 클래스에 적용된다. 

CSS Module과 함께 사용하면 CSS Module 사용이 더 쉬워진다.

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 함수를 사용하면 클래스를 넣어줄 때마다 styles.[클래스 이름] 형태를 사용할 필요가 없다. 
  • 사전에 미리 styles에서 받아온 후 사용하도록 설정하고 cx('클래스 이름1', '클래스 이름2') 형태로 사용할 수 있다.

2) 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;
  }
  // 여기에 다른 클래스를 만들 수도 있겠죠?
}

 

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

 

일반 .css/.scss 파일에서도 :local을 사용하여 CSS Module을 사용할 수 있다.

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

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

9.4 styled-components

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

StyleComponent.js

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

const Box = styled.div`
    background: ${props => props.color || 'blue'};
    padding : 1rem;
    display: flex;
    `;
const Button = styled.button`
    background:wihte;
    color:black;
    border-raidus: 4px;
    padding: 0.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: 1rem;
    font-weight:600;

    &:hover {
        background: rgba(255,255,255,0.9);
    }

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

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를 불러오고, style. 태그명을 사용하여 구현한다. 

import styled from 'styled-components';

const MyComponent = styled.div`
	font-size:2rem;
`;
  • styled.div 뒤에 Tagged 템플릿 리터럴 문법을 통해 스타일을 넣어 주면, 해당 스타일이 적용된 div로 이루어진 리액트 컴포넌트가 생성된다. → 나중에 <Mycomponent>Hello</Mycomponent>와 같은 형태로 사용할 수 있다.
  • 만약 div가 아닌 button이나 input에 스타일링을 하고 싶다면 styled.button 혹은 styled.input 같은 형태로 뒤에 태그명을 넣어주면 된다. 

 

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

 

styled.components를 사용하면 스타일 쪽에서 컴포넌트에 전달된 props 값을 참조할 수 있다. 

 

StyledComponents.js - Box 컴포넌트 

const Box = styled.div`
	background: ${props => props.color || 'blue'|;
	padding: 1rem;
	display: flex;
`;

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

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

 

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

 

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

 

StyledComponents.js - Button

import styled, {css} from 'styled-components';
/* 단순 변수 형태가 아니라 여러 줄의 스타일 구문을 조건부로 설정해야하는 경우에는
css를 불러와야 한다.
*/

const Button = styled.button`
    background:wihte;
    color:black;
    border-raidus: 4px;
    padding: 0.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: 1rem;
    font-weight:600;
		
		/* &문자를 사용하여 Sass처럼 자기 자신 선택 가능 */
    &:hover {
        background: rgba(255,255,255,0.9);
    }
		
		/* 다음 코드는 inberted값이 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;

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

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

 

5) 반응형 디자인 

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

StyledComponents.js - Box

const Box = styled.div`
	/*props로 넣어 준 값을 직접 전달해 줄 수 있다*/
    background: ${props => props.color || 'blue'};
    padding : 1rem;
    display: flex;
    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 쿼리 함수를 만들어 줍니다.
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`
    background: ${props => props.color || 'blue'};
    padding : 1rem;
    display: flex;
    width:1024px;
    margin: 0 auto;
    ${media.desktop`width: 768px;`}
    ${media.tablet`width:100%`}
  `;

 


9.5 정리 

  1. 다양한 리액트 컴포넌트 스타일링 방식을 배워 보았다.