StyleX 뿌수기

Zero·2024년 2월 7일

[라이브러리]

목록 보기
2/3
post-thumbnail

StyleX란?

StyleX란 CSS-In-Js 라는 라이브러리 사용자들의 경험 유지하고 컴파일 이전의 기능(ex: TypeScript)을 사용하여 정적 CSS의 성능 및 확장성과 연결합니다.

CSS-In-JS의 대표적인 라이브러리로는 Emotion, Style-Component 등이 있습니다.

StyleX의 특징

StyleX는 CSS-In-JS의 경험을 유지하지만 기존의 단순한 라이브러리들과 다른 점이 있습니다. 바로 대규모 애플리케이션, 재사용이 가능한 라이브러리, 정적으로 타입을 지정 할 수 있습니다.

확장성

  • 원자 단위의 CSS로 CSS 출력을 최소화합니다.
  • 구성 요소가 늘어나도 CSS 크기는 변하지 않습니다.
  • 스타일이 코드베이스에서 많아지더라도 읽기 쉽고 유지 관리가 가능합니다.

예측 가능성

  • 클래스 이름과 동일한 요소에만 직접 스타일을 지정할 수 있습니다.
  • 특이성 문제는 없습니다.
  • 동일한 스타일이 있더라도 마지막으로 적용된 스타일의 경우가 적용이 됩니다.

재사용성

  • 조건부로 스타일을 적용할 수 있습니다.
  • 다른 스타일이나 파일 경계를 넘어 임의의 스타일을 병합하거나 구성할 수 있습니다.
    • 스타일을 DRY로 관리하고 싶다면 지역 상수와 표현식을 사용하면 가능합니다. 이를 통해 무한히 반복해서 재사용이 가능합니다.

속도성

  • 런타임시에 스타일을 적용하지 않습니다. (컴파일 단계에서 적용합니다.)
    • 동적으로 적용할 수도 있습니다.
  • 모든 스타일은 컴파일 타임에 정적 CSS 파일에 번들로 적용됩니다.
  • 클래스 이름 병합 위해 런타임에 최적화되어 있습니다.

설치방법

styleX 설치

npm install --save @stylexjs/stylex

컴파일러 구성

babel 플러그인 설치

npm install --save-dev @stylexjs/babel-plugin
// babel.config.js
import styleXPlugin from '@stylexjs/babel-plugin';

const config = {
  plugins: [
    [
      styleXPlugin,
      {
        dev: true,
        // Set this to true for snapshot testing
        // default: false
        test: false,
        // Required for CSS variable support
        unstable_moduleResolution: {
          // type: 'commonJS' | 'haste'
          // default: 'commonJS'
          type: 'commonJS',
          // The absolute path to the root directory of your project
          rootDir: __dirname,
        },
      },
    ],
  ],
};

export default config;

next 플러그인 설치

npm install --save-dev @stylexjs/nextjs-plugin
// .babelrc.js

module.exports = {
  presets: ['next/babel'],
  plugins: [
    [
      '@stylexjs/babel-plugin',
      {
        dev: process.env.NODE_ENV === 'development',
        runtimeInjection: false,
        genConditionalClasses: true,
        treeshakeCompensation: true,
        unstable_moduleResolution: {
          type: 'commonJS',
          rootDir: __dirname,
        },
      },
    ],
  ],
};
// next.config.js

/** @type {import('next').NextConfig} */
const stylexPlugin = require('@stylexjs/nextjs-plugin');

const nextConfig = {
  // Configure `pageExtensions` to include MDX files
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
  // Optionally, add any other Next.js config below
};

module.exports = stylexPlugin({
  filename: 'stylex-bundle.css',
  rootDir: __dirname,
  useCSSLayers: true,
})(nextConfig);

eslint 설정

npm install --save-dev @stylexjs/eslint-plugin
// .eslintrc.js

module.exports = {
  plugins: ["@stylexjs"],
  rules: {
    "@stylexjs/valid-styles": ["error", {...options}],
  },
};

사용 방법

1. 스타일 만들기

import * as stylex from '@stylexjs/stylex';

const styles = stylex.create({
  base: {
    fontSize: 16,
    lineHeight: 1.5,
    color: 'rgb(60,60,60)',
  },
  highlighted: {
    color: 'rebeccapurple',
  },
});

2. 가상 선택자 사용하기

가상 선택자를 사용하는 경우 backgroundColor.default와 같이 기본(default)을 설정해주는 것이 좋습니다.

import * as stylex from '@stylexjs/stylex';

const styles = stylex.create({
  button: {
    backgroundColor: {
      default: 'lightblue',
      ':hover': 'blue',
      ':active': 'darkblue',
    },
  },
});

3. 미디어 쿼리 사용하기

가상 선택자와 동일하게 기본(default)를 설정해주는 것이 좋습니다.

import * as stylex from '@stylexjs/stylex';

const styles = stylex.create({
  base: {
    width: {
      default: 800,
      '@media (max-width: 800px)': '100%',
      '@media (min-width: 1540px)': 1366,
    },
  },
});

4. 결합

미디어 쿼리와 가상 선택자를 결합하는 경우 depth가 깊어질 수 있습니다.

import * as stylex from '@stylexjs/stylex';
    
const styles = stylex.create({
  button: {
    color: {
    default: 'var(--blue-link)',
    ':hover': {
    default: null,
    '@media (hover: hover)': 'scale(1.1)',
    },
    ':active': 'scale(0.9)',
    },
  },
});

5. 대체 값 사용하기

대부분의 브라우저는 모든 css 속성을 지원하지만 특정 브라우저의 경우 지원하지 않는 속성이 있을 수도 있습니다.

기존 css에서는 다음과 같이 사용하였습니다.

.header {
  position: fixed;
  position: -webkit-sticky;
  position: sticky;
}

styleX에서는 firstThatWorks 를 활용하여 동일한 기능을 얻을 수 있습니다.

import * as stylex from '@stylexjs/stylex';
    
const styles = stylex.create({
  header: {
    position: stylex.firstThatWorks('sticky', '-webkit-sticky', 'fixed'),
  },
});

6. 애니메이션 (key frame)

애니메이션을 정의하여 사용할 수 있습니다.

import * as stylex from '@stylexjs/stylex';
    
const fadeIn = stylex.keyframes({
  from: {opacity: 0},
  to: {opacity: 1},
});

const styles = stylex.create({
  base: {
    animationName: fadeIn,
    animationDuration: '1s',
  },
});

6. 동적 스타일

동적 스타일의 경우 고급 기능이므로 드물게 사용해야 합니다. (컴파일 단계에서 모든 스타일을 생성하므로)

import * as stylex from '@stylexjs/stylex';

const styles = stylex.create({
  // Function arguments must be simple identifiers
  // -- No destructuring or default values
  bar: (height) => ({
    height,
    // The function body must be an object literal
    // -- { return {} } is not allowed
  }),
});

function MyComponent() {
  // The value of `height` cannot be known at compile time.
  const [height, setHeight] = useState(10);

  return <div {...stylex.props(styles.bar(height))} />;
}

7. props로 스타일을 넘기는 방법

// 💩 bad
<CustomComponent style={stylex.props(styles.base)} />

// 😀 good
<CustomComponent style={[styles.base, isHighlighted && styles.highlighted]} />

8. 구성요소에서 사용하는 방법

import * as stylex from '@stylexjs/stylex';

type Props = {
  ...
  style?: StyleXStyles,
};

// Local Styles
const styles = stylex.create({
  base: {
    /*...*/
  },
});

function CustomComponent({style}:Props) {
  return <div {...stylex.props(styles.base, style)} />;
}

9. 설정 해제하는 방법

스타일을 해제할 때는 해당 값을 null로 설정하면 해제가 가능합니다.

import * as stylex from '@stylexjs/stylex';

const styles = stylex.create({
  base: {
    color: null,
  },
});

변수

1. 규칙

1-1. 변수를 정의할 때 가장 중요한 규칙이 있습니다. .stylex.* 파일내에 정의가 되어 있어야 합니다.

1-2. 모든 변수들은 export로 내보내 사용해야 합니다.

// ✅ - Named export
export const colors = stylex.defineVars({
  /* ... */
});

const sizeVars = { ... };
                  
// ✅ - Another Named export
export const sizes = stylex.defineVars(sizeVars);

2. 변수 정의

변수 그룹은 다음 stylex.defineVars함수를 사용하여 정의됩니다.

import * as stylex from '@stylexjs/stylex';

export const tokens = stylex.defineVars({
  primaryText: 'black',
  secondaryText: '#333',
  accent: 'blue',
  background: 'white',
  lineColor: 'gray',
  borderRadius: '4px',
  fontFamily: 'system-ui, sans-serif',
  fontSize: '16px',
});

3. 미디어 쿼리 (Dark 모드)

변수 값은 미디어 쿼리에 따라 달라질 수 있습니다.

import * as stylex from '@stylexjs/stylex';

// A constant can be used to avoid repeating the media query
const DARK = '@media (prefers-color-scheme: dark)';

export const colors = stylex.defineVars({
  primaryText: {default: 'black', [DARK]: 'white'},
  secondaryText: {default: '#333', [DARK]: '#ccc'},
  accent: {default: 'blue', [DARK]: 'lightblue'},
  background: {default: 'white', [DARK]: 'black'},
  lineColor: {default: 'gray', [DARK]: 'lightgray'},
});

4. 변수 사용

다음 변수가 다음이라는 파일에 정의되어 있다고 가정합니다 tokens.stylex.js.

import * as stylex from '@stylexjs/stylex';

// A constant can be used to avoid repeating the media query
const DARK = '@media (prefers-color-scheme: dark)';

export const colors = stylex.defineVars({
  primaryText: {default: 'black', [DARK]: 'white'},
  secondaryText: {default: '#333', [DARK]: '#ccc'},
  accent: {default: 'blue', [DARK]: 'lightblue'},
  background: {default: 'white', [DARK]: 'black'},
  lineColor: {default: 'gray', [DARK]: 'lightgray'},
});

export const spacing = stylex.defineVars({
  none: '0px',
  xsmall: '4px',
  small: '8px',
  medium: '12px',
  large: '20px',
  xlarge: '32px',
  xxlarge: '48px',
  xxxlarge: '96px',
});

다음과 같이 사용할 수 있습니다.

import * as stylex from '@stylexjs/stylex';
import {colors, spacing} from '../tokens.stylex';

const styles = stylex.create({
  container: {
    color: colors.primaryText,
    backgroundColor: colors.background,
    padding: spacing.medium,
  },
});

5. 테마

테마를 이용하여 특정 컴포넌트 하위의 파일들의 CSS 값들을 재정의 할 수 있습니다.

테마 선언

// theme

import * as stylex from '@stylexjs/stylex';
import {colors, spacing} from './tokens.stylex';

// A constant can be used to avoid repeating the media query
const DARK = '@media (prefers-color-scheme: dark)';

// Dracula theme
export const dracula = stylex.createTheme(colors, {
  primaryText: {default: 'purple', [DARK]: 'lightpurple'},
  secondaryText: {default: 'pink', [DARK]: 'hotpink'},
  accent: 'red',
  background: {default: '#555', [DARK]: 'black'},
  lineColor: 'red',
});

테마 사용

import * as stylex from '@stylexjs/stylex';
import {colors, spacing} from '../tokens.styles';
import {dracula} from '../themes';

const styles = stylex.create({
  container: {
    color: colors.primaryText,
    backgroundColor: colors.background,
    padding: spacing.medium,
  },
});

<div {...stylex.props(dracula, styles.container)}>{children}</div>;

타입 지원

1. StyleXStyles 스타일 props로 받기 지원

StyleX는 정적 유형을 완벽하게 지원합니다. 가장 일반적인 유틸리티 유형은 StyleXStyles임의의 StyleX 스타일을 허용하는 데 사용됩니다.

import type {StyleXStyles} from '@stylexjs/stylex';
import * as stylex from '@stylexjs/stylex';

type Props = {
  ...
  style?: StyleXStyles,
};

function MyComponent({style, ...otherProps}: Props) {
  return (
    <div
    {...stylex.props(localStyles.foo, localStyles.bar, style)}
>
  {/* ... */}
</div>
);
}

💡 StaticStyles 는 임의의 정적 스타일을 허용하지만 동적 스타일은 허용하지 않습니다.

2. StyleXStyles 허용되는 스타일 지원

StyleXStyles<T> 를 활용하여 허용되는 스타일을 제한할 수 있습니다.

import type {StyleXStyles} from '@stylexjs/stylex';

type Props = {
// ...
style?: StyleXStyles<{
  color?: string;
  backgroundColor?: string;
  borderColor?: string;
  borderTopColor?: string;
  borderEndColor?: string;
  borderBottomColor?: string;
  borderStartColor?: string;
  }>;
};

값 또한 제한 할 수 있습니다.

import type {StyleXStyles} from '@stylexjs/stylex';

type Props = {
// Only accept styles for marginTop and nothing else.
// The value for marginTop can only be 0, 4, 8 or 16.
style?: StyleXStyles<{
  marginTop: 0 | 4 | 8 | 16
  }>,
};

3.StyleXStylesWithout 제한되는 스타일 지원

StyleXStylesWithout 를 통해 전달을 안받는 스타일을 지원하는 경우가 더 간편할 수도 있습니다.

import type {StyleXStylesWithout} from '@stylexjs/stylex';
import * as stylex from '@stylexjs/stylex';
    
type NoLayout = StyleXStylesWithout<{
      position: unknown,
      display: unknown,
      top: unknown,
      start: unknown,
      end: unknown,
      bottom: unknown,
      border: unknown,
      borderWidth: unknown,
      borderBottomWidth: unknown,
      borderEndWidth: unknown,
      borderStartWidth: unknown,
      borderTopWidth: unknown,
      margin: unknown,
      marginBottom: unknown,
      marginEnd: unknown,
      marginStart: unknown,
      marginTop: unknown,
      padding: unknown,
      paddingBottom: unknown,
      paddingEnd: unknown,
      paddingStart: unknown,
      paddingTop: unknown,
      width: unknown,
      height: unknown,
      flexBasis: unknown,
      overflow: unknown,
      overflowX: unknown,
      overflowY: unknown,
    }>;
    
  type Props = {
  // ...
  style?: NoLayout,
  };

  function MyComponent({style, ...}: Props) {
  return (
  <div
       {...stylex.props(localStyles.foo, localStyles.bar, style)}
       >
    {/* ... */}
  </div>
  );
  }

사용 후기

styleX를 사용하는 것은 새로운 문법에 적응한다면 사용하는 것은 보기보다 쉬웠습니다. css 속성들을 직접 입력을 해주어야 했기 때문에 css에 느슨해진 저같은 사람들을 위해서 딱 맞는 라이브러리라고 생각하였습니다.

하지만 styleX를 사용할 경우 swc를 비활성화 해야합니다. 최근 프로젝트를 진행을 할 경우 Next.JS를 사용을 하였기에 해당 swc를 포기할 경우 놓치는 이점이 더 크다고 생각하였습니다.

때문에 vanilla extract라던지 tailwind라는 여러 갈림길이 있었지만 아직 Next.js의 app router에 대한 지원이 styleX에 이어서 vanila extract에서도 많이 이뤄지지 않는 것 같았습니다. 때문에 이번에 들어가는 프로젝트에서는 tailwind를 사용하여 프로젝트를 진행하기로 하였습니다.

profile
0에서 시작해, 나만의 길을 만들어가는 개발자.

0개의 댓글