Styled Components

Jeris·2023년 4월 28일
0

코드잇 부트캠프 0기

목록 보기
69/107

1. Styled Components 시작하기

Styled Componets란?

  • Styled Components는 React에서 사용되는 CSS-in-JS 라이브러리로, 컴포넌트 스타일링을 더욱 쉽고 간편하게 만들어주는 도구입니다.
  • Styled Components를 사용하면 컴포넌트와 스타일을 하나의 파일에서 정의할 수 있습니다. 이는 컴포넌트와 스타일을 분리하지 않아도 되므로 코드 유지보수를 더욱 용이하게 만들어줍니다. 또한, 동적으로 스타일을 변경하는 것이 가능하며, 컴포넌트에 스타일을 적용하는 것이 더욱 직관적이고 간단해집니다.
  • Styled Components 예시 코드
import styled from 'styled-components';

const Button = styled.a`
  /* This renders the buttons above... Edit me! */
  display: inline-block;
  border-radius: 3px;
  padding: 0.5rem 0;
  margin: 0.5rem 1rem;
  width: 11rem;
  background: transparent;
  color: white;
  border: 2px solid white;

  /* The GitHub button is a primary button
   * edit this to target it specifically! */
  ${props => props.$primary && css`
    background: white;
    color: black;
  `}
`

2. 기존 방식의 문제점

CSS 클래스 이름이 겹치는 문제

  • 클래스 이름은 전역적인 특성을 가지기 때문에, 부모 컴포넌트의 CSS 파일에서도 같은 클래스 이름이 사용된 부분이 있으면 import 해오지 않은 CSS 파일에서의 스타일이 적용될 수 있습니다. 이렇게 의도하지 않은 방식으로 스타일이 적용되는 걸 막기 위해 클래스 이름은 겹치지 않도록 조심해야 합니다.
  • 하지만 대규모 프로젝트에서는 겹치지 않는 클래스 이름을 짓기가 어렵다는 문제가 있습니다.
  • Styled Components는 클래스 이름을 아예 쓰지 않기 때문에 클래스 이름이 겹칠 일이 없도록 만들어 줍니다.

재사용하는 CSS 코드를 관리하기 어렵다는 문제

  • JavaScript와 달리 CSS 코드는 VSCode 같은 코드 에디터에서 추적하기 어렵기 때문에 직접 텍스트로 하나하나 검색을 해야 하기 때문에, 스타일이 재사용 되는 곳이 많아질수록 코드를 유지 보수 하기 어려워집니다.
  • Styled Components는 스타일 재사용이 필요한 상황에서 클래스가 아니라 JavaScript 변수를 만들기 때문에, 언제 어디서 쓰고 있는지 에디터를 통해 확인하기 쉬워지고, 이름을 바꾸거나 삭제를 하는 것도 쉽게 할 수 있게 해줍니다.

복잡한 선택자를 쓸 때 생기는 문제

  • 여러 선택자를 사용하는 복잡한 선택자는 CSS의 우선순위를 결정하는 규칙을 복잡해지게 만들기 때문에 스타일의 변화가 예상치 못하게 일어나는 원인이 됩니다.
  • Styled Components는 복잡한 선택자를 사용하지 않으므로 스타일 규칙의 우선순위를 결정하는 규칙을 단순하게 만들어주고 코드 유지보수를 더욱 쉽게 만들어줍니다.

3. Styled Components의 특징

Automatic critical CSS

  • 스타일드 컴포넌트는 페이지에서 어떤 컴포넌트가 렌더링되는지 추적하고 해당 스타일만 삽입하며 다른 스타일은 완전히 자동으로 삽입합니다.
  • 코드 분할과 함께 사용하면 사용자가 필요한 최소한의 코드를 로드할 수 있습니다.

No class name bugs

  • 스타일드 컴포넌트는 스타일에 고유한 클래스 이름을 생성합니다. 따라서 duplication, overlap, misspellings에 대해 걱정할 필요가 없습니다.

Easier deletion of CSS

  • 코드베이스 어딘가에서 클래스 이름이 사용되었는지 알기 어려울 수 있지만, 스타일드 컴포넌트는 모든 스타일이 특정 컴포넌트에 연결되어 있기 때문에 명확하게 알 수 있습니다.
  • 도구가 감지했을 때 컴포넌트가 사용되지 않거나, 컴포넌트가 삭제되면 모든 스타일도 함께 삭제됩니다.

Simple dynamic styling

  • prop이나 글로벌 테마를 기반으로 컴포넌트의 스타일을 조정하면 수십 개의 클래스를 수동으로 관리할 필요 없이 간단하고 직관적으로 스타일을 적용할 수 있습니다.

Painless maintenance

  • 컴포넌트에 영향을 미치는 스타일링을 찾기 위해 여러 파일을 찾아다닐 필요가 없으므로 코드베이스의 규모에 관계없이 유지 관리가 매우 간편합니다.

Automatic vendor prefixing

  • 현재 표준에 맞게 CSS를 작성하면 스타일드 컴포넌트가 자동으로 vendor prefix를 처리해줍니다.
    • Vendor prefix
      • 브라우저가 새로운 CSS 속성을 구현하기 전에 해당 속성 이름 앞에 추가하는 접두사(prefix)입니다. 이는 해당 브라우저에서만 해당 CSS 속성을 지원하기 때문에, 다른 브라우저에서는 제대로 작동하지 않을 수 있습니다.
      • 따라서 개발자는 -webkit-*, -moz-* 등과 같은 vendor prefix를 추가하여 모든 브라우저에서 해당 CSS 속성이 일관되게 작동하도록 보장해야 합니다.

4. Styled Components installation

# with npm
npm install styled-components


# with yarn
yarn add styled-components
  • resolutions package.json field를 지원하는 yarn과 같은 패키지 관리자를 사용하는 경우 주요 버전 벌위에 해당하는 항복도 추가할 것을 권장합니다. 이렇게 하면 프로젝트에 여러 버전의 스타일 컴포넌트가 설치되어 발생하는 종류의 문제를 방지할 수 있습니다.
// package.json
{
  "resolutions": {
    "styled-components": "^5"
  }
}
  • Babel 플러그인도 함께 사용하는 것을 적극 권장합니다. 더 읽기 쉬운 클래스 이름, 서버 측 렌더링 호환성, 더 작은 번들 등 많은 이점을 제공합니다.
# with npm
npm install --save-dev babel-plugin-styled-components


# with yarn
yarn add --save-dev babel-plugin-styled-components

5. Nesting 문법

& 선택자

  • Nesting에서 & 선택자는 부모 선택자를 참조합니다.
const Button = styled.button`
  background-color: #6750a4;
  border: none;
  color: #ffffff;
  padding: 16px;

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

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

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

컴포넌트 선택자

  • 하위 컴포넌트를 선택자로 쓰고 싶을 때는 ${Component}같이 컴포넌트 자체를 템플릿 리터럴 안에 넣어주면 됩니다.
const Icon = styled.img`
  width: 16px;
  height: 16px;
`;

const StyledButton = styled.button`
  & ${Icon} {
    margin-right: 4px;
  }
`;

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

export default Button;
.Icon {
  width: 16px;
  height: 16px;
}

.StyledButton .Icon {
  margin-right: 4px;
}
  • &와 자손 결합자를 사용하는 경우에는 &를 생략할 수 있습니다.
const StyledButton = styled.button`
  /*
  & ${Icon} {
	mrgin-right: 4px;
  }
  */

  ${Icon} {
    margin-right: 4px;
  }
`;
  • Nesting은 여러 겹으로 할 수도 있습니다.
const StyledButton = styled.button`
  &:hover,
  &:active {
    ${Icon} {
      opacity: 0.2;
    }
  }
`;
.StyledButton:hover .Icon,
.StyledButton:active .Icon {
  opacity: 0.5;
}

6. 다이나믹 스타일링

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

  • 템플릿 리터럴의 기능으로 값을 넣을 수 있습니다.
const SIZES = {
  large: 24,
  medium: 20,
  small: 16
};

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

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

  • Styled Components는 ${ ... } 안에 함수를 사용할 수 있게 해줍니다.
  • 함수의 파라미터로 props를 받고 스타일 코드를 리턴하도록 작성합니다.
const SIZES = {
  large: 24,
  medium: 20,
  small: 16
};

const Button = styled.button`
  font-size: ${({ size }) => SIZES[size] ?? SIZES['medium']}px;
`;
  • 널 병합 연산자(Nullish coalescing operator), 구조 분해(Destructuring)를 활용할 수 있습니다.

논리 연산자 사용하기

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

7. 상속(inheritance)

styled() 함수

  • Styled Components로 만들어진 컴포넌트를 상속하려면 styled() 함수를 사용하면 됩니다.
import styled from 'styled-components';

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

const SubmitButton = styled(Button)`
  // ...
`;
  • Button 컴포넌트의 스타일을 상속해서 새로운 버튼 SubmitButton을 만드는 코드입니다.

Styled Components를 사용하지 않고 만들어진 컴포넌트 상속

  • 컴포넌트의 Prop에 className 값을 내려줘야 styled() 함수로 상속할 수 있습니다.
function TermsOfService({ className }) {
  return (
    <div className={className}>
      ...
    </div>
  );
}

8. css 함수

  • 반복되는 코드는 한 곳에서 지정하고 여러 군데서 활용하려할 때 styled-components의 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 함수를 사용해서 만드는 것을 권장합니다.

9. 글로벌 스타일

  • 글로벌 스타일을 css 함수를 사용해서 변수로 만들고 사용할 수도 있지만 모든 컴포넌트에 일일이 값을 넣어주어야 하는 문제가 있습니다.
  • 이럴 땐 global style component를 최상위 컴포넌트에서 렌더링 하면 글로벌 스타일이 항상 적용된 상태가 되도록 할 수 있습니다.
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은 html <style> 태그 안에 작성한 CSS 코드가 오버라이딩되도록 내부적으로 처리해줍니다.

10. 애니메이션

@keyframes

  • CSS 애니메이션의 keyframe을 정의할 수 있는 규칙입니다.
@keyframes rotate {
  100% {
    transform: rotate(360deg);
  }
}

.spinner {
  animation: rotate 1.5s linear infinite;
}

keyframes 함수

  • keyframes 함수는 Styled Components에서 @keyframes css 규칙을 사용하고 싶을 때 사용합니다.
import styled, { keyframes } from 'styled-components';

const rotate = keyframes`
  100% {
    transform: rotate(360deg);
  }
`;

const Spinner = styled.div`
  animation: ${rotate} 1.5s linear infinite;
`;

11. 테마

테마(Theme)

  • UI에서 사용되는 색상, 글꼴, 배경 이미지 등과 같은 요소를 변경하여 브라우저의 모양과 느낌을 바꿀 수 있는 기능입니다.
  • Light Mode 테마와 Dark Mode 테마를 선택하는 기능이 자주 사용됩니다.

ThemeProvider로 테마 설정 사용하기

  • 테마로 설정된 값은 전역에서 사용되기 때문에, Styled Components에서도 React의 Context를 기반으로 테마를 사용합니다.
  • ThemeProvider는 styled-components에서 테마 객체를 내려주는 context provider입니다.
// App.js
import { ThemeProvider, createGlobalStyle } 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 = THEMES['light'];

  return (
    <ThemeProvider theme={theme}>
      <div>
        <GlobalStyle />
        <Input />
      </div>
    </ThemeProvider>
  );
}

export default App;


// Input.js
const StyledInput = styled.input`
  background-color: ${({ theme }) => theme.backgroundColor};
  color: ${({ theme }) => theme.color};
`;
  • 여러 테마를 선택하게 하고 싶다면 useState를 활용해서 테마를 선택하게 만들 수 있습니다.
// App.js
import { useState } from 'react';

// ...

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;
  • 테마 설정 페이지처럼 테마 값을 일반적인 컴포넌트에서 참조할 필요가 생긴다면 ThemeContext를 불러오면 됩니다.
  • ThemeContextReact Context이기 때문에 useContext hook으로 접근합니다.
import { useContext } from 'react';
import { ThemeContext } from 'styled-components';

function SettingPage() {
  const theme = useContext(ThemeContext);
}

12. 상황별 유용한 팁

버튼 모양 링크가 필요할 때

  • 스타일은 버튼 스타일이지만 <a> 태그로 사용하고 싶을 때 같은 경우, as라는 키워드로 태그 이름을 내려주면 해당 태그로 사용할 수 있습니다.
const Button = styled.button`
  // ...
`;
<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
  • 위 오류는 React에서 알려주는 오류입니다. HTML 태그에 underline 이라는 속성을 지정했는데, 그 속성의 값이 문자열이 아니라서 생긴 오류입니다.
  • <a> 태그에는 underline 이라는 속성이 없고, <Link> 자식 컴포넌트에서 spread를 하는 과정에서 의도하지 않은 underline prop까지 내려간 것이 원인입니다.
  • 일반적으로 모든 prop은 자식 컴포넌트로 전달되지만, Transient Prop은 해당 컴포넌트에서만 사용될 수 있는 prop입니다.
  • Prop 앞에 $ 기호를 붙이면 Transient prop으로 사용할 수 있습니다.
import styled from 'styled-components';

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

13. Styled Components 파헤치기

태그 함수란?

  • 태그 함수(Tagged Function)는 ES6에서 도입된 템플릿 리터럴 문법을 사용하여 실행할 수 있는 함수입니다.
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에는 표현식의 값들을 저장합니다.

간단한 버전으로 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;

  • StyledH1 컴포넌트는 CSS 코드를 생성하여 <style> 태그로 넣어주는 컴포넌트입니다.
  • 이때 삽입된 값${({ dark }) => dark && 'background-color: black;'}은 함수이기 때문에, props를 아규먼트로 실행해서 CSS로 만듭니다.
  • dark 라는 자바스크립트 표현식도 background-color: black; CSS 문자열로 반영됩니다.

Feedback

  • styled-components 공식 문서로 공부하면 충분할 것 같다.
  • Vite도 공부하자.

Reference

profile
job's done

0개의 댓글