본문 바로가기

FrontEnd

Styled-Components(CSS-in-js) 잘 활용하기

반응형

원문 보기 :https://www.joshwcomeau.com/css/styled-components/

 

The styled-components Happy Path

styled-components is a wonderfully powerful styling library for React, and over the years I've learned a lot about how to use it effectively. This article shares my personal “best practices”.

www.joshwcomeau.com

CSS Variables

Backdrop 컴포넌트에 불투명도와 색상 props가 필요하다고 가정해 보겠습니다.
function Backdrop({ opacity, color, children }) {
  return (
    <Wrapper>
      {children}
    </Wrapper>
  )
}
const Wrapper = styled.div`
  /* ?? */
`;

이러한 속성을 래퍼에 어떻게 적용하나요?

interpolation function을 사용

function Backdrop({ opacity, color, children }) {
  return (
    <Wrapper opacity={opacity} color={color}>
      {children}
    </Wrapper>
  )
}
const Wrapper = styled.div`
  opacity: ${p => p.opacity};
  background-color: ${p => p.color};
`;

이러한 값이 변경될 때마다 스타일드 컴포넌트가 클래스를 다시 생성하고
문서의 <head>에 다시 삽입해야 한다는 것을 의미하며,
이는 특정 경우(예: JS 애니메이션 수행) 성능에 문제가 될 수 있습니다.

CSS 변수를 사용

잘 이해되지 않는다면 CSS Variables in React tutorial 를 참조하세요.

혹은 : https://itchallenger.tistory.com/592

function Backdrop({ opacity, color, children }) {
  return (
    <Wrapper
      style={{
        '--color': color,
        '--opacity': opacity,
      }}
    >
      {children}
    </Wrapper>
  )
}
// 기본값 지정
const Wrapper = styled.div`
  opacity: var(--opacity, 0.75);
  background-color: var(--color, var(--color-gray-900));
`;

단일 진실 원천(SSOT)

이 게시물에서 하나만 얻어갈 수 있다면, 해당 지식을 얻어가세요

 

여기저기서 쓸 수 있는 공통 TextLink 컴포넌트가 있습니다.

const TextLink = styled.a`
  color: var(--color-primary);
  font-weight: var(--font-weight-medium);
`;

Aside 컴포넌트 내부에 TextLink 컴포넌트가 존재하고, 스타일링 한다 가정해 보겠습니다.

우리는 Aside 컴포넌트 내부에서는 TextLink 컴포넌트가 다른 색상을 보여주길 원합니다.

이것을 contextual style이라 할 수 있습니다.
같은 컴포넌트가 문맥에 따라 스타일이 변합니다.

보통 아래와 같이 코딩합니다.

a 때문에 찾기가 좀 더 어렵습니다.

// Aside.js
const Aside = ({ children }) => {
  return (
    <Wrapper>
      {children}
    </Wrapper>
  );
}
const Wrapper = styled.aside`
  /* Base styles */
  a {
    color: var(--color-text);
    font-weight: var(--font-weight-bold);
  }
`;
export default Aside;

styled-components를 사용하면 컴포넌트를 다른 컴포넌트에 "임베딩"할 수 있습니다.

컴포넌트가 렌더링되면 TextLink 스타일드 컴포넌트와 일치하는 클래스의 적절한 셀렉터가 표시됩니다.

// Aside.js
import TextLink from '../TextLink'
const Aside = ({ children }) => { /* Omitted for brevity */ }
const Wrapper = styled.aside`
  /* Base styles */
  ${TextLink} {
    color: var(--color-text);
    font-weight: var(--font-weight-bold);
  }
`;
export default Aside;

당연한 예시인데 무엇이 문제일까요?

문맥이 없으면 TextLink의 스타일은 동일합니다만,

다른 색상이 있는 TextLink를 발견하면 해당 문맥을 찾으러 다녀야 합니다.

특히 aside라는 컴포넌트의 존재 자체도 모른다면, 문제는 어려워집니다.


대신 이렇게 하면 어떨까요?

캡슐화에 대해 이야기해 봅시다.

내가 React를 사랑하게 된 이유는 로직(상태, 효과) 및 UI(JSX)를 재사용 가능한 상자에 포장할 수 있는 방법을 제공한다는 것입니다.

많은 사람들이 "재사용 가능한" 측면에 중점을 두지만 제 생각에는 더 멋진 것은 상자라는 것입니다.

React 컴포넌트는 엄격한 경계를 설정합니다.
컴포넌트에 일부 JSX를 작성할 때 HTML이 해당 컴포넌트 내에서만 수정된다는 것을 신뢰할 수 있습니다.
앱의 반대편에 있는 다른 컴포넌트에 대해 "도달"하고 HTML을 변경하는 것에 대해 걱정할 필요가 없습니다.

TextLink 솔루션을 다시 한 번 살펴보십시오. Aside는 TextLink의 스타일에 손을 대고 간섭합니다!

어떤 컴포넌트가 다른 컴포넌트의 스타일을 덮어쓸 수 있다면 캡슐화를 위반합니다.

주 : 캡슐화는 자기 정보에 대해선 자기 자신이 모든 책임을 지는것(crud)

TextLink 관련 스타일이 전부 TextLink에만 정의되어 있다면 얼마나 좋을까요?

// Aside.js
const Aside = ({ children }) => { /* Omitted for brevity */ }
// Export this wrapper
export const Wrapper = styled.aside`
  /* styles */
`;
export default Aside;
// TextLink.js
import { Wrapper as AsideWrapper } from '../Aside'
const TextLink = styled.a`
  color: var(--color-primary);
  font-weight: var(--font-weight-medium);
  ${AsideWrapper} & {
    color: var(--color-text);
    font-weight: var(--font-weight-bold);
  }
`;

&은 생성된 클래스 명의 플레이스 홀더입니다.
styled-components가 이 컴포넌트에 대해 TextLink-abc123 클래스를 만들 때
& 문자도 해당 선택자(.TextLink-abc123)로 바꿉니다.
.TextLink-abc123 {
  color: var(--color-primary);
  font-weight: var(--font-weight-medium);
}
.Aside-Wrapper-def789 .TextLink-abc123 {
  color: var(--color-text);
  font-weight: var(--font-weight-bold);
}

이 작은 트릭으로 컨트롤을 뒤집었습니다! (제어의 역전!)
구체적인 것을 추상적인 것에 의존하게 하라!
이제 좀 더 구체적인 TextLink는 추상적인 부모 역할을 하는 AsideWrapper에 의존합니다.
AsideWrapper가 어떻게 바뀌든 TextLink는 관계없이 자신의 일을 잘 해냅니다.
 

올바른 상황에 올바른 도구 쓰기 : 일반화와 특수화

할로윈 시즌에만 쓰는 텍스트 링크가 필요합니다. props와 css var를 이용해 variant를 추가할까요?

할로윈 텍스트 링크는 텍스트 링크처럼 일반적인 컴포넌트가 아닙니다.

이 떄는 Composition API가 있습니다.

// HalloweenPage.js
import TextLink from '../TextLink';
const HalloweenTextLink = styled(TextLink)`
  font-family: 'Spooky Font', cursive;
`;

우리는 Aside안에서 TextLink를 사용하는데, AsideTextWrapper, AsideTextLink는 어떤지요?

> 별로입니다.

  • Aside 컴포넌트는 모든 곳에서 사용합니다. 즉 애플리케이션의 핵심적인 부분이기에, TextLink로 작업할 때마다 Aside에 대해 알고 있으면 좋습니다.
  • Aside가 필요할 때마다 AsideTextLink를 사용하는 것을 기억하도록 하고 싶지 않습니다.
<Aside>
  개발자가 이 특수한 컴포넌트가 있다는 것을
  <AsideTextLink href="">
    알아야만 할까요?
  </AsideTextLink>?
</Aside>
위 설명은 간단한 내용이 아니며, 완벽한 솔루션은 없습니다.
그러나 "핵심 변형 컴포넌트"과 "일회성 컴포넌트"을 명확하게 구분하여
가장 중요한 항목의 우선 순위를 지정하고 관리 가능한 상태로 유지합니다

(TextLink에 30개의 일회성 변형을 정의하는 것이 정말 도움이 될까요?).

Escape Hatch

위의 제어의 역전 패턴을 사용하려면, 항상 자식 Styled Component를 만들어야 하는데, 이는 지루할 수 있습니다.
공통 컴포넌트가 아닌,
해당 컴포넌트 내에서만 사용되는 자식 컴포넌트일 경우는 어느 정도 타협을 시도합니다.
// Whatever.js
const Whatever = () => {
  return (
    <Wrapper>
      This is an <em>OK</em> shortcut.
    </Wrapper>
  );
}
const Wrapper = styled.div`
  & > em {
    color: #F00;
  }
`;

상속한 속성들

특정 CSS 속성은 상속 가능하기 때문에 컴포넌트 경계가 완전히 밀폐되지는 않습니다.
그러나 내 생각에는 이것이 큰 문제가 아닙니다.
function App() {
  return (
    <div style={{ color: 'red' }}>
      <Kid />
    </div>
  )
}
function Kid() {
  return <p>Hello world</p>
}

Kid 컴포넌트는 스타일을 설정하지 않지만 빨간색 텍스트를 표시합니다.
이것은 color가 상속된 속성이기 때문입니다.
이것은 기술적으로 누출이지만 몇 가지 이유로 무해합니다.
  • 상속한 속성은 해당 컴포넌트에서 무효화 할 수 있습니다. 강력한 권한은 여전히 컴포넌트에 있습니다.
  • 몇 가지 CSS 속성만 상속할 수 있으며 거의 ​​전적으로 타이포그래피와 관련이 있습니다.
    • 패딩이나 테두리와 같은 레이아웃 속성은 상속되지 않습니다.
    • 일반적으로 타이포그래피 스타일은 상속해야 합니다.
      • 단락의 모든 <strong> 또는 <em>에 현재 글꼴 색상을 다시 적용해야 하는 것은 정말 성가신 일입니다.
  • devtools에서 무슨 일이 일어나고 있는지는 명확합니다. 우리는 스타일이 어디에서 왔는지 이해하기 위해 애쓰지 않을 것입니다.
글로벌 스타일도 마찬가지입니다.
대부분의 프로젝트에서 CSS 재설정과 몇 가지 상식적인 기준 스타일을 적용합니다.
이들은 항상 태그(예: p, h1)에 적용되어 가능한 한 낮은 특이성을 유지합니다.
만약 일부 기본 스타일과 함께 <p> 태그를 사용할 때마다 매번 Paragraph 컴포넌트를 가져와야 한다면 성가실 것입니다.
 
 

독립적인 CSS

Aside 컴포넌트 주변에 다른 컴포넌트가 달라붙지 않게 하고 싶습니다. 마진을 사용할까요?

// Aside.js
const Aside = ({ children }) => {
  return (
    <Wrapper>
      {children}
    </Wrapper>
  );
}
const Wrapper = styled.aside`
  margin-top: 32px;
  margin-bottom: 48px;
`;
export default Aside;

일반적으로 마진은 재사용 가능한 컴포넌트에 도움이 안됩니다.

누군가는 다음과 같이 말했습니다.

 

마진은 무엇을 붙일지 결정하기 전에 접착제를 바르는 것과 같습니다.

또한 마진 겹침은 놀랍도록 이상합니다. (Rules of Margin Collapse)
캡슐화를 깨뜨릴 수 있는 놀랍고 반직관적인 방식으로 붕괴됩니다.
예를 들어 <Aside>를 <MainContent> 안에 넣으면
MainContent에 여백이 있는 것처럼 Aside 위쪽 마진이 전체 그룹을 아래로 밀어냅니다.

마진을 안쓰는 방법들이 논의되고 있습니다.

마진 없이 간격을 어떻게 설정하나요?
  • CSS 그리드 내부에 있는 경우 grid-gap을 사용하여 각 요소의 간격을 지정할 수 있습니다.
  • Flex 컨테이너 내부에 있는 경우 새로운 gap 속성이 경이롭게 작동합니다. 
  • 논란의 여지가 있지만 놀랍도록 즐거운 옵션인 Spacer 구성 요소를 사용할 수 있습니다. (use a Spacer component)
  • Stack과 같은 전용 레이아웃 구성 요소를 사용할 수 있습니다.
목표는 궁지에 몰리는 것을 피하는 것입니다.
장단점을 이해하는 한 가끔 마진을 사용하는 것이 괜찮다고 믿습니다.


쌓임 맥락

// Flourish.js
const Flourish = styled.div`
  position: relative;
  z-index: 2;
  /* Omitted decorative properties */
`;
export default Flourish;

문제가 보이시나요? 이전과 마찬가지로 컴포넌트에 z-index를 미리 지정했습니다.

앞으로의 모든 사용 사례에서 2가 올바른 계층이 되기를 바랍니다!

 

더 위험한 버전을 봅시다. Wapper는 z-index가 없으니 괜찮겠죠?

// Flourish.js
const Flourish = ({ children }) => {
  return (
    <Wrapper>
      <DecorativeBit />
      <DecorativeBackground />
    </Wrapper>
  );
}
const Wrapper = styled.div`
  position: relative;
`;
const DecorativeBit = styled.div`
  position: absolute;
  z-index: 3;
`;
const DecorativeBackground = styled.div`
  position: absolute;
  z-index: 1;
`;

말콤 씨가 컴포넌트를 우리 컴포넌트 위에 뒀네요. 어떻게 될까요? > 중간에 끼입니다. 

쌓임 맥락이 언제 생기는지를 고려하면, 같은 쌓임 맥락에서 z-index를 공유하기 때문입니다.

<div>
  {/* Malcom will get stuck in the middle! */}
  <Malcolm
    style={{ position: 'relative', zIndex: 2}}
  />
  <Flourish />
</div>

이 문제는 isolation 속성으로 해결할 수 있습니다.

const Flourish = ({ children }) => {
  return (
    <Wrapper>
      <DecorativeBit />
      <DecorativeBackground />
    </Wrapper>
  );
}
const Wrapper = styled.div`
  position: relative;
  isolation: isolate;
`;
// The rest unchanged

이렇게 하면 형제 요소가 순서에 따라 요소의 위나 아래에 있게 됩니다.

새로운 스태킹 컨텍스트에는 z-인덱스가 없으므로 순전히 DOM 순서에 의존할 수 있습니다.

(필요할 때 사용 가능)


다양한 팁과 기술들

as props

동적으로 h1 레벨을 정하고 싶을 떄

// `level` is a number from 1 to 6, mapping to h1-h6
function Heading({ level, children }) {
  const tag = `h${level}`;
  return (
    <Wrapper as={tag}>
      {children}
    </Wrapper>
  );
}
// The `h2` down here doesn't really matter,
// since it'll always get overwritten!
const Wrapper = styled.h2`
  /* Stuff */
`;

버튼을 a 링크로 사용하고 싶을 떄

function LinkButton({ href, children, ...delegated }) {
  const tag = typeof href === 'string'
    ? 'a'
    : 'button';
  return (
    <Wrapper as={tag} href={href} {...delegated}>
      {children}
    </Wrapper>
  );
}

선택 우선순위 높이기

다른 CSS 파일과 같이 작업할 때, 내 스타일의 우선순위를 높이는 방법이지만,

important와 같이 기존 규칙을 파괴하므로 조심해야 합니다.

const Wrapper = styled.div`
  p {
    color: blue;
  }
`
const Paragraph = styled.p`
  color: red;
  && {
    color: green;
  }
`;
// Somewhere:
<Wrapper>
  <Paragraph>I'm green!</Paragraph>
</Wrapper>

바벨 플러그인

프로덕션에서 styled-components는 .hNN0ug 또는 .gAJJhs와 같이
각 styled-component에 대해 고유한 해시를 생성합니다.
이러한 간결한 이름은 서버 렌더링 HTML에서 많은 공간을 차지하지 않기 때문에 유용하지만
개발자인 우리에게는 완전히 불투명합니다. 고맙게도 babel 플러그인이 존재합니다!
개발 시 의미론적 클래스 이름을 사용하여 요소/스타일을 소스로 다시 추적하는 데 도움을 줍니다.

create-react-app을 사용하는 경우 아래와 같이 이 플러그인의 이점을 누릴 수 있습니다.
import styled from 'styled-components/macro';

다른 경우 : follow the official documentation


멘탈 모델

지금까지 Styled-Components API를 살펴보았지만
전달하고자 하는 아이디어는 특정 도구나 라이브러리보다 더 큽니다.
 
컴포넌트적 사고 방식을 CSS로 확장하면 모든 종류의 새로운 초능력을 얻게 됩니다.
Styled-Components가 컴포넌트라는 아이디어에 집중해야 합니다!
 
  • CSS 선언을 제거하는 것이 안전한지 자신 있게 알 수 있습니다.
    • 완전히 독립적인 컴포넌트이기 때문입니다.
  • 특정성(specificity)을 높이기 위한 트릭이 필요없습니다.
  • 수동 테스트를 많이 할 필요 없이 페이지가 어떻게 생겼는지 정확히 이해할 수 있습니다.

 

반응형