Styled Components (2)

깨진알·2024년 1월 3일

React

목록 보기
11/12

Styled Components 심화

1. 글로벌 스타일

* {
  box-sizing: border-box;
}

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

CSS를 작성하다 보면, 모든 컴포넌트에 적용하고 싶은 코드가 생기는 경우가 있다. 이런 코드를 css 함수를 사용해서 변수로 만들고 사용할 수도 있다. 하지만 모든 컴포넌트에 일일이 값을 넣어주는 건 불필요한 작업일 수 있다. 이럴 땐 글로벌 스타일 컴포넌트를 사용하면 된다. 글로벌 스타일 컴포넌트를 최상위 컴포넌트에서 렌더링하면 글로벌 스타일이 항상 적용된 상태가 되도록 할 수 있다.

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;

createGlobalStyle이라는 함수는 다른 Styled Components 함수들과 마찬가지로 템플릿 리터럴 문법으로 사용한다. 이 함수는 <style> 태그를 컴포넌트로 만든다. 실제로 <style> 태그가 저 위치에 생기는 건 아니고, Styled Components가 내부적으로 처리해서 <head> 태그 안에 우리가 작성한 CSS 코드를 넣어준다.


2. 애니메이션

(1) 키프레임 (keyframe)

영상과 애니메이션은 여러 개의 사진을 연속으로 보여주면서 마치 움직이는 듯한 효과를 만들어 낸다. 이때 연속으로 보여주는 한 장 한 장의 이미지를 프레임이라고 한다. 과거에는 애니메이션을 만들 때 프레임 각각을 모두 만들었지만, 요즘에는 움직임의 기준이 되는 프레임만 만들고 그 사이의 프레임들을 자동으로 채워 넣는 방식을 주로 사용한다. 이때 '움직임의 기준이 되는 프레임'을 '키프레임'일고 부른다.

CSS에서 키프레임은 CSS 애니메이션을 만들 때 기준이 되는 지점을 정하고, 적용할 CSS 속성을 지정하는 문법을 의미한다. 예를 들어, .ball이라는 <div> 태그를 위아래로 움직이는 애니메이션을 만든다고 가정하자. 시작 지점에서는 기본값인 translateY(0$)를 적용하고, 애니메이션의 중간 시점에서는 translateY(100%)를 적용한 다음, 마지막에는 기본값인 translateY(0%)을 적용한다.

<div class="ball"></div>
@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;
}

(2) keyframes 함수

플레이스홀더 애니메이션은 사이트에서 보여줄 내용을 로딩하는 동안 내용이 들어갈 자리에 미리 네모나 동그라미 같은 걸 보여주면서, 애니메이션으로 로딩 중이라는 걸 보여주는 걸 말한다.

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

여기서 placeholder-glow라는 애니메이션 코드는 애니메이션의 중간인 50% 시점에 0.2의 불투명도로 만드는 것이다. 불투명도의 기본값이 1이니까, 불투명도가 0.2로 낮아졌다가 다시 1로 높아지는 애니메이션이다.

이 코드를 Styled Components 버전으로 작성해 보도록 하겠다. @keyframeskeyframes라는 함수를 쓰면 된다. styled 함수와 마찬가지로 템플릿 리터럴로 사용하는 태그 함수이다. 여기서 특히 keyframes로 만든 애니메이션을 ${placeholderGlow}처럼 템플릿 리터럴에 삽입하는 형태로 사용했다.

// Placeholder.js
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;

// App.js
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 함수가 리턴하는 변수는 단순한 문자열이 아니라 JavaScript 객체이다. 크롬 개발자 도구로 살펴보면 아래처럼 id, 이름, 작성한 CSS 규칙의 내용 등이 값으로 들어가 있다. 리턴되는 값이 이런 객체이기 때문에 반드시 styled 함수나 css 함수를 통해 사용해야 한다.

{
    id: "sc-keyframes-bEnYbJ"
    inject: ƒ (e, t)
    name: "bEnYbJ"
    rules: "\n  50% {\n    opacity: 0.2;\n  }\n"
    toString: ƒ ()
}

3. 테마

요즘 스마트폰 앱이나 웹 서비스들에서는 대부분 라이트 모드와 다크 모드라는 걸 지원한다. 이런 식으로 사용자가 보는 화면의 색상, 글자 크기, 글자 색 등을 모아 놓은 걸 '테마'라고 한다. Styled Components에서는 테마를 쉽게 만들 수 있는 기능이 있다.

(1) ThemeProvider로 테마 설정 사용하기

테마 기능을 만들기 위해서는 현재 테마로 설정된 값을 사이트 전체에서 참조할 수 있어야 한다. 리액트에서는 이런 상황에서 Context라는 걸 사용한다. Styled Components에서도 Context를 기반으로 테마를 사용할 수 있다. Context를 내려주는 컴포넌트로 ThemeProvider라는 걸 사용한다.

// App.js
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라는 Context Provider를 사용해서 theme이라는 객체를 내려준다. 이렇게 하면 ThemeProvider 안에 있는 Styled Components로 만든 컴포넌트에서는 Props를 사용하듯이 theme이라는 객체를 쓸 수 있다.

예를 들어 Button 컴포넌트에서 theme 값을 사용해 보도록 하겠다. Prop 값을 사용하듯이 theme이라는 값을 쓰면 된다. 기존에 있던 배경색 대신에 아래처럼 함수를 삽입해서 테마 값을 사용한다.

// Button.js
const Button = styled.button`
  background-color: ${({ theme }) => theme.primaryColor};
  /* ... */
`;

여러 테마를 선택하게 하고 싶다면 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;

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

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

// ...

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

4. 상황별 유용한 팁

(1) 버튼 모양 링크가 필요할 때

사이트를 개발하다보면 모양은 버튼이지만 역할은 링크인 경우가 많다. 이럴 때 간편하게 사용할 수 있는게 바로 as라는 Prop이다. 예를 들어 아래와 같이 Button이라는 컴포넌트가 <button> 태그로 만들어져 있을 때, 이걸 <a> 태그로 바꿔 사용할 수 있다.

const Button = styled.button`
  /* ... */
`;

as로 태그 이름을 내려주면 해당하는 태그로 사용할 수 있다.

<Button href="https://example.com" as="a">
  LinkButton
</Button>

(2) 원치 않은 Props가 전달될 때

Prop을 Spread 문법을 사용해서 <a> 태그로 전달하는 Link 컴포넌트가 있다고 가정해보자. StyledLink라는 걸 만들어서 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;
react-dom.development.js:86 Warning: Received `false` for a non-boolean attribute `underline`.

If you want to write it to the DOM, pass a string instead: underline="false" or underline={value.toString()}.

If you used to conditionally omit it with underline={condition && value}, pass underline={condition ? value : undefined} instead.
    at a
    at Link (http://localhost:3000/static/js/bundle.js:26:5)
    at O (http://localhost:3000/static/js/bundle.js:44495:6)
    at App

오류가 발생한다. 이 오류는 React에서 알려주는 오류이다. HTML 태그에 underline이라는 속성을 지정했는데, 그 속성의 값이 문자열이 아니라서 생긴 오류이다. <a> 태그에는 underline이라는 속성이 없다. 이 문제의 근본적인 원인은 <a {...props} className={className}> 이 부분이다. Spread를 하는 과정에서 의도하지 않은 underline Prop까지 내려간 것이 원인이다.

underline Prop이 전달되는 순서는 아래와 같다.

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

이럴 때는 구조 분해 코드를 고쳐 underline을 제외하면 원치 않는 Prop이 전달되는 걸 막을 수 있다.

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

그런데 생각해보면 underline이라는 Prop은 Link에서 쓰려고 만든 게 아니라 StyledLink 컴포넌트에서만 쓰려고 만든 건데, Link에 Prop으로 전달되는게 좀 더 근본적인 문제가 될 수 있다. 이럴 때 아예 Prop이 전달되지 않게 만드는 방법이 있다. 바로 Transient Prop이라는 걸 사용하면 된다.

Transient Prop을 사용하면 Styled Components로 스타일링하는 컴포넌트에서만 Prop을 사용하고, 스타일리의 대상이 되는 컴포넌트에는 Prop이 전달되지 않도록 할 수 있다.

StyledLink 컴포넌트 안에서만 Prop을 사용하고 Link에는 전달하지 않는 예시이다. Transient Prop을 만들려면 앞에다 $ 기호를 붙여 주면 된다. $underline Prop은 StyledLink 안에서만 사용되고, Link로 전달되지는 않는다.

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;

5. Styled Components 파헤치기

(1) 태그 함수 (Tagged Function)

태그 함수(Tagged Function)는 템를릿 리터럴 문법을 사용해서 실행할 수 있는 함수이다.

function h1(strings, ...values) {
  return [strings, values];
}
const result = h1`color: pink;`;
console.log(result); // [['color: pink;'], []]

h1이라는 함수는 첫 번째 파라미터로 strings, 그리고 나머지 파라미터들을 values 배열로 받는다. 이렇게 일반적인 형태로 함수를 선언하고, 템를릿 리터럴로 실행하면 특정한 형태로 파라미터가 전달된다.

function h1(strings, ...values) {
  return [strings, values];
}
const backgroundColor = 'black';
const result2 = h1`
  background-color: ${backgroundColor};
  color: pink;
`;
console.log(result2);
// [['\n  background-color: ', ';\n  color: pink;\n'], ['black']]

strings에는 값이 삽입되는 부분 앞뒤의 문자열들이 잘려서 배열로 들어가 있고, values에는 삽입된 값들이 배열로 들어가 있다. 이것이 태그 함수의 기본적인 동작이다. 이걸 사용해서 CSS 스타일이 생성된 리액트 컴포넌트를 만드는 것이 Styled Components의 핵심 아이디어이다.

function h1(strings, ...values) {
  // React 컴포넌트를 만든다
  function Component({ children }) {
    // 템플릿 리터럴에서 받은 값을 CSS 코드로 만든다
    let style = '';
    for (let i = 0; i < strings.length; ++i) {
      style += strings[i];
      if (values[i]) {
        style += values[i];
      }
    }

    // CSS 코드에 따라 클래스 이름을 만든다
    const className = `my-sc-${style.length}`;

    // `<style>` 태그로 만든 CSS 코드를 렌더링한다
    return (
      <>
        <style>{`.${className} {${style}}`}</style>
        <h1 className={className}>{children}</h1>
      </>
    );
  }
  return Component;
}

const backgroundColor = 'black';
const StyledH1 = h1`
  background-color: ${backgroundColor};
  color: pink;
`;

function App() {
  return <StyledH1>Hello World</StyledH1>;
}

export default App;

Component라는 부분이 헷갈릴 수 있는데, 컴포넌트 함수를 선언하는 부분이라고 생각하면 된다. 태그 함수 안에서 컴포넌트를 만들고 이걸 리턴하는 것이다. 이 컴포넌트는 태그 함수에서 집어넣은 CSS 코드를 <style> 태그에 렌더링하는 컴포넌트이다.

내부적으로 사용할 클래스 이름도 만들었다. 단순하게 CSS 코드 길이를 가지고 클래스 이름을 생성했다. (my-sc${style.lengtyh} 부분) Styled Components에서는 내부적으로 클래스 네임을 알아서 생성해주기 때문에 클래스 이름을 신경 쓸 필요가 없다.

function h1(strings, ...values) {
  // React 컴포넌트를 만든다
  function Component({ children, ...props }) {
    // 템플릿 리터럴에서 받은 값을 CSS 코드로 만든다
    let style = '';
    for (let i = 0; i < strings.length; ++i) {
      style += strings[i];
      // 삽입된 값이 함수이면 props를 가지고 실행한 값을 CSS에 넣는다.
      if (typeof values[i] === 'function') {
        const fn = values[i];
        style += fn(props);

        // 그 외에 값이 존재하면 CSS에 문자열로 넣는다.
      } else if (values[i]) {
        style += values[i];
      }
    }

    // CSS 코드에 따라 클래스 이름을 만든다
    const className = `my-sc-${style.length}`;

    // `<style>` 태그로 만든 CSS 코드를 렌더링한다
    return (
      <>
        <style>{`.${className} {${style}}`}</style>
        <h1 className={className}>{children}</h1>
      </>
    );
  }
  return Component;
}

const backgroundColor = 'black';
const StyledH1 = h1`
  color: pink;
  ${({ dark }) => dark && 'background-color: black;'}
`;

function App() {
  return <StyledH1 dark>Hello World</StyledH1>;
}

export default App;

함수를 삽입하는 코드이다. 단순히 stringsvalues 배열을 합쳐주는게 아니라, 리액트 컴포넌트의 Props를 활용하는 함수가 삽입되는 경우를 처리한다.

Component 함수 안에서 CSS 코드를 생성하는 부분에 함수를 처리하는 부분을 추가했다. 위 코드가 실행되는 순서는 다음과 같다.

  1. 템플릿 리터럴로 태그 함수 h1을 실행해서, StyledH1이라는 컴포넌트가 만들어진다.
  2. App 컴포넌트를 렌더링하면 StyledH1 컴포넌트도 렌더링한다.
  3. StyledH1 컴포넌트에서는 CSS 코드를 생성해서 <style> 태그로 넣는다. 이때 함수로 삽입된 값(${({ dark }) => dark && 'background-color: black;'} 부분)은 함수이기 때문에, Props를 가지고 실행해서 CSS로 만든다. dark라는 값이 있기 때문에, CSS에는 background-color: black;이라는 값으로 반영된다.
profile
프론트엔드 지식으로 가득찰 때까지

0개의 댓글