본문 바로가기

FrontEnd

styled-components best practices(모범 사례)

반응형

styled-components(emotion 등)을 잘 사용하는 방법을 정리해 봅니다.

styled-component

1. variant와 as를 사용해 스타일 재사용

styled-component에서 스타일을 재사용 하는 방법은 보통 합성입니다.

이 경우 해당 컴포넌트의 특수화에는 좋지만, 아래와 같이 여러 컴포넌트가 있다는 것을 사용자가 알아야 합니다.

이는 객체지향 프로그래밍의 합성과 유사한 사용 사럐입니다.

const ButtonStyle = styled.button`
    color: ${(props) => props.theme.primary};
`;

const ButtonStyleFlashy = styled(ButtonStyle)`
    color: fuchsia;
`;

const ButtonDisabled = styled(ButtonStyle)`
    color: ${(props) => props.theme.grey};
`;

요새 유행하는 utility css와 유사하게, 컴포넌트를 사용 사례 별로 정의할 수 있습니다.

이는 함수형 프로그래밍의 파라메트릭 다형성 기법과 유사합니다.

const ButtonStyle = styled.button`
  ${({ variant }) =>
    variant == "primary" &&
    css`
      color: "fushia";
    `}
  ${({ variant }) =>
    variant == "secondary" &&
    css`
      color: "gray";
    `}
`;

 

해당 기능을 일급으로 지원하는 stitchjs와 같은 라이브러리가 존재합니다만, 주류가 아니므로 위와 같이 에뮬레이션 할 수 있습니다.

https://itchallenger.tistory.com/887

 

Stitches와 Radix를 이용해 디자인 시스템 만들기

Radix(Radix)라이브러리의 메인테이너가 디자인 시스템 컴포넌트를 만드는 방법에 관해 작성한 글이 있어서 요약 정리해 봤습니다. 원문 링크 Pedro Duarte Why I Build Design Systems with Stitches and Radix ped.ro

itchallenger.tistory.com

as prop을 사용해 기존 컴포넌트의 스타일을 재사용하면서 태그나 컴포넌트만 바꾸는 방법도 있습니다.

function Heading({ level, children }) {
  const tag = `h${level}`;
  return (
    <Wrapper as={tag}>
      {children}
    </Wrapper>
  );
}

2. 스타일의 캡슐화; CSS 고립시키기

리액트 컴포넌트는 독립적으로 진화할 수 있어야 합니다.

컴포넌트의 스타일이 누출되어 다른 컴포넌트에 영향을 미친다는 것은 캡슐화를 위반한 것입니다.

따라서 컴포넌트는 적절한 컨텍스트 바운더리를 생성해야 합니다.

그리고 해당 컴포넌트를 삭제하거나, 해당 컴포넌트의 스타일시트가 없어지는게 다른 컴포넌트에 영향을 미치지 않아야 합니다.

https://fe-developers.kakaoent.com/2022/220609-storybookwise-component-refactoring/

 

스토리북 작성을 통해 얻게 되는 리팩토링 효과

카카오엔터테인먼트 FE 기술블로그

fe-developers.kakaoent.com

쌓임 맥락

컴포넌트 내에서 쌓임 맥락을 형성하면, 컴포넌트 자체가 ppt의 그룹 요소처럼 동작합니다.

그룹 요소는 다른 요소와 위 아래로 쌓일 때, 통째로 움직이게 됩니다.

따라서 내부 컴포넌트의 버튼만 다른 컴포넌트의 위에 올라가는 일은 발생하지 않습니다.

따라서 z-index는 내부 컴포넌트끼리만 신경쓰면 됩니다.

 

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

특이도(selectivity)

컴포넌트 내에서 셀렉터의 완결성을 갖는 것이 좋습니다.

하지만, 하위 children 컴포넌트에서 불필요한 스타일이 적용되는걸 피하고 싶다면,

직계 자식만 선택하는 child combinator (>) 를 사용해 외부 주입된 컴포넌트에 영향을 미치는 것을 피할 수 있습니다.

const Whatever = () => {
  return (
    <Wrapper>
      This is an <em>OK</em> shortcut.
    </Wrapper>
  );
}
const Wrapper = styled.div`
  & > em {
    color: #F00;
  }
`;

좋은 방법은 아니지만 자기 자신의 스타일을 강제하는 방법도 있습니다.

const Paragraph = styled.p`
  color: red;
  && {
    color: green;
  }
`;

또한 몇몇 특정 사례에서, 컨텍스트를 다른 스타일드 컴포넌트의 자손으로 강제 지정할 수 있는 방법도 있습니다.

(개인적으로 좋은 방법이라고 생각하지는 않습니다.)

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

마진(margin)

은근히 마진 때문에 div, span 등의 특수화된 컴포넌트를 만드는 일이 잦습니다.

이 경우 spacer 컴포넌트를 쓰면 좋습니다.

굉장히 레이아웃이 복잡한 컴포넌트(ex 대시보드 및 카드)들을 여러개 개발할 일이 있었는데,

FlexBox를 Row, Col로 컴포넌트화 하고, gap으로 해결되지 않는 마진을 해당 컴포넌트를 사용해 개발하니

생산성이 확실히 늘어난 것을 체감했습니다.

바로 사용해보시는걸 추천합니다.


3. 컴포넌트 명명

최대한 일반적인 컴포넌트를 만들어서 재사용하되, 필요한 경우 특수화를 적용합니다.

이는 컴포넌트의 네이밍에서도 나타납니다.

 

아래와 같이 TextLink컴포넌트를 AsideTextLink로 명명할 경우, 

  • 사용자는 해당 컴포넌트가 있다는 것을 알아야 합니다.
  • 해당 컴포넌트는 Aside 내에서만 사용할 수 없습니다.
<Aside>
  Do you think developers will remember to use
  <AsideTextLink href="">
    this special variant
  </AsideTextLink>?
</Aside>

합성 컴포넌트일 경우(compound component) 아래와 같은 제약이 유효하지만,

TextLink와 같은 경우는 매우 일반적이기에 다른 곳에서도 유사한 스타일을 사용할 것 같습니다.

따라서 차라리 아래가 나은 선택일 것 같습니다.

<Aside>
  Do you think developers will remember to use
  <TextLink variant="chevron_right">
   this special variant has a image on the left side of this. (>)
  </TextLink>?
</Aside>

하지만 이벤트성 페이지 컴포넌트일 경우, 해당 디자인이 재사용될 것이라는 보장이 없습니다.

이 경우 특수화 및, 도메인적인 컴포넌트에 캡슐화하여 별개로 취급하는 것이 나을 것입니다.

해당 페이지 개발자가 아닌 다른 개발자들은 이러한 컴포넌트가 있는지 잘 몰라도 될 것입니다.

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

4. 단일 진실 원천(Single Source Of Truth)

디자인 시스템이란 컴포넌트를 기반으호 화면을 조립하는 방식으로 디자인에 접근하는 체계를 의미하며,

해당 시스템에는 디자인 토큰이라는 단일 진실 원천이 있습니다.

디자인 토큰은 디자인 시스템의 사양을 나타내는 키-값 쌍입니다.

그리고 이 디자인 사양은 객체로 나타내면 좋습니다.

const COLORS = {
  text: 'black',
  background: 'white',
  primary: 'rebeccapurple',
};
const SIZES = [
  8,
  16,
  24,
  32,
  /* And so on */
];

객체로 나타내면 좋은 이유는 사양을 동적으로 다루거나, 다른 플랫폼에 이관하기도 무척 편하기 때문입니다.

그리고 의외로 탬플릿 리터럴 css 선언보다 재사용성이 높습니다. (ex) css-in-js 라이브러리 마이그레이션)


5. CSS Variables을 단일 진실 원천(Single Source ot Truth)

4번 내용과 이어집니다.

이는 비유하자면 CSS Variables를 리덕스 스토어처럼 생각하고,

각 컴포넌트들을 스토어를 구독하는 컴포넌트로 생각하는 것입니다.

 

4번으로 정의한 디자인 토큰을 가져다 css 변수로 사용할 수 있습니다.

따라서 css 선언의 값을 좀 더 시맨틱하게 사용할 수 있습니다.

즉, 단일 진실 원천인 디자인 토큰 기반으로 css를 작성할 수 있게 됩니다.

(1번의 설명과 함께)

const GlobalStyles = createGlobalStyle`
  html {
    --color-text: ${COLORS.text};
    --color-background:  ${COLORS.background};
    --color-primary:  ${COLORS.primary};
  }
`;

그리고 아래와 같이 글로벌 CSS로 스타일 시트에 삽입합니다.

(css 변수는 디폴트로 상속됩니다.)

import { createGlobalStyle } from 'styled-components';
const GlobalStyles = createGlobalStyle`
  html {
    --color-text: ${COLORS.text};
    --color-background:  ${COLORS.background};
    --color-primary:  ${COLORS.primary};
  }
`;
const App = ({ children }) => {
  return (
    <>
      <GlobalStyles />
      {children}
    </>
  );
};

해당 방법은 해당 글에서 언급한 성능적 이슈를 피하기 위해선 반드시 사용하는게 좋습니다.

또한 직접적인 디자인 토큰의 임포트를 피할수 있습니다.

import { COLORS } from '../constants';
const Button = styled.button`
  background: ${COLORS.primary};
`;

 

컨텍스트 전달을 피할수 있어 컴포넌트 코드가 깔끔해지며 컨텍스트에 대한 의존성이 사라집니다.

// Elsewhere…
const Button = styled.button`
  background: ${(props) => props.theme.colors.primary};
`;

해당 코드는 아래와 같이 변경됩니다.

const Button = styled.button`
  background: var(--color-primary);
`;

 

또한 애플리케이션의 컨텍스트가 디자인 토큰의 컨텍스트와 맞물려 전체적인 일관성을 쉽게 얻을 수 있습니다.

 

예를 들어 아래 코드는 마우스나 트랙패드가 아닌 덜 정확한 조작 장치(터치패드) 등을 사용할 때 버튼과 인풋의 크기를 키워주기 위한 코드입니다.

컴포넌트 중심적 사고는 다음과 같은 결과물을 낳습니다.

const App = ({ children }) => {
  return (
    <ThemeProvider
      theme={{
        colors: COLORS,
        coarseTapHeight: 48,
        fineTapHeight: 32,
      }}
    >
      {children}
    </ThemeProvider>
  );
};

const Button = styled.button`
  min-height: ${(props) => props.theme.fineTapHeight}px;
  @media (pointer: coarse) {
    min-height: ${(props) => props.theme.coarseTapHeight}px;
  }
`;

const TextInput = styled.input`
  min-height: ${(props) => props.theme.fineTapHeight}px;
  @media (pointer: coarse) {
    min-height: ${(props) => props.theme.coarseTapHeight}px;
  }
`;

하지만 디자인 토큰적 사고방식은 다음과 같습니다.

pointer : coarse의 경우, tap 대상의 높이값을 바꿉니다.

const GlobalStyles = createGlobalStyle`
  html {
    --min-tap-target-height: 32px;
    @media (pointer: coarse) {
      --min-tap-target-height: 48px;
    }
  }
`;

그 값을 "구독"하면, 해당 값이 변경되었을 때, 변경된 값에 반응하여 높이를 조정할 수 있습니다.

const Button = styled.button`
  min-height: var(--min-tap-target-height);
`;
const TextInput = styled.input`
  min-height: var(--min-tap-target-height);
`;
주 : 그런데 어차피 테마 자체도 컨텍스트이기 때문에, 컨텍스트를 뮤테이션 하면 유사하게 구현 가능할 것 같기도 합니다.
하지만 성능 이슈가 있으므로 그렇게 하지 마세요

이는 불가능한 것들을 애니메이션("Magical Rainbow Gradients")할 수 있게 해주기도 합니다.

 

타입 안정성을 걱정하시나요?

타입스크립트를 적용하는 방법은 해당 글의 설명에 있는 링크를 참조하세요

 

중요하진 않지만 아래와 같은 응용이 가능합니다.

 

css 변수를 사용하여 코드를 값을 불러올 수 있습니다.

getComputedStyle(document.documentElement)
  .getPropertyValue('--color-primary');

js로 동적 값 변경도 쉽습니다.

document.documentElement.style.setProperty(
  '--color-primary',
  'hsl(245deg, 100%, 60%)'
);

사실 해당 방법의 단점이 있는데, media query와 같이 사용 불가능한 부분이 있다는 점입니다.

(미디어 쿼리 내에서는 안되는에 container query 내에서는 쓸 수 있는것 같습니다. ㅎ)

이 경우 4번의 객체 디자인 토큰을 응용하면 좋습니다. (활용 예제 : media-queries)


기타

위 내용들과 직교하는 부분도 있을 수 있으나, 일반적인 utility css로의 진화는 아래와 같은 방향성을 갖고 있습니다.

  • CSS prop을 사용합니다
  • JS 객체 사용
  • 스타일은 최상위 스코프에 선언
  • 스타일을 가져오거나 내보내지 않습니다.
  • 스타일을 중첩하지 않습니다.
  • 스타일을 다른 스타일 선언 내에서 합성하지 않습니다.
const styles = css({
  borderRadius: 3,
});

const disabledStyles = css({
  color: 'lightgrey',
  backgroundColor: 'gray',
});

const subtleStyles = {
  default: css({
    color: 'black',
    backgroundColor: 'lightgrey',
  }),
  primary: css({
    color: 'blue',
    backgroundColor: 'white',
  }),
};

const boldStyles = {
  default: css({
    color: 'lightgrey',
    backgroundColor: 'black',
  }),
  primary: css({
    color: 'white',
    backgroundColor: 'blue',
  }),
};

function Lozenge({ children, isDisabled, isBold, appearance }) {
  const appearanceStyles = isBold ? boldStyles[appearance] : subtleStyles[appearance];
  return (
    <span
      css={[
        styles,
        isDisabled ? disabledStyles : appearanceStyles,
      ]}
    >
      {children}
    </span>
  );
}

tailwind를 사용하거나, css module을 사용하거나와 관계없이, 재사용 가능한 css와 생산성을 추구한다면,

유틸리티 css에 집중하는게 맞는것 같습니다.

이는 짧은 시간에 많은 양의 컴포넌트를 퍼블리싱 및 구현하게 되면 뼈저리게 체감되더라구요

참고

https://emotion.sh/docs/best-practices

 

Emotion – Best Practices

Emotion is an extremely flexible library, but this can make it intimidating, especially for new users. This guide contains several recommendations for how to use Emotion in your application. Keep in mind, these are only recommendations, not requirements! R

emotion.sh

https://douges.dev/blog/taming-the-beast-that-is-css-in-js

 

Change how you write your CSS-in-JS for better performance

CSS-in-JS is awesome, but flexible. What if we could change how we write styles to improve performance and understanding without needing to jump to another library?

douges.dev

https://www.joshwcomeau.com/css/css-variables-for-react-devs/#the-holy-trinity-13

 

CSS Variables for React Devs

CSS Variables are *really* cool, and they're incredibly powerful when it comes to React! This tutorial shows how we can use them with React to create dynamic themes. We'll see how to get the most out of CSS-in-JS tools like styled-components, and how our m

www.joshwcomeau.com

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

https://www.michaelmang.dev/blog/introduction-to-design-tokens

 

Introduction to Design Tokens

Design tokens are being used by companies like Amazon and Adobe to solve the pitfalls of coding a design system. I provide an introduction to design tokens.

www.michaelmang.dev

 

반응형