본문 바로가기

FrontEnd

styled-components의 동작 원리와 주의사항

반응형

styled-component의 동작 원리와 주의사항을 알아봅니다.

styled-components

본문에선 언급하지 않았으나, 실제로 styled-components는 useMemo, useCallback과 같은 도구를 이용하여
자체적인 최적화를 많이 수행하고 있습니다.
따라서 불필요한 css 재평가 부하에 대해선 걱정할 필요가 없습니다.

1. styled-components가 CSS를 생성 및 주입하는 방법

  1. styled-components는 css를 lazy 하게 생성 및 삽입함
    • 컴포넌트 렌더링 전까지 탬플릿 리터럴, 혹은 객체로 전달된 css 규칙을 평가하지도 않고, cssom에 반영하지 않음
    • 렌더링하는 css에 대해서만 css 평가 비용을 지불 (비교 - 사전 컴파일 tailwind)
      • js는 defer를 사용하여 다운로드, 처리 전에 화면을 렌더링하도록 할 수 있지만, 브라우저는 css를 전부 다운로드 완료하고 처리하기 전까지 화면을 렌더링하지 않음
  2. styled-components는 cssom을 이용해 css 규칙을 추가함
    1. document.stylesheets[0], styleSheet.insertRule(rulesets)

예시 코드 (실제와 다름)

function h1(styles) {
  return function NewComponent(props) {
  	// styles 객체, 혹은 태그드 탬플릿 리터럴을 활용해 해시된 클래스 명 생성
    const uniqueClassName = comeUpWithUniqueName(styles);
    // 해당 객체를 sass, less 등의 규칙을 처래히주는 Stylus css processor를 통과시켜 실제 css 룰셋을 생성
    const processedStyles = runStylesThroughStylis(styles);
    // class명과 css룰셋을 cssom에 삽입함
    createAndInjectCSSClass(uniqueClassName, processedStyles);
    return <h1 className={uniqueClassName} {...props} />
  }
}

2. styled-components가 tag를 결정하는 방법

아래 문법은 실제로 사용할 수 있음

  const Tag = 'button';
  return <Tag>Hello</Tag>

아래 코드는 바벨 등의 트랜스파일러를 통해 다음과 같이 변경됨.

즉, 첫번째 인자가 컴포넌트냐, 문자열이냐는 중요하지 않음

(소문자면 htmlEement, 첫번째 글자가 대문자면 컴포넌트로 판단하기에 네이밍 규칭이 중요한 것)

const Tag = 'button';
React.createElement(Tag, {}, "Hello");

따라서 1번에서 설명한 코드는 실제로 아래와 유사하다고 생각할 수 있음

const styled = (Tag) => (styles) => {
  return function NewComponent(props) {
    const uniqueClassName = comeUpWithUniqueName(styles);
    createAndInjectCSSClass(uniqueClassName, styles);
    return <Tag className={uniqueClassName} {...props} />
  }
}

3. 컴포넌트 합성

1번에서 설명하였듯, 실제 styled-components는 className을 이용해 컴포넌트에 스타일을 적용함

따라서, 합성 대상 컴포넌트에 className 프롭이 없으면 동작하지 않음

 

아래 코드는 합성할 수 있음

function Message({ children, ...delegated }) {
  console.log(delegated);
  // { className: 'OkjqvF' }
  return (
    <p {...delegated}>
      You've received a message: {children}
    </p>
  );
}

아래 코드는 합성할 수 없음

function Message({ children }) {
  /*
    Because we're ignoring the `className` prop,
    the styles will never be set.
  */
  return (
    <p>
      You've received a message: {children}
    </p>
  );
}

지금까지 합성을 잘 활용해왔다면, 합성한 컴포넌트가 위의 컨벤션을 잘 따라온 것임.

 

또한 CSS 규칙은 충돌 시에, 뒤에 나오는 규칙을 적용하도록 되어있음.

예를 들어 아래의 두 paragraph내 텍스트는 전부 파란색으로 렌더링 됨.

<style>
.red {
  color: red;
}
.blue {
  color: blue;
}
</style>
<!-- blue -->
<p class="blue red">Hello</p>
<!-- blud -->
<p class="red blue">Hello</p>

styled-components는 리액트 컴포넌트 함수(명)와 렌더링 순서를 고려하여 css 규칙을 추가하여

해당 문제를 알아서 해결해줌

const styled = (Tag) => (styles) => {
  return function NewComponent(props) {
    const uniqueClassName = comeUpWithUniqueName(styles);
    const processedStyles = runStylesThroughStylis(styles);
    createAndInjectCSSClass(uniqueClassName, processedStyles);
    // 클래스 명을 결합하여 반영
    const combinedClasses =
      [uniqueClassName, props.className].join(' ');
    return <Tag {...props} className={combinedClasses} />
  }
}

4. Style 보간(Interpolated styles)

아래 styled-components를 활용하여

const ContentImage = styled.img`
  display: block;
  margin-bottom: 8px;
  width: 100%;
  max-width: ${p => p.maxWidth};
`;

다음 JSX를 렌더링 하였다고 가정하면,

  <>
    <ContentImage
      alt="A running shoe with pink laces and a rainbow decal"
      src="/images/shoe.png"
      maxWidth="200px"
    />
    <ContentImage
      alt="A close-up shot of the same running shoe"
      src="/images/shoe-closeup.png"
    />
  </>

아래와 같이 css 규칙이 두 개 생긴다.

<style>
  .JDSLg {
    display: block;
    margin-bottom: 8px;
    width: 100%;
    max-width: 200px;
  }
  .eXyedY {
    display: block;
    margin-bottom: 8px;
    width: 100%;
  }
</style>
<img
  alt="A running shoe with pink laces and a rainbow decal"
  src="/images/shoe.png"
  class="sc-bdnxRM JDSLg"
/>
<img
  alt="A close-up shot of the same running shoe"
  src="/images/shoe-closeup.png"
  class="sc-bdnxRM eXyedY"
/>

이는, css가 런타임에 생성 및 평가된다는 것과, 사전에 css 규칙들을 미리 생성해 둘 수 없다는 것을 의미한다.

 

따라서, 해당 사용사례는 공식 문서에서 추천하지 않는 방법이다. 

상항에 따라 너무 많은 스타일 규칙이 생성될 수 있다.

 

한가지 방법은 style 속성을 이용하는 것이다.(참고 : use-the-style-prop-for-dynamic-styles)

<ContentImage
  alt="A running shoe with pink laces and a rainbow decal"
  src="/images/shoe.png"
  style={{maxWidth : '200px'}}
/>

 

다른 방법은 css variable를 사용하는 것이다. (참고 : advanced-css-variables-with-style)

이전의 예시는 아래와 같이 변경될 것이다.

const ContentImage = styled.img`
  display: block;
  margin-bottom: 8px;
  width: 100%;
  max-width: var(--max-width);
`;

const Component = ()=>(
  <>
    <ContentImage
      alt="A running shoe with pink laces and a rainbow decal"
      src="/images/shoe.png"
      style={{
        '--max-width': '200px',
      }}
    />
    <ContentImage
      alt="A close-up shot of the same running shoe"
      src="/images/shoe-closeup.png"
    />
  </>
)

css Variables는 디폴트 값 사용도 허용한다.

const Wrapper = styled.div`
  opacity: var(--opacity, 0.75);
  background-color: var(--color, var(--color-gray-900));
`;

 

링크에서 언급하는 방법을 통해 타입스크립트 지원을 추가할 수 있다.

how-to-define-css-variables-in-style-attribute-in-react-and-typescript

 

How to define css variables in style attribute in React and typescript

I want to define jsx like this: <table style={{'--length': array.lenght}}> <tbody> <tr>{array}</tr> </tbody> </table> and I use --length in CSS, I a...

stackoverflow.com

정리

지금까지 내용을 코드로 옮긴다면 다음과 같이 될 것이다.

styled-components 자체적인 최적화 로직과, 태그드 탬플릿 리터럴을 처리하는 부분들을 매우 간소화 한다면 말이다.

const styled = (Tag) => (rawStyles, ...interpolations) => {
  return function NewComponent(props) {
    /*
      템플릿 문자열을 이용해 스타일을 계산,
      React props와 보간 함수를 고려해 스타일 규칙 생성
    */
    const styles = reconcileStyles(
      rawStyles,
      interpolations,
      props
    )
    /* 나머지는 이전과 동일: */
    const uniqueClassName = comeUpWithUniqueName(styles);
    const processedStyles = runStylesThroughStylis(styles);
    createAndInjectCSSClass(uniqueClassName, processedStyles);
    const combinedClasses =
      [uniqueClassName, props.className].join(' ');
    return <Tag {...props} className={combinedClasses} />
  }
}

지금까지의 다룬 내용을 요약하자면 다음과 같다.

  1. styled-components는 컴포넌트 명과 css 규칙을 이용하여 className명(해시)을 만들고, class를 이용해 컴포넌트에 css를 적용
  2. css는 런타임에 평가되므로, 렌더링 대상 컴포넌트들에 대해서만 css 평가 및 cssom 조작 비용을 지불.
  3. 컴포넌트 합성을 위해선 className 속성을 지원해야 함. 규칙 충돌 시 오버라이딩하는 규칙 적용은 styled-components가 핸들링.
  4. 동적 스타일이 필요하면, style interpolation보다 css variables를 사용하는 것이 좋음

참고

https://www.joshwcomeau.com/react/demystifying-styled-components/

 

Demystifying styled-components

For so many React devs, styled-components seems kinda magical. It isn't at all clear how it uses traditional CSS features under-the-hood, and that lack of clarity can cause real problems when things go awry. In this post, we'll learn exactly how styled-com

www.joshwcomeau.com

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

 

 

반응형