CSS-IN-JS는 어떻게 컴포넌트를 스타일링해줄 수 있는가?

LUCAS·2022년 11월 1일
0

CSS-IN-JS는 단어 그대로 Javascript 내에 CSS 문법을 사용하는 방식을 의미합니다.
본고에서는 기본적인 지식부터 학습해 어떻게 CSS-IN-JS가 동작하는지 알아보겠습니다.

전처리기

기존의 CSS는 웹 어플리케이션이 시대에 따라 점차 확장되어나가면서 한계점을 맞이하게 되었습니다.

CSS 문서를 작성해나가면서 반복적인 일을 하는 경우가 많았고 방대해진 CSS 문서를 유지보수하는 공수도 점차 커졌습니다.
이러한 문제점을 해결하기 위해서 변수, 함수, 상속 등 프로그래밍의 개념을 사용하였고, 그것이 우리가 알고있는 less, sass 등의 문법입니다.

CSS 전처리기는 이러한 파일을 실제로 웹사이트에서 사용하는 CSS 파일로 변환해주며, 이러한 처리는 다음과 같은 장단점을 지닙니다.

장점

  • 공통 요소 또는 반복적인 행위를 변수 또는 함수로 대체할 수 있습니다. (재사용성)
    예) 색상 코드를 따로 분리해, 반복적인 RGB선언을 줄이고 추상적인 재사용이 가능해집니다.
  • 임의 함수 및 built-in 함수의 사용을 통한 개발 시간적 비용을 절감할 수 있습니다. (시간적 비용 감소)
    예) 사용자가 작성한 믹스인 혹은 darken과 같은 내장 함수를 통해 선언적으로 스타일링을 할 수 있습니다.
  • 중첩 상속과 같은 요소로 인해 구조화된 코드로 유지 및 관리에 용이. (유지 관리)
    예) .StyleA .StyleB 상속, .StyleA { .StyleB } 중첩과 같이 기존의 코드를 구조화할 수 있습니다.

단점

  • 변수, 함수, 상속 등의 프로그래밍 개념을 따르는 전처리기 문법은 개발자의 입장에서는 러닝커브가 높지 않지만, 퍼블리싱을 진행하는 디자이너 입장에서는 그렇지 않을 수 있기에 이러한 부분까지 신경을 써주어야 합니다.

CSS-IN-JS가 처음부터 동적인 스타일을 생성하고, JS 내부에서 선언해 사용할 수 있게 되었던 것은 아니었습니다.

모든 기능이 그래왔듯이, CSS-IN-JS도 세대를 걸쳐 진화하게 됩니다.

1st Generation

처음부터 CSS파일을 JS에서 사용할 수 있었던 것은 아닙니다.
CSS-IN-JS의 초기 형태에서는 *.(module).css으로 파일을 생성하고, CSS 전처리기를 사용해 CSS Module 형태로 사용하였습니다.

import styles from './Button.module.css'

const Button = () => {
  return <button className={styles.submit}>제출</button>;
}

위와 같은 방식은 CSS를 정적으로 분석하여 별도의 CSS파일로 추출하는 방식입니다.
runtime으로서의 동작이 일절 없기에 zero runtime css-in-js의 특징을 갖습니다.

2nd Generation

JS변수를 활용하여 CSS를 작성할 수 있는 Radium과 같은 라이브러리가 등장합니다.
컴포넌트에서 스타일을 제어할 수 있는 형태였지만, inline style을 사용하므로 :before, :nth-child 등의 psuedo selector를 사용할 수 없는 등 CSS의 모든 Spec을 사용할 수 없었습니다.

의사 클래스 문서

// Radium: https://formidable.com/open-source/radium
const styles = {
  base: {
    background: 'blue',
  },

  block: {
    display: 'block'
  }
};

// Inside render
return (
  <button
    style={[
      styles.base,
      this.props.block && styles.block
    ]}>
    {this.props.children}
  </button>
);

3rd Generation

2세대에서 inline-style 방식을 채택하여 사용할 수 없게된 css syntax 문제를 해결하기 위해 aphrodite, glamor 등의 라이브러리에서는 다른 방식으로 스타일을 생성합니다.
Javascript 템플릿으로 CSS를 작성하게 될 경우 빌드 과정에서 <style> 태그를 생성하여 주입하게 됩니다.

styleTag = document.createElement('style');

styleTag.type = 'text/css';
styleTag.setAttribute("data-aphrodite", "");
head.appendChild(styleTag);
        

pesudo element, media query 등 기존에 지원하지 않았던 CSS Spec을 지원하기 시작하였으나, 동적으로 변경되는 스타일은 정의하기가 까다로웠습니다.

4th Generation

4세대에서는 3세대의 한계인 'JavaScript 코드로 제어하는 동적인 스타일링'을 runtime 개념을 도입하여 해결했습니다.

Runtime CSS-in-JS

// https://github.com/styled-components/styled-components/blob/8165cbe994f6f749236244f6f7017c2f0b9afcfe/packages/styled-components/src/constructors/constructWithOptions.ts#L39-L44
/* Modify/inject new props at runtime */
templateFunction.attrs = <Props = OuterProps>(attrs: Attrs<Props>) =>
  constructWithOptions<Constructor, Props>(componentConstructor, tag, {
    ...options,
    attrs: Array.prototype.concat(options.attrs, attrs).filter(Boolean),
  });

위 코드는 styled-components의 일부인데, prop가 변할 때마다 스타일을 동적으로 생성하여 JavaScript 코드로 동적인 스타일링이 가능해집니다. 즉, build time에서 모든 스타일을 생성하는 것이 아닌 runtime을 활용한 것입니다.

runtime에 스타일을 생성하는 방식은 대부분 문제가 없지만, 스타일 계산 비용이 커지기 때문에 스타일이 복잡한 컴포넌트에서는 차이가 발생합니다. (참고: necolas/react-native-web#benchmark)

Next Generation

복잡한 스타일 계산에서 발생하는 Runtime overhead를 해결하기 위해 zero-runtime을 주장하는 라이브러리 등장했습니다.

zero-runtime css-in-js

zero-runtime이란 말 그대로 앞서 설명한 runtime에서의 동작이 없다는 것을 의미합니다.
즉 동적으로 스타일을 생성하지 않는데, 이러한 문제점을 Linaria라는 라이브러리에서는 지혜롭게 풀어냈습니다.

Linaria라이브러리는 styled-components에서 영감을 받아 유사한 API를 가진 CSS-in-JS 라이브러리이고 zero-runtime으로 동작하고 있습니다.

이 라이브러리는 babel plugin과 webpack loader를 통해 사용된 css코드를 추출해 정적인 스타일시트를 생성합니다.

소스코드

import { styled } from '@linaria/react';
import { families, sizes } from './fonts';

const background = 'yellow';

const Title = styled.h1`
  font-family: ${families.serif};
`;

const Container = styled.div`
  font-size: ${sizes.medium}px;
  background-color: ${background};
  color: ${props => props.color};
  width: ${100 / 3}%;
  border: 1px solid red;

  &:hover {
    border-color: blue;
  }
`;

빌드결과:

.Title_t1ugh8t9 {
  font-family: var(--t1ugh8t-0);
}

.Container_c1ugh8t9 {
  font-size: var(--c1ugh8t-0);
  background-color: yellow;
  color: var(--c1ugh8t-2);
  width: 33.333333333333336%;
  border: 1px solid red;
}

.Container_c1ugh8t9:hover {
  border-color: blue;
}

1세대 zero-runtime Css-in-Js에서는 할 수 없었던 상태의 변화에 따른 동적인 스타일링이 가능해지는데, 그 이유로는 해당 라이브러리에서 내부적으로 css variable을 사용하고 있기 때문에, 스타일 시트를 새로 만들지 않고 해당 css variable만 수정하여 달라지는 조건에 대해 스타일을 다르게 해줄 수 있는 것입니다.

https://so-so.dev/web/css-in-js-whats-the-defference/

렌더링 최적화 기법, Critical CSS

브라우저 렌더링 동작에 의해, 우리는 사이트를 열게 되면 브라우저는 전체 CSS 파일을 받아 현재 페이지에 대한 스타일 처리를 진행하게 됩니다.

Critical CSS는 현재 화면에서 필요한 CSS만을 효율적으로 먼저 로드하는 방법인데요, 각 라이브러리에서는 어떻게 이를 지원하고 있을까요?

먼저, Styled-Component에서 상태값의 변화에 따라 변경되는 스타일은, 헤더 내의 DOM Tree에서 기존의 Style Tag에 동적 삽입이 됩니다. (개발 환경일 때만 DOM TREE 변경)

<style data-styled="active">
  .sktAwS{font-size:14px;}
  .drxWoN{font-size:16px;}  << 삽입
</style>

runtime으로 동작하는 emotion은 extractCritical을 통해 Styled-component와 비슷한 방식으로 제공합니다.

zero-runtime으로 동장하는 linaria는 빌드시 mini-css-extract-plugin과 같은 플러그인을 사용해 critical css를 추출합니다.

정리

방금 전 DOM Tree에 기존의 Style Tag가 동적 삽입된다고 설명드렸는데요.
이러한 방식을 DOM injection이라고 합니다.
Styled-component등이 개발 환경에서 동작 중일때 스타일 적용을 위해 사용하는 방식입니다.

다른 방식으로는 Stitches.js 라이브러리에서 사용하는 CSSStyleSheet API가 있습니다.
이 경우는 스타일을 CSSOM에 직접 삽입해, style 태그에는 빈 내용이 보이게 되지만, DevTools에서 직접 선택해 rule을 확인하는 형태로 결과를 확인할 수 있습니다.

출처: https://so-so.dev/web/css-in-js-whats-the-defference

profile
안녕하세요! FE개발자 최근원입니다.

0개의 댓글