[P3_S4] Styled-Components

보리·2024년 4월 15일
0

codeit-sprint

목록 보기
19/22
npm install styled-components

✨Nesting 문법

  • CSS 규칙 안에서 CSS 규칙을 만드는 것.

📘 & 선택자

& 선택자를 사용해서 앞에서 만든 버튼 컴포넌트를 호버하거나 클릭했을 때 배경색이 바뀌게👇🏻👇🏻

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=5426&directory=4-1.gif&name=4-1.gif

src/Button.js


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;
  • Nesting에서 &는 부모 선택자를 의미한다.
  • 위 코드에서는 버튼 컴포넌트의 클래스를 뜻하는 건데요.
  • 버튼 컴포넌트가 .Button이라는 클래스 이름을 쓸 때 &:hover.Button:hover와 같은 의미

.Button {
  background-color: #6750a4;
  border: none;
  color: #ffffff;
  padding: 16px;
}

.Button:hover,
.Button:active {
  background-color: #463770;
}

📘컴포넌트 선택자

Styled Components에선 클래스 이름을 쓰지 않는다. 그럼 컴포넌트 안에 있는 또 다른 컴포넌트를 선택하고 싶으면 어떻게 해야 할까?

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=5426&directory=4-2.gif&name=4-2.gif

  • 컴포넌트를 선택자로 쓰고 싶을 때는 ${Icon}같이 컴포넌트 자체를 템플릿 리터럴 안에 넣어주면 된다.

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

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

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

export default Button;

자손 결합자(Descendant Combinator)로 쓴 & ${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;
  }

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

참고로 Nesting은 여러 겹으로 할 수도 있다.


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

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

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=5426&directory=4-3.gif&name=4-3.gif

&:hover, &:active { ... } 안에 있는 ${Icon} 선택자를 CSS 코드로 표현해 보면


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

✨다이나믹 스타일링

📘${ ... } 안에 값(변수) 사용하기

  • 가장 기본적인 사용법은 JavaScript 변수를 그대로 넣는 방식
const a = 1;
const b = 2;
const str = `${a} 더하기 ${b}${a + b} 입니다.`;

아래 예시 코드에서 ${SIZES['medium']} 부분은 숫자 20을 뜻하기 때문에, font-size: ${SIZES['medium']}px;font-size: 20px;란 코드가 된다.


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

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

📘${ ... } 안에 함수 사용하기

  • Prop에 따라 스타일을 다르게 적용하는 함수를 넣기
  • 함수의 파라미터로는 Props를 받고, 리턴 값으로는 스타일 코드를 리턴하면 된다.

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

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

만약에 구조 분해(Destructuring)하면 아래처럼 쓸 수 있다.

font-size: ${({ size }) => SIZES[size]}px;

size Prop이 값이 없거나 잘못된 값이면 어떻게 될까?

Styled Components에서는 undefined 값을 빈 문자열로 처리해 주기 때문에 font-size: px 같은 잘못된 CSS 코드가 된다. 그래서 가능하면 기본 값을 정해주는 게 좋다. (널 병합 연산자 쓰기..)


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

📘논리 연산자 사용하기

함수를 사용할 때 많이 사용하는 패턴 중 하나는 논리 연산자를 사용하는 것.

예를 들어서, round라는 Prop이 참일 때 컴포넌트의 모서리를 둥글게 만듦


const Button = styled.button`
  ...
  ${({ round }) => round && `
      border-radius: 9999px;
    `}
`;

round 값이 참이면 그 뒤에 값까지 계산하기 때문에 border-radius: 9999px이라는 문자열이 리턴돼서 적용된다. 반대로, round 값이 거짓이면 그냥 false가 리턴돼서 아무런 값도 적용되지 않는다.

📘삼항 연산자 사용하기

round 가 참이면 완전히 둥근 모서리를 보여주고, 거짓이면 3px 정도로 살짝 부드럽게 깎인 모서리를 보여주고 싶다면 아래와 같이 삼항 연산자로 쓸 수 있다.


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

✨스타일 재사용: 상속

HTML 태그에 스타일링하는 건 styled.tagname을 사용해서 할 수 있다. 그런데, JSX 문법으로 직접 만든 컴포넌트나, Styled Components를 사용해 이미 만들어진 다른 컴포넌트에 스타일을 입히려면 어떻게 해야 할까? → 상속

📘styled() 함수

Styled Components로 만들어진 컴포넌트를 상속하려면 styled() 함수를 사용하면 된다.

src/Button.js


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;

src/App.js


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;

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=5431&directory=9-1.png&name=9-1.png

Button 컴포넌트의 스타일을 상속해서 새로운 버튼 SubmitButton을 만들고, App 컴포넌트 안에 SubmitButton을 배치하고 있다.

styled(Button)

SubmitButtonButton의 스타일을 상속받게 된다. Button 컴포넌트에 SubmitButton의 스타일이 상속됐기 때문에, 마찬가지로 글씨는 흰색이다.

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

styled.tagname으로 만든 컴포넌트는 바로 styled() 함수를 사용할 수 있지만, 그렇지 않은 컴포넌트는 따로 처리가 필요하다.

예시) 약관을 보여주는 TermsOfService라는 컴포넌트가 있으면

src/TermsOfService.js


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

export default TermsOfService;

TermsOfService는 JSX 문법을 직접 사용해서 바로 컴포넌트가 만들어졌다.

이 컴포넌트를 styled() 함수로 감싸면

src/App.js


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;

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=5431&directory=9-2.png&name=9-2.png

styled()로 지정한 스타일이 적용되지 않는다.

StyledTermsOfService에 지정한 배경색이랑 너비가 적용이 안 된 거 같다. 왜 그럴까?

Styled Components는 내부적으로 className을 따로 생성합니다. 그리고, 자체적으로 생성된 className이 있는 부분에 styled() 함수의 스타일이 입혀진다.

그런데, JSX 문법으로 직접 만든 컴포넌트는 styled() 함수가 적용될 className에 대한 정보가 없다. styled() 함수에서 지정한 스타일이 입혀질 부분이 어딘지 알 수 없으니 스타일이 적용되지 않은 것이다.

이렇게, Styled Components를 사용하지 않고 직접 만든 컴포넌트는 className 값을 Prop으로 따로 내려줘야 styled() 함수를 사용할 수 있다.

src/TermsOfService.js


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

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=5431&directory=9-3.png&name=9-3.png

직접 만든 컴포넌트에 className Prop을 따로 내려주는 건 syled() 함수가 적용될 부분의 className을 별도로 정해주는 거라고 이해하자.

위 코드의 경우엔, <div> 태그에 className을 내려줬기 때문에 styled(TermsOfService)에서 작성한 코드는 TermsOfService 안에 있는 <div> 태그에 적용된다.

정리하자면, 스타일 상속을 하려면 styled() 함수를 사용하면 되는데, styled.tagname으로 만든 컴포넌트는 styled() 함수로 바로 상속하면 되고, Styled Components를 사용하지 않고 직접 만든 컴포넌트에는 클래스 이름을 내려준 후에 styled() 함수로 상속해야 한다.

✨스타일 재사용: css 함수

가끔 중복되는 CSS 코드들을 변수처럼 저장해서 여러 번 다시 사용하고 싶을 때가 있다. → css 함수

Button 컴포넌트와 Input 컴포넌트에 같은 글자 크기를 갖도록 하는 상황을 생각해보자.

size라는 Prop으로 small, medium, large 각각에 지정된 크기를 전달하면 16, 20, 24 픽셀로 글자 크기를 지정하려 한다. 가장 단순한 방법은 아래처럼 똑같은 코드를 두 번 작성하는 형태가 될 것이다.


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;
`;
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라는 태그 함수를 붙여서 쓴다는 점!!!

Props를 받아서 사용하는 함수가 들어있기 때문에 반드시 css 함수를 사용해야 한다.

만약에 아래 코드처럼 함수를 삽입하지 않는 단순한 문자열이라면 일반적인 템플릿 리터럴을 써도 된다.


const boxShadow = `
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
`;

하지만, 이런 경우에도 항상 css 함수를 사용하도록 습관화하는 걸 권장 드립니다.


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

✨글로벌 스타일

CSS를 작성하다 보면, 모든 컴포넌트에 적용하고 싶은 코드가 생기는 경우가 있다. ( 폰트나 box-sizing: border-box )

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

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 코드를 넣어 준다.

✨애니메이션

📘키프레임이란?

영상과 애니메이션은 여러 개의 사진을 연속으로 보여주면서 마치 움직이는 듯한 효과를 만들어 낸다. 이때 연속으로 보여주는 한 장 한 장의 이미지를 프레임이라고 한다. 이때 '움직임의 기준이 되는 프레임'을 '키프레임'이라고 부른다.

CSS에서 키프레임은 CSS 애니메이션을 만들 때 기준이 되는 지점을 정하고, 적용할 CSS 속성을 지정하는 문법을 뜻한다.

예시) 아래 HTML/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;
}

@keyframes로 키프레임 애니메이션을 선언한 다음에, animation 속성에서 이름으로 쓰고 있다.

📘keyframes 함수

Styled Components에서는 키프레임 애니메이션을 어떻게 넣을 수 있을까?

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

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=5437&directory=15-2.gif&name=15-2.gif

플레이스홀더 애니메이션을 HTML/CSS 코드로 간단히 만들어보면 아래와 같다.


<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}처럼 템플릿 리터럴에 삽입하는 형태로 사용했다는 점!!

src/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;

src/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 객체다. 리턴되는 값이 이런 객체이기 때문에 반드시 styled 함수나 css 함수를 통해 사용해야 한다!

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

✨테마

📘ThemeProvider로 테마 설정 사용하기

테마 기능을 만들기 위해서는 현재 테마로 설정된 값을 사이트 전체에서 참조할 수 있어야 한다. React에서는 이런 상황에서 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이라는 값을 쓰면 된다. 기존에 있던 배경색 대신에 아래처럼 함수를 삽입해서 테마 값을 사용한다.

src/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;

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=5439&directory=17-2.gif&name=17-2.gif

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


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

// ...

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

💡tip

📍버튼 모양 링크가 필요할 때

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=5441&directory=19-1.png&name=19-1.png

사이트를 개발하다보면 모양은 버튼이지만 역할은 링크인 경우가 있다. 예를 들어 페이스북의 로그인 페이지에 있는 "Create new account" 버튼은 모양은 버튼이지만 "Log In"이랑 달리 <a> 태그로 되어 있고, 클릭하면 회원가입 페이지로 이동한다.

버튼의 스타일 코드는 버튼 컴포넌트에 있을텐데, 이걸 <a> 태그 버전으로도 만들어야 하는 걸까? 이렇게 반복되는 스타일링 코드를 어떻게 관리하면 좋을까?

이럴 때 간편하게 사용할 수 있는게 바로 as 라는 Prop 이다.

예를 들어서 아래와 같이 Button 이라는 컴포넌트가 <button> 태그로 만들어져 있을 때, 이걸 <a> 태그로 바꿔서 사용할 수 있다.


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

as 로 태그 이름을 내려주면 해당하는 태그로 사용할 수 있다. 굳이 버튼 모양의 링크 컴포넌트를 만들 필요가 없다!!

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

📍원치 않는 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

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;

✨태그 함수(Tagged Function)

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


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

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

console.log 로 출력하면 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의 핵심 아이디어다.

간단하게 <style> 태그를 렌더링하는 컴포넌트를 만들어 보자.


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;

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=5443&directory=21-1.png&name=21-1.png

태그 함수 안에서 컴포넌트를 만들고 이걸 리턴하는 것이다. 이 컴포넌트는 우리가 태그 함수에서 집어넣은 CSS 코드를 <style> 태그에 렌더링하는 컴포넌트다.

Styled Components에서는 내부적으로 클래스네임을 알아서 생성해주기 때문에 우리가 클래스 이름을 신경 쓸 필요가 없다.

함수를 삽입하는 예시)

단순히 stringsvalues 배열을 합쳐주는 게 아니라, React 컴포넌트의 Props를 활용하는 함수가 삽입되는 경우를 처리할 것임.


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;

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개의 댓글