맨 땅에 프론트엔드 개발자 되기

styled-components를 이용하여 스타일링 변경이 가능한 컴포넌트 만들기 본문

코딩 공부 일지/React JS

styled-components를 이용하여 스타일링 변경이 가능한 컴포넌트 만들기

헬로코딩 2022. 5. 3. 17:24
728x90

styled-components를 사용하는 이유

CSS in JS 는 웹 개발 방식의 한 방법으로 JS 파일 안에 CSS 작성해서 경우에 따라 JS 로 CSS 변경을 용이하게 만들어 스타일링 변경이 가능한 컴포넌트를  사용할 수 있게 해주는 하나의 방법이다.

Tagged Template Literal을 이용하여 함수의 파라미터로 템플릿 리터럴을 받는 기술인데 복잡하므로 원리를 이해할 필요는 없다.

 

npm i styled-components

 

기본 문법

import styled from 'styled-components';

// Create a Title component that'll render an <h1> tag with some styles
const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

// Create a Wrapper component that'll render a <section> tag with some styles
const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`;

// Use Title and Wrapper like any other React component – except they're styled!
function App() {
	return (
    <Wrapper>
        <Title>
          Hello World!
        </Title>
    </Wrapper>
);

export default App;
}

styled-components 를 처음 사용하면서 헷갈렸던 것은 기존에 JSX 문법으로 컴포넌트를 불러와 return을 시키는 것이 아니라 html 태그 대신 스타일링이 적용된 컴포넌트를 상단에 구성하고 JSX 문법으로 사용하는 것이었다. 

CSS 문법과 매우 유사하고 SASS와 같이 nesting 문법이 사용 가능하기 때문에 조금만 익숙해지면 매우 쉽게 사용할 수 있다.

 

styled-components를 이용해서 재사용 가능한 버튼 컴포넌트 만들기

// ./components/Button.jsx

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

const colorStyles = css`
  ${({theme, color}) => {
    const selected = theme.palette[color];
    return css`
      background: ${selected};
      &:hover {
        background: ${lighten(0.1, selected)};
      }
      &:active {
        background: ${darken(0.1, selected)}
      }
      ${props => props.outline && 
      css`
        color: ${selected};
        background: none;
        border: 1px solid ${selected};
        &:hover {
          background: ${selected};
          color: white;
        }
      `}
    `;
    /* 
    props => {
      const selected = props.theme.palette[props.color]
    }
    */
  }}
`;

const sizes = {
  large: {
    padding: '1.5rem 2rem',
    fontSize: '1.5rem'
  },
  medium: {
    padding: '1rem 1.5rem',
    fontSize: '1.25rem'
  },
  small: {
    padding: '0.7rem 1rem',
    fontSize: '1rem'
  }
};

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

const fullWidthStyle = css`
  ${props => props.fullWidth &&
  css`
    width: 100%;
    justify-content: center;
    &:not(:first-child) {
      margin-left: 0;
      margin-top: 1rem;
    }
  `}
`;

const StyledButton = styled.button`
  /* 공통 스타일 */
  display: inline-flex;
  outline: none;
  border: none;
  border-radius: 4px;
  color: white;
  font-weight: bold;
  cursor: pointer;
  padding-left: 1rem;
  padding-right: 1rem;

  /* 크기 */
  ${sizeStyles}

  /* 색상 */
  ${colorStyles}

  /* 기타 */
  &:not(:first-child) {
    margin-left: 1rem;
  }

  ${fullWidthStyle}
`;

Button.defaultProps = {
  color: 'blue',
  size: 'medium'
}

export default function Button({children, color, size, outline, fullWidth, ...rest}) {
  return <StyledButton 
    color={color} 
    size={size} 
    outline={outline} 
    fullWidth={fullWidth}
    {...rest}>
      {children}
    </StyledButton>
}

styled-components를 이용해 color, size, outline, fullWidth 적용 여부를 선택할 수 있는 스타일링 변경이 가능한 컴포넌트가 만들어졌다. (위의 예제는 '벨로퍼트와 함께 하는 모던 리액트'를 참고해서 공부했습니다.)

 

3. styled-components · GitBook

03. styled-components 이번에 배워볼 기술은 CSS in JS 라는 기술입니다. 이 문구가 뜻하는 그대로, 이 기술은 JS 안에 CSS 를 작성하는 것을 의미하는데요, 우리는 이번 튜토리얼에서 해당 기술을 사용하

react.vlpt.us

 

위의 컴포넌트를 가져다가 사용할 때는 아래와 같이 사용할 수 있다.

// App.jsx

import { useState } from 'react';
import styled, { css, ThemeProvider } from 'styled-components';
import Button from './components/Button';

const Block = styled.div`
    width: 512px;
    margin: 0 auto;
    margin-top: 4rem;
    border: 1px solid black;
    padding: 1rem;
`;

const ButtonGroup = styled.div`
    &:not(:first-child) {
        margin-top: 1rem;
    }
`;

export default function Styled() {
    const [dialog, setDialog] = useState(false);
    const onClick = () => {
        setDialog(true);
    }
    const onConfirm = () => {
        setDialog(false);
    }
    const onCancel = () => {
        setDialog(false);
    }
    return (
        <main>
            <ThemeProvider
                theme={{
                    palette: {
                        blue: '#228be6',
                        gray: '#495057',
                        pink: '#f06595'
                    }
                }}
            >
                <Block>
                    <ButtonGroup>
                        <Button size="large">BUTTON</Button>
                        <Button>BUTTON</Button>
                        <Button size="small">BUTTON</Button>
                    </ButtonGroup>
                    <ButtonGroup>
                        <Button color="gray" size="large">
                        BUTTON
                        </Button>
                        <Button color="gray">BUTTON</Button>
                        <Button color="gray" size="small">
                        BUTTON
                        </Button>
                    </ButtonGroup>
                    <ButtonGroup>
                        <Button color="pink" size="large">
                        BUTTON
                        </Button>
                        <Button color="pink">BUTTON</Button>
                        <Button color="pink" size="small">
                        BUTTON
                        </Button>
                    </ButtonGroup>
                    <ButtonGroup>
                        <Button size="large" outline>
                        BUTTON
                        </Button>
                        <Button color="gray" outline>
                        BUTTON
                        </Button>
                        <Button color="pink" size="small" outline>
                        BUTTON
                        </Button>
                    </ButtonGroup>
                    <ButtonGroup>
                        <Button size="large" fullWidth>
                        BUTTON
                        </Button>
                        <Button size="large" color="gray" fullWidth>
                        BUTTON
                        </Button>
                        <Button size="large" color="pink" fullWidth onClick={onClick}>
                        삭제
                        </Button>
                    </ButtonGroup>
                </Block>
            </ThemeProvider>
        </main>
    )
}

styled-components로 애니메이션 적용하기

styled-components를 이용하여 모달 창 애니메이션을 구현해보려고 한다.

일단, 먼저 모달 창 컴포넌트의 코드는 아래와 같다.

// ./components/Dialog.jsx

import { useEffect, useState } from 'react';
import styled, { keyframes, css } from 'styled-components';
import Button from "./Button";

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

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

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

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

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

  animation-duration: 0.25s;
  animation-timing-function: ease-out;
  animation-name: ${fadeIn};
  animation-fill-mode: forwards;

  ${props =>
    props.disappear &&
    css`
      animation-name: ${fadeOut};
    `}
`;

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

  animation-duration: 0.25s;
  animation-timing-function: ease-out;
  animation-name: ${slideUp};
  animation-fill-mode: forwards;

  ${props => 
    props.disappear &&
    css`
      animation-name: ${slideDown};
    `}
`;

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

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

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

export default function Dialog({ 
  title, 
  children, 
  confirmText, 
  cancelText, 
  onConfirm, 
  onCancel, 
  visible 
}) {
  const [animate, setAnimate] = useState(false);
  const [localVisible, setLocalVisible] = useState(visible);

  useEffect(() => {
    // visible 값이 true -> false가 되는 것을 감지
    if(localVisible && !visible) {
      setAnimate(true);
      setTimeout(() => setAnimate(false), 250);
    }
    setLocalVisible(visible);
  }, [localVisible, visible]);

  if(!animate && !localVisible) return null;
  return (
    <DarkBackground disappear={!visible}>
      <DialogBlock disappear={!visible}>
        <h3>{title}</h3>
        <p>{children}</p>
        <ButtonGroup>
          <ShortMarginButton 
          color="gray" 
          size="small"
          onClick={onCancel}>
            {cancelText}
          </ShortMarginButton>
          <ShortMarginButton 
          color="pink" 
          size="small"
          onClick={onConfirm}>
            {confirmText}
          </ShortMarginButton>
        </ButtonGroup>
      </DialogBlock>
    </DarkBackground>
  );
}

styled-components 에서 애니메이션을 적용하기 위해서는 keyframes를 import 해서 사용할 수 있다. state 값을 설정해 상태 값에 따라 모달 창이 보였다가 사라지게 구현하는 기본 원리를 두고, 거기에 localVisible 이라는 상태 값을 모달 창 컴포넌트에 내부적으로 추가해 setTimeout을 이용하여 상태값을 관리한다.

 

모달 창을 불러오는 곳에서는 아래와 같이 작성한다.

// App.jsx

import { useState } from 'react';
import styled, { ThemeProvider } from 'styled-components';
import Button from './components/Button';
import Dialog from './components/Dialog';

const Block = styled.div`
    width: 512px;
    margin: 0 auto;
    margin-top: 4rem;
    border: 1px solid black;
    padding: 1rem;
`;

const ButtonGroup = styled.div`
    &:not(:first-child) {
        margin-top: 1rem;
    }
`;

export default function Styled() {
    const [dialog, setDialog] = useState(false);
    const onClick = () => {
        setDialog(true);
    }
    const onConfirm = () => {
        setDialog(false);
    }
    const onCancel = () => {
        setDialog(false);
    }
    return (
        <main>
            <ThemeProvider
                theme={{
                    palette: {
                        blue: '#228be6',
                        gray: '#495057',
                        pink: '#f06595'
                    }
                }}
            >
                <Block>
                    <ButtonGroup>
                        <Button size="large">BUTTON</Button>
                        <Button>BUTTON</Button>
                        <Button size="small">BUTTON</Button>
                    </ButtonGroup>
                    <ButtonGroup>
                        <Button color="gray" size="large">
                        BUTTON
                        </Button>
                        <Button color="gray">BUTTON</Button>
                        <Button color="gray" size="small">
                        BUTTON
                        </Button>
                    </ButtonGroup>
                    <ButtonGroup>
                        <Button color="pink" size="large">
                        BUTTON
                        </Button>
                        <Button color="pink">BUTTON</Button>
                        <Button color="pink" size="small">
                        BUTTON
                        </Button>
                    </ButtonGroup>
                    <ButtonGroup>
                        <Button size="large" outline>
                        BUTTON
                        </Button>
                        <Button color="gray" outline>
                        BUTTON
                        </Button>
                        <Button color="pink" size="small" outline>
                        BUTTON
                        </Button>
                    </ButtonGroup>
                    <ButtonGroup>
                        <Button size="large" fullWidth>
                        BUTTON
                        </Button>
                        <Button size="large" color="gray" fullWidth>
                        BUTTON
                        </Button>
                        <Button size="large" color="pink" fullWidth onClick={onClick}>
                        삭제
                        </Button>
                    </ButtonGroup>
                </Block>
                <Dialog 
                title="정말로 삭제하시겠습니까?"
                confirmText="삭제"
                onConfirm={onConfirm}
                onCancel={onCancel}
                visible={dialog}>
                    데이터를 정말로 삭제하시겠습니까?
                </Dialog>
            </ThemeProvider>
        </main>
    )
}

 

삭제 버튼을 누르면 모달 창이 보이고, '삭제' 혹은 '취소' 버튼을 누르면 모달 창이 사라지도록 구현되었다.

 

사용 후기

프로젝트에서 공통으로 사용되는 컴포넌트가 있을 경우, CSS 파일이 분리되어 있다면 JS로 수정하기가 불편했던 점이 있었는데, styled-components를 이용해서 수정이 가능하게 되어 컴포넌트 재사용이 훨씬 용이해진 것을 느낄 수 있었다. 

728x90