StyleX란 CSS-In-Js 라는 라이브러리 사용자들의 경험 유지하고 컴파일 이전의 기능(ex: TypeScript)을 사용하여 정적 CSS의 성능 및 확장성과 연결합니다.
CSS-In-JS의 대표적인 라이브러리로는 Emotion, Style-Component 등이 있습니다.
StyleX는 CSS-In-JS의 경험을 유지하지만 기존의 단순한 라이브러리들과 다른 점이 있습니다. 바로 대규모 애플리케이션, 재사용이 가능한 라이브러리, 정적으로 타입을 지정 할 수 있습니다.
확장성
예측 가능성
재사용성
속도성
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}],
},
};
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
base: {
fontSize: 16,
lineHeight: 1.5,
color: 'rgb(60,60,60)',
},
highlighted: {
color: 'rebeccapurple',
},
});
가상 선택자를 사용하는 경우 backgroundColor.default와 같이 기본(default)을 설정해주는 것이 좋습니다.
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
button: {
backgroundColor: {
default: 'lightblue',
':hover': 'blue',
':active': 'darkblue',
},
},
});
가상 선택자와 동일하게 기본(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,
},
},
});
미디어 쿼리와 가상 선택자를 결합하는 경우 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)',
},
},
});
대부분의 브라우저는 모든 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'),
},
});
애니메이션을 정의하여 사용할 수 있습니다.
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',
},
});
동적 스타일의 경우 고급 기능이므로 드물게 사용해야 합니다. (컴파일 단계에서 모든 스타일을 생성하므로)
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))} />;
}
// 💩 bad
<CustomComponent style={stylex.props(styles.base)} />
// 😀 good
<CustomComponent style={[styles.base, isHighlighted && styles.highlighted]} />
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)} />;
}
스타일을 해제할 때는 해당 값을 null로 설정하면 해제가 가능합니다.
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
base: {
color: null,
},
});
1-1. 변수를 정의할 때 가장 중요한 규칙이 있습니다. .stylex.* 파일내에 정의가 되어 있어야 합니다.
1-2. 모든 변수들은 export로 내보내 사용해야 합니다.
// ✅ - Named export
export const colors = stylex.defineVars({
/* ... */
});
const sizeVars = { ... };
// ✅ - Another Named export
export const sizes = stylex.defineVars(sizeVars);
변수 그룹은 다음 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',
});
변수 값은 미디어 쿼리에 따라 달라질 수 있습니다.
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'},
});
다음 변수가 다음이라는 파일에 정의되어 있다고 가정합니다 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,
},
});
테마를 이용하여 특정 컴포넌트 하위의 파일들의 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>;
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는 임의의 정적 스타일을 허용하지만 동적 스타일은 허용하지 않습니다.
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
}>,
};
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를 사용하여 프로젝트를 진행하기로 하였습니다.