9장 - 컴포넌트 스타일링(2)

sh·2022년 8월 23일
0

CSS Module

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

.module.css 확장자로 파일을 저장하기만 하면 CSS Module이 적용된다.

/* CSSModule.module.css */

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

/* 글로벌 CSS : 특정 클래스가 웹 페이지에서 전역적으로 사용되는 경우*/
:global .something {
  font-weight: 800;
  color: aqua;
}

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

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이 적용된 스타일 파일(CSSModule.module.css)을 불러오면 객체(styles)를 하나 전달받게 되는데 CSS Module에서 사용한 클래스 이름과 해당 이름을 고유화한 값이 키-값 형태로 들어 있다.

console.log(styles);의 결과 :

이 고유한 클래스 이름을 사용하려면 클래스를 적용하고 싶은 JSX 엘리먼트에 className={styles.[클래스 이름]} 형태로 전달해 주면 된다.
:global 을 사용하여 전역적으로 선언한 클래스는 그냥 문자열로 넣으면 된다.


CSS Module을 사용한 클래스 이름을 두 개 이상 적용할 때는 다음과 같이 사용한다.

<div className={`${styles.wrapper} ${styles.inverted}`}>

ES6 문법 템플릿 리터럴을 사용해 문자열을 합해 주는 방식이다.

만약 템플릿 리터럴 분법을 사용하고 싶지 않다면 다음과 같이 작성할 수도 있다.

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

classnames

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

$ yarn add classnames 또는 $ npm install 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 클래스를 설정할 수 있기 때문에 컴포넌트에서 조건부로 클래스를 설정할 때 매우 편리하다.

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

만약 위에서 highlighted 값이 true 이면 highlighted 클래스가 적용되고, false이면 적용되지 않는다.

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

위에서 CSS Module을 사용한 코드에 bind 함수를 적용한 예를 보면 다음과 같다.

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

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

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

export default CSSModule;

Sass와 함께 사용하기

Sass를 사용할 때도 파일 이름 뒤에 .modules.scss 확장자를 사용해 CSS Module로 사용할 수 있다.

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

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

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

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


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


styled-components

컴포넌트 스타일링의 또 다른 패러다임은 자바스크립트 파일 안에 스타일을 선언하는 방식이다.
이 방식을 'CSS-in-JS`라고 부르고 이와 관련된 라이브러리는 정말 많다.
CSS-in-JS 라이브러리 중에서 개발자들이 가장 선호하는 것이 styled-components이다.

설치
$ yarn add styled-components 또는 $ npm install styled-components

styled-components를 사용하면 자바스크립트 파일 하나에 스타일까지 작성할 수 있기 때문에 .css 또는 .scss 확장자를 가진 스타일 파일을 따로 만들지 않아도 된다는 큰 이점이 있다.
또한 props 값으로 전달해 주는 값을 쉽게 스타일에 적용할 수 있다.

// 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 StyledComponents = () => {
  return (
    <Box color="black">
      <Button>안녕하세요</Button>
      <Button inverted={true}>테두리만</Button>
    </Box>
  );
};

export default StyledComponents;

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'}!`
	

Tagged 템플릿 리터럴을 사용하면 이렇게 템플릿 사이사이에 들어가는 자바스크립트 객체나 함수의 원본 값을 그대로 추출할 수 있다.
styled-components는 이러한 속성을 사용해 컴포넌트의 props를 스타일 쪽에서 쉽게 조회할 수 있도록 해준다.


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

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

import styled from 'styled-components';

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

styled.div 뒤에 Tagged 템플릿 리터럴 문법을 통해 스타일을 넣어 주면, 해당 스타일이 적용된 div로 이루어진 리액트 컴포넌트가 생성된다.

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

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

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

컴포넌트를 styled의 파라미터에 넣는 경우에는 해당 컴포넌트에 className props를 최상위 DOM의 className값으로 설정하는 작업이 내부적으로 되어 있어야 한다.

const Sample = ({ className }) => {
  return <div className={className}>Sample</div>;
};

const StyledSample = styled(Sample)`
  font-size: 2rem;
`;

스타일에서 props 조회하기

스타일 쪽에서 컴포넌트에게 전달된 props 값을 참조할 수 있다.

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>

props에 따른 조건부 스타일링

위에서 작성한 Button 컴포넌트는 다음과 같이 props를 사용해 서로 다른 스타일을 적용할 수 있다.

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

스타일 코드 여러 줄을 props에 따라 넣어 주어야 할 때는 CSS를 styled-components에서 불러와야 한다.
import styled, { css } from "styled-components";

CSS를 사용하지 않고 다음과 같이 문자열을 넣어도 작동하기는 하지만 해당 내용이 그저 문자열로만 취급되기 때문에 신택스 하이라이팅이 제대로 이루어지지 않는다. 또 Tagged 템플릿 리터럴이 아니기 때문에 함수를 받아 사용하지 못해 해당 부분에서는 props 값을 사용하지 못한다.

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

여러 줄의 코드에서 props 를 참조한다면 반드시 CSS로 감싸 주어 Tagged 템플릿 리터럴을 사용해야 한다.


반응형 디자인

브라우저 가로 크기에 따라 다른 스타일을 적용하기 위해서는 일반 CSS를 사용할 때와 똑같이 media 쿼리를 사용한다.

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

이러한 작업을 여러 컴포넌트에서 반복하면 조금 귀찮 -> 이 작업을 함수화하여 간편하게 사용가능!
styled-components 매뉴얼에서 제공하는 유틸 함수를 따라 사용해보면 다음과 같다.

https://www.styled-components.com/docs/advanced#media-templates

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 를 StyledComponents.js에서 만들어 주었지만, 실제로 사용한다면 아예 다른 파일로 모듈화한 뒤 여기저기서 불러와 사용하는 방식이 편할 것이다.

0개의 댓글