웹개발

리액트에서 스타일드-컴포넌츠를 사용하는 방법

별을 보고 걷는 사람 2020. 7. 24. 22:52
 

How To Use Styled-Components In React — Smashing Magazine

While the component-driven approach has ushered in a new frontier in the way we build web applications, it isn’t without its imperfections — one being its usability and scalability with CSS. This has given birth to a new way to construct and manage our

www.smashingmagazine.com

Adebiyi Adedotun Luman, 2020년 7월 23일

 

짧은 요약 → 컴포넌트 위주의 접근이 웹 애플리케이션들을 구축하는 새로운 방식을 개척했지만 이것도 결점이 없지는 않다. 하나는 그 사용성과 CSS 확장성이다. 이것은 컴포넌트 특유의 방식 - 또는 CSS-in-JS로 알려짐 - 에서 스타일들을 구성하고 관리하는 새로운 방식을 탄생시켰다. 

 

 

스타일드-컴포넌츠는 컴포넌트들과 스타일링 사이의 틈을 이어주는 CSS-IN-JS(자바스크립트 안의 CSS) 도구로써, 컴포넌트들을 스타일링하는 데 있어 실용성 있고 재사용 면에서 제대로 작동할 수 있도록 수 많은 특질들을 제공해준다. 이 글에서는 스타일드-컴포넌츠의 기초를 배우고 그것들을 리액트 애플리케이션에 어떻게 적절히 적용하는지를 배울 것이다. 리액트 컴포넌트들을 스타일링 하는 다양한 선택 사항들을 찾고 있다면 이 주제 관련 우리의 예전 게시글을 살펴볼 수 있다.

 

CSS의 핵심은 DOM 트리에서 어느 위치에 있는지에 상관 없이 어떠한 HTML 요소라도 - 전체적으로 - 겨냥할 수 있다는 것이다. 이는 컴포넌트들과 사용될 때는 장애물이 될 수 있는데, 왜냐면 컴포넌트들은 어느 정도는 그것이 사용되는 곳에 가까운 곳에 동일 배치(즉, 상태나 스타일링과 같은 자산을 가지고 있는 것)를 요구하기 때문이다. (현지화-localization-로 알려짐)

 

리액트 내의 뜻으로, 스타일드-컴포넌츠는 "컴포넌트들을 위한 시각적 초기 단계" 이며 이것들의 목표는 컴포넌트들을 스타일링 하기 위한 융통성 있는 방법을 주는 것이다. 그 결과, 컴포넌트들과 그것들의 스타일들은 서로 꽉 결합된다.

 

노트: 스타일드-컴포넌츠는 리액트와 리액트 네이티브 둘 다에 쓸 수 있다. 리액트 네이티브 가이드도 반드시 살펴봐야 하겠지만 우리는 여기서 리액트에 대한 스타일된 컴포넌트들에만 중점을 두겠다.

 

왜 스타일드-컴포넌츠(styled-components)인가?

스타일을 자세히 알아보는 것을 도와주는 것은 일단 제쳐두고, 스타일된 컴포넌트들은 다음 특징들을 포함한다:

 

  • 자동 벤더 접두사 붙이기

당신은 표준 CSS 속성들을 쓸 수 있고, 그러면 스타일된 컴포넌트들이 필요한 대로 벤터 접두사를 덧붙일 것이다.

 

  • 유일한 클래스 이름들

스타일된 컴포넌트들은 서로 독립적이이고, 그 이름들은 라이브러리가 처리해줄 것이므로 걱정할 필요가 없다.

 

  • 죽은 스타일들 제거

사용되지 않는 스타일들은 코드 안에 선언되어 있다 하더라도 스타일된 컴포넌트들이 제거할 것이다.

 

 

설치

스타일드-컴포넌츠를 설치하는 것은 쉽다. CDN이나 Yarn 과 같은 패키지 매니저를 통해서 할 수 있다.

yarn add styled-components

 

또는 npm:

npm add styled-components

 

우리의 데모는 create-react-app을 사용한다.

 

 

시작하기

아마도 스타일드-컴포넌츠에서 첫번째로 눈에 들어오는 것이 그 문법일텐데, 스타일드-컴포넌츠 뒤의 마법을 이해하지 못하면 벅찰 수도 있다. 간략히 말하자면, 스타일드-컴포넌츠는 컴포넌트들과 스타일들의 틈을 잇기 위해서 자바스크립트의 템플릿 축약어를 사용한다. 그래서 스타일된 컴포넌트 하나를 만들 때 실제로는 리액트 컴포넌트를 스타일과 함께 만들게 되는 것이다. 이렇게 생겼다:

 

import styled from "styled-components";
// Styled component named StyledButton
const StyledButton = styled.button`
  background-color: black;
  font-size: 32px;
  color: white;
`;
function Component() {
  // Use it like any other component.
  return <StyledButton> Login </StyledButton>;
}

여기서, StyledButton은 스타일된 컴포넌트이고 이것이 구현될 때는 HTML 버튼이 스타일을 보유한 것처럼 된다. styled는 스타일링을 자바스크립트에서 실제 CSS로 변환하는 (라이브러리) 내부의 다용도 메소드이다. 

 

가공되지 않은 HTML과 CSS에서는 이렇게 돼있을 것이다:

button {
  background-color: black;
  font-size: 32px;
  color: white;
}
<button> Login </button>

만약 스타일드-컴포넌츠가 리액트 컴포넌트들이라면 프롭(prop)을 쓸 수 있을까? 그렇다. 쓸 수 있다.

 

프롭(Prop)에 근거하여 적용하기

스타일드-컴포넌츠는 기능적이어서 요소들을 쉽게 역동적으로 스타일링 할 수 있다. 한 페이지에 두 가지 종류의 버튼이 있다고 가정해보자. 하나는 검은색 배경이고 다른 하나는 파란색이다. 그것들을 위해 두 개의 스타일드-컴포넌츠를 만들 필요는 없다; 프롭에 근거하여 그것들의 스타일링을 적용할 수 있다.

import styled from "styled-components";
const StyledButton = styled.button`
  min-width: 200px;
  border: none;
  font-size: 18px;
  padding: 7px 10px;
  /* The resulting background color will be based on the bg props. */
  background-color: ${props => props.bg === "black" ? "black" : "blue";
`;
function Profile() {
  return (
    <div>
      <StyledButton bg="black">Button A</StyledButton>
      <StyledButton bg="blue">Button B</StyledButton>
    </div>
  )
}

StyledButton이 프롭을 수용하는 리액트 컴포넌트기 때문에, bg 프롭의 존재나 그 값에 근거하여 다른 배경색을 지정할 수 있다.

 

그런데 그 버튼에 type을 지정해주지 않았다는 것을 눈치챘을 것이다. 그렇게 하자:

function Profile() {
  return (
    <>
      <StyledButton bg="black" type="button">
        Button A
      </StyledButton>
      <StyledButton bg="blue" type="submit" onClick={() => alert("clicked")}>
        Button B
      </StyledButton>
    </>
  );
}

스타일드-컴포넌츠들은 자신들이 받는 프롭의 종류를 구분할 수 있다. type이 HTML의 성질임을 알아서 자기 것을 처리 하면서 bg프롭을 사용하여 <button type="button">Button A</button>을 실제로 구현할 수 있다. 이벤트 핸들러도 어떻게 붙였는지 봤는가?

 

성질들에 대한 말이 나왔으니 말인데, 확장된 문법은 attrs 생성자를 사용하여 프롭들을 관리할 수 있게 해준다.

const StyledContainer = styled.section.attrs((props) => ({
  width: props.width || "100%",
  hasPadding: props.hasPadding || false,
}))`
  --container-padding: 20px;
  width: ${(props) => props.width}; // Falls back to 100%
  padding: ${(props) =>
    (props.hasPadding && "var(--container-padding)") || "none"};
`;

너비를 설정하는데 3변수 없이 어떻게 하는지 보이는가? 그것은 우리가 이미 width: props.width || "100%", 로 기본값을 설정했기 때문이다. 또 우리는 CSS 맞춤 속성들도 사용했다. 그렇게 할 수 있으니까!

 

노트: 스타일드-컴포넌츠가 리액트 컴포넌트들이고 프롭들을 패스할 수 있다면 상태들도 사용할수 있겠지? 이 라이브러리의 깃헙 계정에서 바로 이 문제를 제기하고 있다.

 

스타일들 확장

당신이 랜딩 페이지 작업을 하고 있다고 치고, 당신은 요소들을 가운데 정렬하기 위해 컨테이너를 특정 최대 너비로 설정했다. 이를 위해 StyledContainer가 있다:

const StyledContainer = styled.section`
  max-width: 1024px;
  padding: 0 20px;
  margin: 0 auto;
`;

그리고나서 당신은 더 작은 컨테이너가 필요하다는 것을 발견한다. 양쪽이 20 픽셀인 대신 10 픽셀인 것을. 처음 드는 생각은 또 다른 스타일된 컴포넌트를 만드는 것일테고 그것도 옳을 수 있겠지만, 머지 않아 당신은 스타일들을 복제하고 있다는 것을 깨닫게 될 것이다.

const StyledContainer = styled.section`
  max-width: 1024px;
  padding: 0 20px;
  margin: 0 auto;
`;
const StyledSmallContainer = styled.section`
  max-width: 1024px;
  padding: 0 10px;
  margin: 0 auto;
`;

StyledSmallContainer를 만들러 가기 전에, 위의 코드 토막처럼 스타일을 재사용하고 전수 받는 방법을 배워보자. 이것은 spread 연산자가 작동하는 방식과 다소 흡사하다.

const StyledContainer = styled.section`
  max-width: 1024px;
  padding: 0 20px;
  margin: 0 auto;
`;
// Inherit StyledContainer in StyledSmallConatiner
const StyledSmallContainer = styled(StyledContainer)`
  padding: 0 10px;
`;
function Home() {
  return (
    <StyledContainer>
      <h1>The secret is to be happy</h1>
    </StyledContainer>
  );
}
function Contact() {
  return (
    <StyledSmallContainer>
      <h1>The road goes on and on</h1>
    </StyledSmallContainer>
  );
}

StyledSmallContainerStyledContainer의 모든 스타일들을 받을 수 있지만 패딩은 덮어쓰기 될 것이다. 대개는 StyledSmallContainer를 위해 구현된 섹션 요소가 있을 것이라는 점을 염두에 두어라. 왜냐하면 그것이 StyledContainer가 구현하는 것이기 때문이다. 하지만 그것이 돌에 각인되어 바뀔 수 없다는 것을 의미하는 것은 아니다.

 

다형성 프롭 "as"

다형성 프롭 as로, 구현되는 양 끝 요소들을 교환할 수 있다. 한 사례는 스타일을 물려 받을 때이다. (이 전 예시에서처럼) 만약 예를 들어 당신이 StyledSmallContainer에 section 대신 div를 선호한다면 당신이 선호하는 요소의 값을 가진 as 프롭을 스타일된 컴포넌트에 패스할 수 있다. 이렇게:

function Home() {
  return (
    <StyledContainer>
      <h1>It’s business, not personal</h1>
    </StyledContainer>
  );
}
function Contact() {
  return (
    <StyledSmallContainer as="div">
      <h1>Never dribble when you can pass</h1>
    </StyledSmallContainer>
  );
}

이제, StyledSmallContainer는 div로 구현될 것이다. 심지어 당신의 값과 같은 맞춤 컴포넌트를 가질 수도 있다.

function Home() {
  return (
    <StyledContainer>
      <h1>It’s business, not personal</h1>
    </StyledContainer>
  );
}
function Contact() {
  return (
    <StyledSmallContainer as={StyledContainer}>
      <h1>Never dribble when you can pass</h1>
    </StyledSmallContainer>
  );
}

이걸 당연한 것으로 여기지 말아라.

 

SCSS 같은 문법

CSS 전처리 장치인 Stylis는 SCSS 같은 문법, 네스팅과 같은 것을 지원하기 위해 스타일된 컴포넌트들을 활성화 한다.

const StyledProfileCard = styled.div`
  border: 1px solid black;
  > .username {
    font-size: 20px;
    color: black;
    transition: 0.2s;
    &:hover {
      color: red;
    }
    + .dob {
      color: grey;
    }
  }
`;
function ProfileCard() {
  return (
    <StyledProfileCard>
      <h1 className="username">John Doe</h1>
      <p className="dob">
        Date: <span>12th October, 2013</span>
      </p>
      <p className="gender">Male</p>
    </StyledProfileCard>
  );
}

 

애니메이션

스타일드-컴포넌츠는 (재사용 가능한) 애니메이션 키프레임들을 구성하는데 도움을 주는 keyframes 도우미를 가지고 있다. 이것의 장점은 키프레임들이 스타일드-컴포넌츠로부터 분리되어 필요한 곳 어디에서든지 내보내지고 재사용될 수 있는 것이다.

import styled, {keyframes} from "styled-components";
const slideIn = keyframes`
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
`;
const Toast = styled.div`
  animation: ${slideIn} 0.5s cubic-bezier(0.4, 0, 0.2, 1) both;
  border-radius: 5px;
  padding: 20px;
  position: fixed;
`;

 

글로벌 스타일링

CSS-in-JS, 더 나아가 스타일드-컴포넌츠의 원래 목적이 스타일들의 영역을 지정하는 것이긴 하지만 우리는 또한 스타일드-컴포넌츠의 전체적인 스타일링도 가져다 쓸 수 있다. 대부분 영역이 지정된 스타일들로만 작업할 수 있기 때문에 그것이 불변의 공장 설정이라고 생각하겠지만 당신이 틀렸을 수도 있다. 생각해 보라: 영역 지정이란 게 진짜 무엇인가? 이런 것과 유사한 것을 - 글로벌 스타일링이란 명목 하에 - 하는 것이 기술적으로 가능하다.:

ReactDOM.render(
  <StyledApp>
    <App />
  </StyledApp>,
  document.getElementById("root")
);

하지만 우리에겐 이미 자신의 유일한 존재의 이유가 전체적으로 스타일을 적용하는 도우미 함수 - createGlobalStyle - 가 있다. 그럼 왜 그 책임을 거부하는가?

 

createGlobalStyle을 쓰는 곳 한군데는 CSS 정상화를 위해서이다:

import {createGlobalStyle} from "styled-components";
const GlobalStyle = createGlobalStyle`
    /* Your css reset here */
`;
// Use your GlobalStyle
function App() {
  return (
    <div>
      <GlobalStyle />
      <Routes />
    </div>
  );
}

노트: createGlobalStyle로 생성된 스타일들은 자식들을 받아들이지 않는다. 문서에서 더 알아보기.

 

이쯤에서 당신은 왜 createGlobalStyle을 굳이 써야하는지 궁금할 것이다. 여기 몇 가지 이유가 있다:

 

  • 이것 없이는 뿌리 구현 말고 다른 것은 할 수가 없다. (html, body 등이 예)
  • createGlobalStyle은 스타일들을 주입하지만 실제 요소는 하나도 구현하지 않는다. 마지막 예시를 자세히 보면 구현할 어떤 HTML 요소도 구체화하지 않았다는 것을 알 수 있을 것이다. 실제로 그 요소가 필요하지 않을 수도 있기 때문에 이건 좋다. 결국에 우리는 글로벌 스타일에만 관심이 있다. 우리는 구체적인 요소가 아니라 전반적으로 셀렉터들을 겨냥한다.
  • createGlobalStyle의 영역은 지정되지 않아 앱의 어디에서든 구현될 수 있고 DOM 내에 있는 한 어디에도 적용될 수 있다. 구조가 아닌 개념을 생각하라.
import {createGlobalStyle} from "styled-components";
const GlobalStyle = createGlobalStyle`
  /* Your css reset here */
  .app-title {
    font-size: 40px;
  }
`;
const StyledNav = styled.nav`
    /* Your styles here */
`;
function Nav({children}) {
  return (
    <StyledNav>
      <GlobalStyle />
      {children}
    </StyledNav>
  );
}
function App() {
  return (
    <div>
      <Nav>
        <h1 className="app-title">STYLED COMPONENTS</h1>
      </Nav>
      <Main />
      <Footer />
    </div>
  );
}

구조를 생각해 보면 app-titleGlobalStyle에 설정된 것처럼 스타일되어서는 안 된다. 그런데 이게 그렇게 작동하지 않는다. 당신이 GlobalStyle을 구현하려고 선택할 때마다 컴포넌트가 구현될 때 이것이 주입될 것이다.

 

주의: createGlobalStyles는 DOM안에 있을 때에만 구현될 것이다.

 

CSS 도우미

이미 우리는 어떻게 프롭들에 근거하여 스타일을 적용하는 지를 보았다. 만약 우리가 좀 더 깊이 들어가고 싶다면? CSS 도우미 함수가 이것을 달성하게 도와준다. 글자 입력 필드 두 개가 상태들과 함께 있다고 가정하자: 비었으나 활성화되어 있고 각자 다른 색상이다. 이렇게 할 수 있다:

const StyledTextField = styled.input`
  color: ${(props) => (props.isEmpty ? "none" : "black")};
`;

다 잘 되었다. 그 다음에 우리는 또 다른 채워진 상태를 더할 필요가 있으니, 스타일을 수정해야 한다:

const StyledTextField = styled.input`
  color: ${(props) =>
    props.isEmpty ? "none" : props.active ? "purple" : "blue"};
`;

이제 3변수 연산자가 더 복잡하게 커지고 있다. 이 후에 이 글자 입력 필드들에 또 다른 상태를 추가하면 어떻게 될까? 혹은 각 상태에 색깔 외 추가 스타일들을 주고 싶다면? 3변수 연산자 안에 스타일들을 욱여넣는 것을 상상할 수 있는가? css 도우미가 쓸모가 있다.

const StyledTextField = styled.input`
  width: 100%;
  height: 40px;
  ${(props) =>
    (props.empty &&
      css`
        color: none;
        backgroundcolor: white;
      `) ||
    (props.active &&
      css`
        color: black;
        backgroundcolor: whitesmoke;
      `)}
`;

우리가 한 것은 그 3 변수 문법을 더 많은 스타일들을 수용할 수 있고 더 이해하기 쉽고 정리된 형태로 일종의 확장을 한 것이다. 만약 이전 명령문이 틀린 것 같다면 코드가 너무 많은 것을 하려 하고 있기 때문이다. 그러니 한 발자국 떨어져서 다듬자:

 

const StyledTextField = styled.input`
width: 100%;
height: 40px;
// 1. Empty state
${(props) =>
  props.empty &&
  css`
    color: none;
    backgroundcolor: white;
  `}
// 2. Active state
${(props) =>
  props.active &&
  css`
    color: black;
    backgroundcolor: whitesmoke;
  `}
// 3. Filled state
${(props) =>
  props.filled &&
  css`
    color: black;
    backgroundcolor: white;
    border: 1px solid green;
  `}
`;

 

다음었더니 스타일링이 관리 가능하고 이해하기 쉬운 세 개의 다른 묶음으로 나뉘었다. 이득이다.

 

 

스타일시트매니저

CSS 도우미처럼, StyleSheetManager도 어떻게 스타일들이 처리되는지를 수정하는 도우미 메소드이다. 특정한 프롭들을 받는데 - disableVendorPrefixes 같은 (전체 목록을 확인 할 수 있다) - 벤더 접두사의 종속 트리로부터 손 뗄 수 있게 도와준다.

import styled, {StyleSheetManager} from "styled-components";
const StyledCard = styled.div`
  width: 200px;
  backgroundcolor: white;
`;
const StyledNav = styled.div`
  width: calc(100% - var(--side-nav-width));
`;
function Profile() {
  return (
    <div>
      <StyledNav />
      <StyleSheetManager disableVendorPrefixes>
        <StyledCard> This is a card </StyledCard>
      </StyleSheetManager>
    </div>
  );
}

disableVendorPrefixes<StyleSheetManager>에 프롭으로 넘겨진다. 그래서 <StyledNav>에 있는 것이 아닌 <StyleSheetManager>로 쌓여있는 스타일된 컴포넌트 들은 비활성화된다.

 

더 쉬운 디버깅

스타일드-컴포넌츠를 내 동료들 중 한 명한테 소개할 때 불만 중 하나가 DOM 안에서 구현된 요소를 찾기가 어렵다는 것이다. - 또는 같은 문제로 리액트 개발자 도구 안에서. 이것이 스타일드-컴포넌츠의 단점들 중 하나이다: 유일한 클래스 이름들을 제공하려는 시도를 하면서 요소들에 유일한 해쉬들을 배정하고 이것이 암호화되는 경우가 있다. 하지만 이것은 더 쉬운 디버깅을 위한 displayName을 해독할 수 있게 한다.

import React from "react";
import styled from "styled-components";
import "./App.css";
const LoginButton = styled.button`
  background-color: white;
  color: black;
  border: 1px solid red;
`;
function App() {
  return (
    <div className="App">
      <LoginButton>Login</LoginButton>
    </div>
  );
}

기본적으로 스타일된 컴포넌트들은 LoginButton<button class="LoginButton-xxxx xxxx">Login</button>로 DOM에 , 리액트 개발자 도구에는 LoginButton로 구현하는데, 이것은 디버깅을 쉽게 해준다. 우리는 displayName이 불리언처럼 행동하길 바라지 않는 이상 그렇게 전환할 수 없다. 이것은 바벨 환경설정을 필요로 한다.

 

노트: 문서에 babel-plugin-styled-components 패키지가 구체화되어 있고 .babelrc 환경설정 파일도 있다. 여기에서 문제는 우리가 create-react-app을 사용하고 있기 때문에 많은 것들을 꺼내지 않고는 설정 변경을 할 수 없다는 것이다. 여기가 바벨 매크로들이 들어오는 지점이다.

 

우리는 babel-plugin-macros를 npm이나 Yarn으로 설치하고, 그리고 나서 다음 내용과 함께 애플리케이션의 뿌리에 babel-plugin-magros.config.js를 생성할 필요가 있다:

module.exports = {
  styledComponents: {
    displayName: true,
    fileName: false,
  },
};

fileName 값이 반전되면 displayName에는 심지어 더 독창적으로 정교한 접두사가 붙는다.

 

우리는 또한 macro에서 불러오기를 해야 한다:

// Before
import styled from "styled-components";
// After
import styled from "styled-components/macro";

 

결론

이제 당신이 코딩으로 CSS를 구성할 수 있다고 해서 그 자유를 남용하지 말라. 스타일된 컴포넌트들을 온전한 상태로 유지하는 데에 최선을 다하라는 것이 내 의견이다. 무거운 조건문들을 구성하려 하지 말고 모든 것들이 스타일된 컴포넌트여야 한다는 생각을 하지 말라. 또, 언젠가는 쓰겠지란 생각으로 그런 사례들을 위한 스타일된 컴포넌트들을 초기에 과하게 추상화 하지 말라.