본문 바로가기

WINK-(Web & App)/React.js 스터디

[2024 여름방학 React.js 스터디] 이종윤 #5주차

반응형

리엑트 컴포넌트 스타일링하기

리액트에서 컴포넌트를 스타일링 하는 가장 기본적인 방법은 css 파일을 만들어서 컴포넌트에서 import 해서 사용하는 것 이다. 하지만 컴포넌트를 스타일링 할 때 다른 도구들을 사용하면 훨씬 더 편하게 작업을 할 수 있다.

  1. Sass
  2. CSS Module
  3. styled-components

이 블로그에서 위 3가지 기술들을 볼 것이다.

1. Sass

: CSS의 단점을 보완하기 위해 만든 CSS 전처리기이다. 보통 CSS를 사용하다보면 단순 반복되는 부분이 많은 등, 불편함이 느껴지기 마련인데, Sass는 이 부분을 거의 완전히 해소시켜주는 스타일시트 언어다. Sass에는 Sass와 SCSS가 있다.
또한 CSS의 확장팩 같은 언어이기 때문에 CSS 파일 그 자체를 SCSS로 확장자만 바꾸어주어도 정상적으로 작동한다.

다만 SASS 자체는 개발자용 언어이기 때문에 웹브라우저가 읽을 수 없다. 따라서 중간에 컴파일러를 거쳐서 CSS로 변환하여 HTML에 연결한다. - namuWiki

 

* SCSS( Sassy CSS)
말그대로 세시(Sassy)한 CSS라는 뜻이다. 실제 뜻을 생각해보면 CSS한 Sass가 더 맞는 것 같다. 가장 큰 특징은 기존 Sass의 문법에서, CSS의 원래 문법에서 사용되는 중괄호를 사용하여 CSS만 알던 사람들이 처음 접해도 직관적으로 의미를 이해할 수가 있다. 단순히 가독성만이 아니라 Sass의 기존문법이 들여쓰기 및 줄바꿈에 의존하는 문법임에 비해, SCSS는 중괄호가 있기에 공백에 의해 컴파일에러가 발생할 확률히 현저히 적다. 따라서 대부분의 사용자들이 SCSS 문법을 사용하여 Sass의 공식 문법으로 사용되고 있다. - namuWiki

 

+ 위 Sass를 설명할 때 SASS자체는 개발자용 언어이기 때문에 웹브라우저가 앍을 수 없다고 했다. 그래서 중간에 CSS로 변환해서 HTML에 연결해야 한다고 했다. 이를 위해 프로젝트 다이어리에 node-sass 라이브러리를 설치하여 사용한다.

 

이 sass에서는 파이썬의 클래스처럼 사용할 수 있는 크래스가 있다.

function Button({ children, size, color }) {
  return (
    <button className={classNames('Button', size, color)}>{children}</button>
  );
}

 

이렇게 기본 함수를 만들고 사용할 인자(여기서는 'size'와 'color')를 classNames로 이렇게 적어둔 한수를 만든다.

그리고 scss파일에 .&라는 기호에 파라미터를 작성하고 형태는 함수모양으로 작성한다.

 

 &.small {
    height: 1.75rem;
    font-size: 0.875rem;
    padding-left: 1rem;
    padding-right: 1rem;
  }

  // 색상 관리
  &.blue {
    background: $blue;
    &:hover {
      background: lighten($blue, 10%);
    }

    &:active {
      background: darken($blue, 10%);
    }
  }

이렇게 적는다. ( '&.'는 각 유형별로 다 적고 함수 호출 할 때 유형별로 하나씩 쓴다.)

*  ':&' 기호는 특정이벤트 발생시 발동이다.

 

추가로 이렇게 색상관련 코드들은 쓸데없이 반복되는 부분이 많다.

Sass 의 mixin 이라는 기능을 사용하여 쉽게 재사용 할 수 있다.

$blue: #228be6;
$gray: #495057;
$pink: #f06595;

@mixin button-color($color) {
  background: $color;
  &:hover {
    background: lighten($color, 10%);
  }
  &:active {
    background: darken($color, 10%);
  }
}
.Button{
 &.blue {
    @include button-color($blue);
  }

  &.gray {
    @include button-color($gray);
  }

  &.pink {
    @include button-color($pink);
  }
 }

그리고

<Button size="large" color="gray">버튼</Button>

 

함수를 호출 할 때는 이렇게 하면 된다.

function Button({ props1, props2, { 이거요 }) {
  return (
    <button className={classNames('Button', props2, { 이거요 })}>
      {props1}
    </button>
  );
}

그리고  인자값이 true일 때만 그 값을 받고싶으면 '이거요' 처럼 쓰면 된다.

function Button({ children, size, color, outline, fullWidth, onClick, onMouseMove }) {
  return (
    <button
      className={classNames('Button', size, color, { outline, fullWidth })}
      onClick={onClick}
      onMouseMove={onMouseMove}
    >
      {children}
    </button>
  );
}

만약 함수를 다 작성하고 온클릭이나 온마우스를 넣고싶다면 이렇게 넣어야한다.

그러나 여기서는  spread 와 rest를 사용할 수 있다.

function Button({ children, size, color, outline, fullWidth, ...rest }) {
  return (
    <button
      className={classNames('Button', size, color, { outline, fullWidth })}
      {...rest}
    >
      {children}
    </button>
  );
}

이거를 이렇게 써주면 된다.

2. CSS Module

: 리액트 프로젝트에서 컴포넌트를 스타일링 할 때 CSS Module 이라는 기술을 사용하면, CSS 클래스가 중첩되는 것을 완벽히 방지할 수 있다. CSS 파일의 확장자를 .module.css 로 하면 된다.

import React from "react";
import styles from "./Box.module.css";

function Box() {
  return <div className={styles.Box}>{styles.Box}</div>;
}

export default Box;

이렇게 그냥 css파일을 받아와서 쓸 때 클래스 이름을 파일 경로, 파일 이름, 클래스 이름, 해쉬값 등을 사용해서 고유하게 만든다.

위 방법을 이럴 때 좋다.

  • 레거시 프로젝트에 리액트를 도입할 때 (기존 프로젝트에 있던 CSS 클래스와 이름이 중복되어도 스타일이 꼬이지 않게 해줍니다.)
  • CSS 클래스를 중복되지 않게 작성하기 위하여 CSS 클래스 네이밍 규칙을 만들기 귀찮을 때

조건 삼항 연산자

: 조건 (삼항) 연산자는 JavaScript에서 세 개의 피연산자를 받는 유일한 연산자이다. 앞에서부터 조건문, 물음표(?), 조건문이 참(truthy)일 경우 실행할 표현식, 콜론(:), 조건문이 거짓(falsy)일 경우 실행할 표현식이 배치된다.

condition ? exprIfTrue : exprIfFalse;

react-icons 라이브러리

: 다양한 아이콘들을 React 프로젝트에서 사용할 수 있게 해주는 라이브러리이다.

> npm install react-icons --save
> yarn add react-icons --save

 

React icons 홈페이지에 가서 원하는 아이콘을 고르고

import { 아이콘명 } from 'react-icons/아이콘명 앞글자';

이렇게 쓰면 된다.

여기서 주의할 점은 'react-icons/아이콘명 앞글자' 이 부분이다.
아이콘의 이름들은 모두 파스칼 케이스로 되어 있다. (파스칼 케이스 : 첫 글자와 중간 글자가 모두 대문자)

여기서 파스칼 케이스 중 첫 번째 부분을 'react-icons/' 뒤에 소문자로 써야 한다.

'FiMenu'를 예로 들면, 'FiMenu'의 첫 번째 부분인 'Fi'를 소문자 'fi'로 바꿔 'react-icons/fi' 이렇게 써야 정상적으로 작동이 된다.

import { FiMenu } from 'react-icons/fi';
import { FaApple } from 'react-icons/fa';

 

이렇게

CSS Module 사용

  • 먼저 [컴포넌트 이름].module.css 라는 이름의 css 를 만들어 준다.
  • 컴포넌트 상단에 해당 css를 import (styles와 같은 변수에 담을 수 있음)
  • styles 객체 안에 있는 클래스 값들을 styles.Box와 같은 이름으로 가져오게 되면 해싱 된 값으로 보여지게 됨
  • 만약에 클래스 이름에 -가 들어가 있으면 styles['my-class'] 와 같이 가져옴
    여러 개가 있다면 ${styles.one} ${styles.two}와 같이 조회해야함.

-> 이런 번거로움을 개선한 것이 classnames 라이브러리의 bind기능을 이용해 조건부 렌더링이나 여러 개의 클래스를 사용시 좀 더 편하게 사용할 수 있게 함.

<사용볍>

  • npm install classnames
  • component 파일 상단에 import classNames from 'classnames/bind';
  • const cx = classNames.bind(styles);
  • className={cx("클래스 이름")} 과 같이 해당 스타일 가져옴
  • 여러 클래스 사용
cx('one', 'two')
  • 조건부 스타일링
cx("my-component", {
  condition: true,
});
cx("my-component", ["another", "classnames"]);

3. styled-components

: styled-components 는 현존하는 CSS in JS 관련 리액트 라이브러리 중에서 가장 인기 있는 라이브러리이다.

: CSS in JS이름에서 알 수 있듯이 CSS-in-JS를 사용하면 자바스크립트 또는 타입스크립트 코드에서 직접 CSS를 작성하여 리액트 컴포넌트 스타일을 지정할 수 있습니다.

 

먼저 Tagged Template Literals를 배워보자

Template Literals

: 동적인 문자열을 처리할 때 우리가 많이 사용했던 것이다.

const userName = 'jongyun';
const age = 20;
const output = `Hi, ${userName} and I am ${age}.`;

..... 이렇게

Tagged Template Literals

: 그렇다면 Tagged Template Literals는 무엇이냐, ' ${ } ' 안에 일반 문자열 / 숫자가 아닌 객체를 넣으면 타입에 상관없이 Function, Number, Array, Object 등을 전달하고 이를 실행할 수 있게 되는 것이다.

function transform(staticData, ...dynamicData) {
  console.log(staticData); // ["Hi, ", " and I am ", "."]
  console.log(dynamicData); // ["JongYun", 20]
}

transform`Hi, ${userName} and I am ${age}.`;

그냥 function으로 실행하면 20이 string로 나왔을 텐데 이렇게 Tagged Template Literals를 사용하면 Number로 나온다.

 

우선 styled-components를 설치한다.

$ npx create-react-app styling-with-styled-components
$ cd styling-with-styled-components
$ yarn add styled-components

..... 이렇게

styled-components 사용

- HTML 엘리먼트를 스타일링 할 때는 모든 알려진 HTML 태그에 대해서 이미 속성이 정의되어 있기 때문에 해당 태그명의 속성에 접근한다.

import styled from "styled-components";

styled.button`
  // <button> HTML 엘리먼트에 대한 스타일 정의
`;

 

- React 컴포넌트를 스타일링 할 때는 해당 컴포넌트를 임포트 후 인자로 해당 컴포넌트를 넘기면 된다.

import styled from "styled-components";
import Button from "./Button";

styled(Button)`
  // <Button> React 컴포넌트에 스타일 정의
`;

 

두가지 문법 모두 ES6의 Tagged Template Literals을 사용해서 스타일을 정의한다.

그리고 styled 함수는 결국 해당 스타일이 적용된 HTML 엘리먼트나 React 컴포넌트를 리턴한다.

polished

: Sass 를 사용 할 때에는 lighten() 또는 darken() 과 같은 유틸 함수를 사용하여 색상에 변화를 줄 수 있었는데, CSS in JS 에서도 비슷한 유틸 함수를 사용하기 위한 라이브러리이다.

 

우선 이 패키지를 받는다.

$ yarn add polished
import {darken, lighten} from 'polished';

그리고 얘를 받고

 &:hover{
        background: ${lighten(0.1, '#228be6')};
    }
 &:active{
        background:  ${darken(0.1, '#228be6')};
    }

이 lighten과 darken은 뒤의 색을 x%(여기서는 10%) 더 밝게, 어둡게 만들어주는 것이다.

ThemeProvider

: ThemeProvider는 Styled-Components안에 들어있는 컴포넌트중 하나이다. ThemeProvider를 사용하면 하위에 있는 태그들에게 모두 스타일이 미치게되어 전역적으로 스타일링을 해줄 수 있다.

import styled, { ThemeProvider } from 'styled-components';

우선 ThemeProvider를 받는다.

그리고 ThemeProvider를 사용하려면 다음과 같이 사용할 컴포넌트 계층 구조 내에 포함시키면 된다.

const theme = {
  colors: {
    primary: '#fca311',
    secondary: '#ffba08',
  },
};

function App() {
  return (
    <ThemeProvider theme={theme}>
      {/* 테마를 사용할 컴포넌트 계층 구조 */}
    </ThemeProvider>
  );
}

이렇게 theme에 props들을 넣고 theme 하위에서 사용해주면 된다.

function App() {
  return (
    <ThemeProvider
      theme={{
        palette: {
          blue: '#228be6',
          gray: '#495057',
          pink: '#f06595'
        }
      }}
    >
      <AppBlock>
        <Button>BUTTON</Button>
      </AppBlock>
    </ThemeProvider>
  );
}

이렇게 작성한다고 가정하자

theme provider를 Button 컴포넌트에서 사용할 수 있다.

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

const StyledButton = styled.button`
  /* 공통 스타일 */

  /* 크기 */

  /* 색상 */
  ${props => {
    const selected = props.theme.palette[props.color];
    return css`
      background: ${selected};
      &:hover {
        background: ${lighten(0.1, selected)};
      }
      &:active {
        background: ${darken(0.1, selected)};
      }
    `;
  }}

  /* 기타 */
  & + & {
    margin-left: 1rem;
  }
`;

function Button({ children, ...rest }) {
  return <StyledButton {...rest}>{children}</StyledButton>;
}

Button.defaultProps = {
  color: 'blue'
};

export default Button;

버튼 컴포넌트에서 이렇게 사용할 수 있다.

 

여기에 사이즈 관련 코드도 넣을 수 있다.

const sizes = {
  large: {
    height: '3rem',
    fontSize: '1.25rem'
  },
  medium: {
    height: '2.25rem',
    fontSize: '1rem'
  },
  small: {
    height: '1.75rem',
    fontSize: '0.875rem'
  }
};

const sizeStyles = css`
  ${({ size }) => css`
    height: ${sizes[size].height};
    font-size: ${sizes[size].fontSize};
  `}
`;

..... 이렇게

 

이전에 배웠던 것들을 잘 활용하면 outline하고 fullwidth 버튼도 만들 수 있을 것이다.

Dialog

import React from 'react';
import styled from 'styled-components';
import Button from './Button';

const DarkBackground = styled.div`
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.8);
`;

const DialogBlock = styled.div`
  width: 320px;
  padding: 1.5rem;
  background: white;
  border-radius: 2px;
  h3 {
    margin: 0;
    font-size: 1.5rem;
  }
  p {
    font-size: 1.125rem;
  }
`;

const ButtonGroup = styled.div`
  margin-top: 3rem;
  display: flex;
  justify-content: flex-end;
`;

const ShortMarginButton = styled(Button)`
  & + & {
    margin-left: 0.5rem;
  }
`;

function Dialog({
  title,
  children,
  confirmText,
  cancelText,
  onConfirm,
  onCancel,
  visible
}) {
  if (!visible) return null;
  return (
    <DarkBackground>
      <DialogBlock>
        <h3>{title}</h3>
        <p>{children}</p>
        <ButtonGroup>
          <ShortMarginButton color="gray" onClick={onCancel}>
            {cancelText}
          </ShortMarginButton>
          <ShortMarginButton color="pink" onClick={onConfirm}>
            {confirmText}
          </ShortMarginButton>
        </ButtonGroup>
      </DialogBlock>
    </DarkBackground>
  );
}

Dialog.defaultProps = {
  confirmText: '확인',
  cancelText: '취소'
};

export default Dialog;

이렇게 dialog를 만들 수 있다.

위처럼 dialog의 기능을 구현하는 것은 간단하다.

이제 트랜지션을 구현해보자

트랜지션

: 트랜지션(transion)이란 한 장면에서 다른 장면으로 바뀔 때, 또는 다른 기능의 실행 시 사용하는 장면 전환기술을 의미한다. 트랜지션 효과를 적용 할 때에는 CSS Keyframe 을 사용하며, styled-components 에서 이를 사용 할 때는 keyframes 라는 유틸을 사용한다.

- Dialog가 나타날 때 DarkBackground 쪽에는 서서히 나타나는 fadeIn 효과를 주고, DialogBlock 에는 아래에서부터 위로 올라오는 효과를 보여주는 slideUp 효과를 줘보겠다. 애니메이션의 이름은 여러분들이 마음대로 지정 할 수 있다.

fadeIn 애니메이션

const fadeIn = keyframes`
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
`;

 

  • fadeIn은 요소의 불투명도를 점진적으로 0에서 1로 변경하여 요소가 서서히 나타나는 효과를 준다.
  • from 단계에서는 opacity가 0으로 설정되어 요소가 보이지 않으며, to 단계에서는 opacity가 1로 설정되어 요소가 완전히 보이게 된다.

slideUp 애니메이션

const slideUp = keyframes`
  from {
    transform: translateY(200px);
  }
  to {
    transform: translateY(0px);
  }
`;

 

  • slideUp은 요소를 아래에서 위로 슬라이드하여 나타나게 하는 효과를 준다.
  • from 단계에서는 transform: translateY(200px);로 설정되어 요소가 아래로 200px 이동된 상태이다.
  • to 단계에서는 transform: translateY(0px);로 설정되어 요소가 원래 위치로 돌아오며 나타난다.

- 사라지는 효과를 구현하려면 Dialog 컴포넌트에서 두개의 로컬 상태를 관리해주어야 한다. 하나는 현재 트랜지션 효과를 보여주고 있는 중이라는 상태를 의미하는 animate, 나머지 하나는 실제로 컴포넌트가 사라지는 시점을 지연시키기 위한 localVisible 값이다.

- 그리고 useEffect 를 하나 작성해야 하는데, visible 값이 true 에서 false 로 바뀌는 시점을 감지하여 animate 값을 true 로 바꿔주고 setTimeout 함수를 사용하여 250ms 이후 false로 바꿔야 한다.

추가적으로, !visible 조건에서 null 를 반환하는 대신에 !animate && !localVisible 조건에서 null 을 반환하도록 수정해야 한다.

fadeOut 애니메이션

const fadeOut = keyframes`
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
`;

 

 

  • fadeOut은 요소의 불투명도를 점진적으로 1에서 0으로 변경하여 요소가 서서히 사라지는 효과를 준다.

slideDown 애니메이션

const slideDown = keyframes`
  from {
    transform: translateY(0px);
  }
  to {
    transform: translateY(200px);
  }
`;
  • slideDown은 요소를 위에서 아래로 슬라이드하여 사라지게 하는 효과를 준다.

1. 상태 관리

const [animate, setAnimate] = useState(false);
const [localVisible, setLocalVisible] = useState(visible);
  • animate: 트랜지션 효과를 보여주는 상태를 관리한다.
  • localVisible: 컴포넌트가 실제로 사라지는 시점을 지연시키기 위한 상태를 관리한다.

2. useEffect 사용

useEffect(() => {
  if (localVisible && !visible) {
    setAnimate(true);
    setTimeout(() => setAnimate(false), 250);
  }
  setLocalVisible(visible);
}, [localVisible, visible]);
  • visible 값이 true에서 false로 바뀌는 것을 감지하여 animate 값을 true로 설정하고, 250ms 후에 false로 바꾼다.
  • setLocalVisible(visible): localVisible 값을 visible 값으로 업데이트한다.

3. 조건 수정

if (!animate && !localVisible) return null;
  • !visible 조건 대신 !animate && !localVisible 조건에서 null을 반환하여 애니메이션이 끝날 때까지 컴포넌트가 화면에 남아 있도록 한다.

 

이렇게 코드를 잘~~ 활용해 트랜지션을 작성하면 된다 :)

반응형