본문 바로가기

Style/StyledComponents

React - Styled Components

import styled from 'styled-components';

const Button = styled.button`
  background-color: #ededed;
  border: none;
  border-radius: 8px;
`;

function App() {
  return (
    <div>
      <h1>안녕 Styled Components!</h1>
      <Button>확인</Button>
    </div>
  );
}

export default App;

css코드로 컴포넌트를 만들고있다.

 

기존방식의 문제점인

  1. 클래스 이름이 겹치는 경우, 의도하지 않은 스타일이 설정될 수 있다.
  2. 재사용하는 CSS 코드를 관리하기 어렵다.

을 보완할 수 있는 방식이다.

 

설치

npm install styled-components

 

이 글에서는 styled-components 버전5를 기준으로 다룬다.

 

styled-component를 사용하는 방법

import styled from 'styled-components';

const Button = styled.button`
  background-color: #6750a4;
  border: none;
  color: #ffffff;
  padding: 16px;
`;

export default Button;
import Button from './Button';

function App() {
  return (
    <div>
      <Button>Hello Styled!</Button>
    </div>
  );
}

export default App;
  1. styled-components의 default import로 'styled'를 가져온다. 대부분의 작업은 이 'styled'함수를 사용할 것이다.
  2. 변수(Button)에 styled.tagname 을 적고, 그 뒤에는 템플릿 리터럴로 CSS코드를 적는다. 이러한 형식을 태그함수라고 부른다.
  3. styled.tagname으로 만든 컴포넌트는 일반적인 React Component처럼 JSX에서 사용할 수 있다.

nesting 문법

CSS규칙 안에서 CSS규칙을 만드는 것을 말한다.

&선택자, 컴포넌트 선택자로 Nesting을 활용할 수 있다.

 

&선택자

&선택자를 사용해서 앞에서 만든 버튼 컴포넌트를 호버하거나 클릭했을 때, 배경색이 바뀌는 예시는 아래와 같다.

import styled from 'styled-components';

const Button = styled.button`
  background-color: #6750a4;
  border: none;
  color: #ffffff;
  padding: 16px;

  &:hover,
  &:active {
    background-color: #463770;
  }
`;

export default Button;
  • 위 코드에서 &선택자를 사용해 작성된 부분을 css로 작성해보면 아래와 같다.
.Button:hover,
.Button:active {
  background-color: #463770;
}

 

컴포넌트 선택자

import styled from 'styled-components';
import nailImg from './nail.png';

const Icon = styled.img`
  width: 16px;
  height: 16px;
`;

const StyledButton = styled.button`
  background-color: #6750a4;
  border: none;
  color: #ffffff;
  padding: 16px;

  & ${Icon} {
    margin-right: 4px;
  }
`;

function Button({ children, ...buttonProps }) {
  return (
    <StyledButton {...buttonProps}>
      <Icon src={nailImg} alt="nail icon" />
      {children}
    </StyledButton>
  );
}

export default Button;
  • " ${Icon} " 부분이 컴포넌트 선택자이다.
  • 이 부분은 css로 작성하면 아래와 같다.
.StyledButton {
  ...
}

.StyledButton .Icon {
  margin-right: 4px;
}
  • 위 코드에선 "&" 와, 컴포넌트 선택자"${Icon}" 사이에 공백을 줌으로 자손결합자로 쓰인것을 볼 수 있는데, 자손결합자로 쓰인 경우에는 "&"를 생략하고 아래와같이 작성할 수 있다. (이 방법을 권장!)
 const StyledButton = styled.button`
  background-color: #6750a4;
  border: none;
  color: #ffffff;
  padding: 16px;

  ${Icon} {
    margin-right: 4px;
  }
`;

 

 

nesting은 여러겹으로 할 수 있다.

const StyledButton = styled.button`
  ...
  &:hover,
  &:active {
    background-color: #7760b4;

    ${Icon} {
      opacity: 0.2;
    }
  }
`;

위 코드는 아래와 같은 의미를 가진다.

.StyledButton:hover .Icon,
.StyledButton:active .Icon {
  opacity: 0.5;
}

다이나믹 스타일링

import styled from 'styled-components';

const SIZES = {
  large: 24,
  medium: 20,
  small: 16,
};

const Button = styled.button`
  background-color: #6750a4;
  border: none;
  border-radius: ${({ round }) => round ? `9999px` : `3px`};
  color: #ffffff;
  font-size: ${({ size }) => SIZES[size] ?? SIZES['medium']}px;
  padding: 16px;

  &:hover,
  &:active {
    background-color: #463770;
  }
`;

export default Button;
import styled from 'styled-components';
import Button from './Button';

const Container = styled.div`
  ${Button} {
    margin: 10px;
  }
`;

function App() {
  return (
    <Container>
      <h1>기본 버튼</h1>
      <Button size="small">small</Button>
      <Button size="medium">medium</Button>
      <Button size="large">large</Button>
      <h1>둥근 버튼</h1>
      <Button size="small" round>
        round small
      </Button>
      <Button size="medium" round>
        round medium
      </Button>
      <Button size="large" round>
        round large
      </Button>
    </Container>
  );
}

export default App;
  • styled로 만든 컴포넌트도 위와같이 prop을 받을 수 있다.
  • 컴포넌트를 불러올 때, prop을 보냈으니, 해당 컴포넌트에서는 그 prop을 사용하여 스타을 다르게 할 수 있다.
  • 모든 styled컴포넌트들은 파라미터로 모든 props를 받아오는데, 위에서는 destructuring문법을 사용해 필요한 것만 받아온 것이다.
  • 모두 받아오는 형식은 아래와같이 작성한다.
import styled from 'styled-components';

const SIZES = {
  large: 24,
  medium: 20,
  small: 16,
};

const Button = styled.button`
  background-color: #6750a4;
  border: none;
  border-radius: ${(props) => props.round ? `9999px` : `3px`};
  color: #ffffff;
  font-size: ${(props) => SIZES[props.size] ?? SIZES['medium']}px;
  padding: 16px;

  &:hover,
  &:active {
    background-color: #463770;
  }
`;

export default Button;
  • ${...} : 표현식 삽입.
  • 표현식 삽입법 안에는 JavaScript코드인 변수나 함수를 집어넣을 수 있다.
  • prop에 따라 스타일을 다르게 적용하려면 함수를 집어넣어야한다.
  • 위 코드에서 'font-size'를 다루는 부분에서는 함수의 결과가 undefined면 에러가 발생할 것이다. 따라서 널병합연산자를 통해 널처리를 해주었다.

스타일 재사용 (상속)

styled components로 만들어진 컴포넌트에 스타일을 입히고 싶을 때에는 styled()함수를 사용한다.

import styled from 'styled-components';

const SIZES = {
  large: 24,
  medium: 20,
  small: 16,
};

const Button = styled.button`
  background-color: #6750a4;
  border: none;
  color: #ffffff;
  font-size: ${({ size }) => SIZES[size] ?? SIZES['medium']}px;
  padding: 16px;

  ${({ round }) =>
    round
      ? `
      border-radius: 9999px;
    `
      : `
      border-radius: 3px;
    `}

  &:hover,
  &:active {
    background-color: #463770;
  }
`;

export default Button;
import styled from 'styled-components';
import Button from './Button';

const SubmitButton = styled(Button)`
  background-color: #de117d;
  display: block;
  margin: 0 auto;
  width: 200px;

  &:hover {
    background-color: #f5070f;
  }
`;

function App() {
  return (
    <div>
      <SubmitButton>계속하기</SubmitButton>
    </div>
  );
}

export default App;

위 코드를 보면, Button컴포넌트를 상속해서 SubmitButton을 만든 것을 확인할 수 있다.

 

JSX로 직접 만든 컴포넌트에 styled() 사용하기

function TermsOfService({ className }) {
  return (
    <div className={className}>
      <h1>㈜코드잇 서비스 이용약관</h1>
      <p>
        환영합니다.
        <br />
        Codeit이 제공하는 서비스를 이용해주셔서 감사합니다. 서비스를
        이용하시거나 회원으로 가입하실 경우 본 약관에 동의하시게 되므로, 잠시
        시간을 내셔서 주의 깊게 살펴봐 주시기 바랍니다.
      </p>
      <h2>제 1 조 (목적)</h2>
      <p>
        본 약관은 ㈜코드잇이 운영하는 기밀문서 관리 프로그램인 Codeit에서
        제공하는 서비스를 이용함에 있어 이용자의 권리, 의무 및 책임사항을
        규정함을 목적으로 합니다.
      </p>
    </div>
  );
}

export default TermsOfService;
import styled from 'styled-components';
import Button from './Button';
import TermsOfService from './TermsOfService';

const StyledTermsOfService = styled(TermsOfService)`
  background-color: #ededed;
  border-radius: 8px;
  padding: 16px;
  margin: 40px auto;
  width: 400px;
`;

const SubmitButton = styled(Button)`
  background-color: #de117d;
  display: block;
  margin: 0 auto;
  width: 200px;

  &:hover {
    background-color: #f5070f;
  }
`;

function App() {
  return (
    <div>
      <StyledTermsOfService />
      <SubmitButton>계속하기</SubmitButton>
    </div>
  );
}

export default App;

 

 

  • styled()로 만든 컴포넌트가 아닌 react 컴포넌트에 스타일을 입히기위해서는 위와같이 처리가 필요하다.
  • styled()함수의 파라미터로 해당 컴포넌트를 넣어주는 부분은 똑같다.
  • 하지만 JSX컴포넌트에서 파라미터로 ClassName을 받아주고, 내부의 태그에 className을 적용시켜주는 과정이 필요하다.
  • styled컴포넌트는 내부적으로 알아서 className을 만들고, 그 className으로 스타일을 적용시키는 방식으로 돌아가기때문에 jsx로 만든 컴포넌트는 className이 없기때문에 위와같은 작업이 필요한 것이다.
  • 이 작업이 없다면 className이 없으니까 JSX컴포넌트에 스타일이 입혀질 수 없다.

스타일 재사용(CSS함수)

  • 중복되는 CSS코드들을 변수처럼 저장해서 여러번 다시 사용하고 싶을 때 사용할 수 있다.
  • 기존에는 아래와같이 같은 코드를 두 번 작성해야했다.
import styled from 'styled-components';

const SIZES = {
  large: 24,
  medium: 20,
  small: 16
};

const Button = styled.button`
  font-size: ${({ size }) => SIZES[size] ?? SIZES['medium']}px;
`;

const Input = styled.input`
  font-size: ${({ size }) => SIZES[size] ?? SIZES['medium']}px;
`;
  • 하지만 css함수를 사용하면 아래와같이 수정할 수 있다.
import styled, { css } from 'styled-components';

const SIZES = {
  large: 24,
  medium: 20,
  small: 16
};

const fontSize = css`
  font-size: ${({ size }) => SIZES[size] ?? SIZES['medium']}px;
`;

const Button = styled.button`
  ${fontSize}
`;

const Input = styled.input`
  ${fontSize}
`;
  • css`css코드` 형태로 작성된 것을 변수에 담아두었다가, 실제 컴포넌트를 만들 때 내부에 이 변수를 넣어주면된다.
  • 위 코드에서는 size라는 prop을 받아서 prop에 따라 각각 다른 값을 내려주는 함수를 가지고 있는데, 이러한 함수가 없는 경우에는 'css'라는 함수명을 생략해도되지만 권장하지는 않는다.
// 권장하지않음.
const boxShadow = `
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
`;

// 권장.
const boxShadow = css`
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
`;

글로벌 스타일

createGlobalStyle 함수를 사용해서 만들 수 있다.

글로벌 스타일컴포넌트를 최상위 컴포넌트에서 렌더링하면, 글로벌스타일이 항상 적용된 상태가 되도록 할 수 있다.

예를들면, 아래는 사이트 전체의 폰트와 box-sizing을 지정하는 코드이다.

import { createGlobalStyle } from 'styled-components';

const GlobalStyle = createGlobalStyle`
  * {
    box-sizing: border-box;
  }

  body {
    font-family: 'Noto Sans KR', sans-serif;
  }
`;

function App() {
  return (
    <>
      <GlobalStyle />
      <div>글로벌 스타일</div>
    </>
  );
}

export default App;
  • 이 함수는 <style>태그를 컴포넌트로 만든다. 그렇다고 <GlobalStyle/> 부분에 스타일태그가 생기는 것은 아니고, Styled Component가 내부적으로 처리해서, head태그안에 우리가 작성한 css코드를 넣어준다.

애니메이션

keryframes : CSS 애니메이션을 만들 때 기준이 되는 지정을 정하고, 적용할 CSS속성을 지정하는 문법을 뜻한다.

아래의 코드는 .ball이란 div태그를 위아래로 움직이는 애니메이션효과를 주는 코드이다.

// js
<div class="ball"></div>

// css
@keyframes bounce {
  0% {
    transform: translateY(0%);
  }

  50% {
    transform: translateY(100%);
  }

  100% {
    transform: translateY(0%);
  }
}

.ball {
  animation: bounce 1s infinite;
  background-color: #ff0000;
  border-radius: 50%;
  height: 50px;
  width: 50px;
}

맨 마지막에 .ball에대한 css규칙을 보면, animation prop에 bounce라는, keyframes로 만든 변수를 쓰고있다.

애니메이션은 이런식으로만들어진다.

 

아래에서는 placeholder 애니메이션으로 차이점을 알아볼 수 있다.

기존방식

<div class="placeholder">
  <div class="placeholder-item a"></div>
  <div class="placeholder-item b"></div>
  <div class="placeholder-item c"></div>
</div>

@keyframes placeholder-glow {
  50% {
    opacity: 0.2;
  }
}

.placeholder {
  animation: placeholder-glow 2s ease-in-out infinite;
}

.placeholder-item {
  background-color: #888888;
  height: 20px;
  margin: 8px 0;
}

.a {
  width: 60px;
  height: 60px;
  border-radius: 50%;
}

.b {
  width: 400px;
}

.c {
  width: 200px;
}

styled컴포넌트에서의 방식

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

const placeholderGlow = keyframes`
  50% {
    opacity: 0.2;
  }
`;

export const PlaceholderItem = styled.div`
  background-color: #888888;
  height: 20px;
  margin: 8px 0;
`;

const Placeholder = styled.div`
  animation: ${placeholderGlow} 2s ease-in-out infinite;
`;

export default Placeholder;
import styled from 'styled-components';
import Placeholder, { PlaceholderItem } from './Placeholder';

const A = styled(PlaceholderItem)`
  width: 60px;
  height: 60px;
  border-radius: 50%;
`;

const B = styled(PlaceholderItem)`
  width: 400px;
`;

const C = styled(PlaceholderItem)`
  width: 200px;
`;

function App() {
  return (
    <div>
      <Placeholder>
        <A />
        <B />
        <C />
      </Placeholder>
    </div>
  );
}

export default App;
  • keyframes라는 함수를 사용해서 'placeholderGlow' 변수를 만들었다.
  • 이 변수는 placeholder컴포넌트의 animation prop으로 넘겨지고있다.
  • 따라서 placeholder 컴포넌트의 하위에 작성된 A, B, C는 애니메이션의 영향을 받는다.

 

  • keyframes함수가 리턴하는 변수는 단순한 문자열이 아니라 javaScript객체이다.
  • 크롬 개발자도구로 살펴보면 아래와같이 id, css규칙등의 내용이 값으로 들어가있다.
  • 리턴값이 객체기때문에 반드시 styled함수나 css함수를 통해 사용해야한다.

테마

  • light 모드, dark 모드 등을 선택할 수 있는것을 여러 웹사이트에서 찾아볼 수 있다.
  • 글자크기나 색 등의 css규칙을 모아놓은 것을 테마라고 부른다.
  • styled components에서는 'ThemeProvider'로 테마를 설정할 수 있다.
  • 테마기능을 만들기위해서는 현재 테마로 설정된 값을 사이트 전체에서 참조할 수 있어야한다.
  • React에서는 이렇게 전체 컴포넌트에서 접근할 수 있는 것은 context로 관리한다.
  • styled components에서도 context를 기반으로 테마를 사용할 수 있다.
  • ThemeProvider가 context를 내려주는 컴포넌트이다.
const Button = styled.button`
  background-color: ${({ theme }) => theme.primaryColor};
import { ThemeProvider } from "styled-components";
import Button from "./Button";

function App() {
  const theme = {
    primaryColor: '#1da1f2',
  };

  return (
    <ThemeProvider theme={theme}>
      <Button>확인</Button>
    </ThemeProvider>
  );
}

export default App;

위에서 ThemeProvider컴포넌트에 theme 이라는 prop을 내려주고있다.

Button컴포넌트에서는 theme을 받아서 background-color를 '#1da1f2' 로 설정해준다.

 

아래는 여러개의 테마를 선택할 수 있는 코드이다. useState를 활용한다.

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

function App() {
  const [theme, setTheme] = useState({
    primaryColor: '#1da1f2',
  });

  const handleColorChange = (e) => {
    setTheme((prevTheme) => ({
      ...prevTheme,
      primaryColor: e.target.value,
    }));
  };

  return (
    <ThemeProvider theme={theme}>
      <select value={theme.primaryColor} onChange={handleColorChange}>
        <option value="#1da1f2">blue</option>
        <option value="#ffa800">yellow</option>
        <option value="#f5005c">red</option>
      </select>
      <br />
      <br />
      <Button>확인</Button>
    </ThemeProvider>
  );
}

export default App;

select 요소가 바뀔 때 마다, setTheme함수를 실행해서 primaryColor가 바뀌고, 그 값은 Button컴포넌트로 전달된다.

 

 

만약, 테마 설정페이지를 만든다고하면, 테마값을 일반적인 컴포넌트에서 참조할 필요도 생기는데, 그럴때에는 ThemeContext를 불러오면 된다. 

이 값은 React Context이기때문에 useContext도 필요하다.

import { useContext } from 'react';
import { ThemeContext } from 'styled-components';

// ...

function SettingPage() {
  const theme = useContext(ThemeContext); // { primaryColor: '#...' }
}

 

아래는 light모드와 dark모드가 있는 예시이다.

import styled, { keyframes } from 'styled-components';
import spinnerImg from './spinner.png';

const SIZES = {
  large: 24,
  medium: 20,
  small: 16,
};

const StyledInput = styled.input`
  font-size: ${({ size }) => SIZES[size] ?? SIZES['medium']}px;
  border: 2px solid ${({ error }) => (error ? `#f44336` : `#eeeeee`)};
  border-radius: ${({ round }) => (round ? `9999px` : `4px`)};
  background-color: ${({theme})=> theme.backgroundColor};
  color: ${({theme})=> theme.color};
  outline: none;
  padding: 16px;
  position: relative;
`;
...

function Input({ loading, ...inputProps }) {
  return (
    <Container>
      <StyledInput {...inputProps} />
      {loading && <Spinner />}
    </Container>
  );
}

export default Input;
import { useState } from 'react';
import { createGlobalStyle, ThemeProvider } from 'styled-components';
import Input from './Input';

const THEMES = {
  light: {
    backgroundColor: '#ffffff',
    color: '#000000',
  },
  dark: {
    backgroundColor: '#121212',
    color: '#ffffff',
  },
};

const GlobalStyle = createGlobalStyle`
  body {
    background-color: ${({theme})=> theme.backgroundColor};
    color: ${({theme})=> theme.color}
  }
`;


function App() {
  const [theme, setTheme] = useState(THEMES['light']);

  const handleSelectChange = (e) => {
    const nextThemeName = e.target.value;
    setTheme(THEMES[nextThemeName]);
  };

  return (
    <ThemeProvider theme={theme}>
      <div>
        <select onChange={handleSelectChange}>
          <option value="light">라이트 모드</option>
          <option value="dark">다크 모드</option>
        </select>
        <GlobalStyle />
        <Input />
      </div>
    </ThemeProvider>
  );
}

export default App;

일단 전체적인 배경색, 글자색은 GlobalStyle을 통해서 지정해준다.

그리고 Input의 배경색과 글자색은 Input 컴포넌트가 받아서 처리했다.


상황별 팁

1. 모양은 버튼이지만 기능은 링크인 컴포넌트가 필요할때

버튼태그의 style은 정의되어있지만 a태그의 style은 없다. 따로 만들어줘야할 것 같지만 아래와같이 'as' prop으로 처리할 수 있다.

const Button = styled.button`
  /* ... */
`;
<Button href="https://example.com" as="a">
  LinkButton
</Button>

위 코드는 버튼스타일이 입혀질것인데, as prop으로 a 가 전달되니, 그 기능은 a태그와 같다. 따라서 href 속성을 주어도 잘 기능한다.

 

2. 원하지 않는 깊이까지 prop이 전달될 때

  • transient prop을 사용할 것이다.
  • 아래코드는 Link컴포넌트를 상속하는 StyledLink에 underline 이라는 prop을 주고있다.
  • 이 underline prop은 styledLink에서만 사용하는 것을 알 수 있다. Link를 상속하고있기때문에 Link에도 ...props 로 underline이 전달될것이다.
  • Link컴포넌트에서는 prop을 a태그안에 넣고있는데, a태그에는 underline이라는 prop을 사용하지않기때문에 에러가 발생한다.
// 에러가 발생하는 코드이다.
import styled from 'styled-components';

function Link({ className, children, ...props }) {
  return (
    <a {...props} className={className}>
      {children}
    </a>
  );
};

const StyledLink = styled(Link)`
  text-decoration: ${({ underline }) => underline ? `underline` : `none`};
`;

function App() {
  return (
    <StyledLink underline={false} href="https://codeit.kr">
      Codeit으로 가기
    </StyledLink>
  );
}

export default App;

Link컴포넌트에 underline prop이 전달되기까지의 과정은 아래와 같다.

  1. StyledLink 컴포넌트에서 underline prop을 받는다.
  2. StyledLink 컴포넌트가 스타일링하고있는 Link컴포넌트에 underline prop이 전달된다.
  3. Link컴포넌트에 Spread문법을 통해 a태그에 underline이 전달된다.

그렇다면 a태그에 underline 속성을 주지않기위해 아래와같이 수정할 수 있겠다.

function Link({ className, children, underline, ...props }) {
  return (
    <a {...props} className={className}>
      {children}
    </a>
  );
};
  • 하지만 근본적으로 사용하지 않을 underline prop은 Link까지 전달되지 않는것이 맞다.
  • TransientProp은 syled components로 스타일링하는 컴포넌트에서만 사용되고, 스타일링의 대상이되는 컴포넌트까지는 prop이 전달되지 않도록 할 수 있다.
  • transient : 일시적인, 순간적인

 

아래코드는 transientProp을 사용한 예시이다.

import styled from 'styled-components';

function Link({ className, children, ...props }) {
  return (
    <a {...props} className={className}>
      {children}
    </a>
  );
};

const StyledLink = styled(Link)`
  text-decoration: ${({ $underline }) => $underline ? `underline` : `none`};
`;

function App() {
  return (
    <StyledLink $underline={false} href="https://codeit.kr">
      Codeit으로 가기
    </StyledLink>
  );
}

export default App;

transientProp은 앞에 "$" 를 붙여서 만들 수 있다.