CSS 도 타입으로 쓰자 (Vanilla Extract) -2

최예준·2024년 11월 27일

공부

목록 보기
15/19

Vanilla Extract 의 다양한 API 들

1. Theme

다양한 스타일 라이브러리나 큰 프로젝트를 경험했다면 색상, 크기, 길이, 너비 등 다양한 값들을 px 혹은 % 로 각각 박아서 쓰지 않고,
획일화된 크기나 값으로 테마화 해서 사용하는 것을 알 것이다.

예를 들자면 fontSize : sm : 12px / md: 16px / lg : 18px 요런식으로,

이렇게 프로젝트에서 테마로 사용될 다양한 변수들을 미리 정의하고, 또 다크모드 와 같이 특정 테마에서 의 스타일 변수등을 미리 지정하는데 사용하는 옵션이다.

createGlobalTheme

전체 애플리케션에서 사용될 전역적인 테마 변수를 정의할 수 있는 함수다.

첫번째는 루트 선택자 이고 두번째는 오브젝트다.


const commonVars = createGlobalTheme(':root', {
    color: {
        primary: '#3361FF',
        white: '#FFFFFF',
        red: '#FF3737',
        green: '#00D98B',
        yellow: '#FFB800',
...

위처럼 선언해 놓고 나중에 commonVars.color.primary 이런식으로 가져다가 쓴다.

createThemeContract

테마의 구조 를 정의하는 함수이다. 우리가 다크 모드와 같이 전반적인 테마를 사용해야 할때 유용하게 쓰이는데,

미리 테마의 구조를 잡고, 기본일때, 다크모드일때 의 css 속성을 유동적으로 교체 할 수 있게 해준다.

const themeColor = createThemeContract({
    border: null,
    overlay: null,
    background: {
        primary: null,
        layout: null,
    },
    text: {
        default: null,
        primary: null,
        secondary: null,
    },
    ...

위처럼 선언된 테마 구조를 아래와 같이 사용한다.


// 일반 모드일때 

createGlobalTheme(':root', themeColor, {
    border: commonVars.color.gray['200'],
    overlay: 'rgba(24, 28, 64, 0.50)',
    background: {
        primary: commonVars.color.white,
        layout: 'linear-gradient(to bottom, #3361ff 274px, #F4F5FA 274px)',
    },
    text: {
        default: commonVars.color.blue['200'],
        primary: commonVars.color.blue['200'],
        secondary: commonVars.color.gray['400'],
    },

}

// 다크 모드일때 
    createGlobalTheme('.dark', themeColor, {
        border: commonVars.color.black['200'],
        overlay: 'rgba(0, 0, 0, 0.70)',
        background: {
            primary: commonVars.color.black['100'],
            layout: commonVars.color.black['100'],
        },
        text: {
            default: commonVars.color.white,
            primary: commonVars.color.gray['400'],
            secondary: commonVars.color.gray['400'],
        },
    }

이렇게 하면 동일한 구조를 사용해서 각 테마에 맞게 css 속성을 쉽게 교체할 수 있다.

2. Sprinkles

위 createThemeContract 가 다크모드 만들때 주로 쓰인다면 Sprinkles 는 주로 반응형 작업할때 쓴다고 생각하면 좋다.

Sprinkles 를 사용하려면 먼저 defineProperties 를 만들어야 한다.
예를들면 mobile : 미디어 쿼리 몇몇 tablet 은 미디어쿼리 몇몇 이런식으로?

빠르게 예시 코드를 확인해보자


const responsiveProperties = defineProperties({
  // 조건 정의
  conditions: {
    mobile: {}, // 기본 (mobile)
    tablet: { '@media': 'screen and (min-width: 768px)' }, // 태블릿 이상
    desktop: { '@media': 'screen and (min-width: 1024px)' }, // 데스크톱 이상
  },
  defaultCondition: 'mobile', // 조건이 없으면 mobile이 기본값
  properties: {
    color: ['red', 'blue', 'green'], // 사용할 색상 값
    padding: ['sm', 'md', 'lg'],    // 사용할 패딩 값
    margin: ['sm', 'md', 'lg'],     // 사용할 마진 값
  },
});

이렇게 defineProperties 를 통해 조건들을 작성하고,

const buttonStyle = sprinkles({
  color: {
    mobile: 'red',   // 기본 상태: 빨간색
    tablet: 'blue',  // 태블릿 이상: 파란색
  },
  padding: {
    mobile: 'sm',    // 기본 상태: 작은 패딩
    tablet: 'md',    // 태블릿 이상: 중간 패딩
  },
});

sprinkles 를 통해 이렇게 사용할 수 있다.

이게 실제로는

.sprinkles1 {
  color: red;
  padding: var(--padding-sm);
}

@media screen and (min-width: 768px) {
  .sprinkles1 {
    color: blue;
    padding: var(--padding-md);
  }
}

이런식으로 만들어진다.

3. Recipes

아마 제일 많이 쓰게될 친구이지 않을까 싶다.

이름 그대로 컴포넌트에서 다양한 props 를 가지고 조건을 걸어서 스타일의 레시피? 를 만들어 쓰는 느낌이다

바로 예제를 보자

import { buttonStyle } from './button.css';

const Button = ({ colorSchema = 'primary', size = 'small' }) => {
  return <button className={buttonStyle({ color, size })}>Click Me</button>;
};

// 스타일
export const buttonStyle = recipe({
    base: {
        fontWeight: 'bold',
        borderRadius: '8px',
        padding: '12px 24px',
    },
    variants: {
        colorSchema: {
            primary: { backgroundColor: 'blue', color: 'white' },
            secondary: { backgroundColor: 'gray', color: 'black' },
        },
        size: {
            small: { fontSize: '12px', padding: '8px 16px' },
            large: { fontSize: '18px', padding: '16px 32px' },
        },
    },
    defaultVariants: {
        colorSchema: 'primary',
        size: 'small',
    },
});

대충 감이 올 것이다. 주로 공통컴포넌트 만들때 자주 보게 될 친구인데,

Button 에 colorSchema 와 size 를 props 로 던지면,
각 props 에 맞는 스타일들을 종합적으로 만들 수 있게 된다.

- variants 와 defaultVariants

위 소스에서 볼 수 있듯, recipe 의 기본 성질은 base 에 , 레시피북은 variants,
그리고 디폴트값을 정할 수 있는 defaultVariants 가 있다.

- compoundVariants

함수 닉값 처럼 들어오는 props 의 값을 조합해서 특정한 값을 만들 수 있다.
(사용하기 조금 복잡하다...)

얼른 예시를 보자

import { buttonStyle } from './button.css';

export const Button = ({ variant = 'primary', size = 'small' }) => {
  return (
    <button className={buttonStyle({ variant, size })}>
      Click Me
    </button>
  );
};

// 스타일 영역 

import { recipe } from '@vanilla-extract/recipes';

export const buttonStyle = recipe({
    base: {
        fontWeight: 'bold',
        borderRadius: '8px',
        padding: '12px 24px',
    },
    variants: {
        colorSchema: {
            primary: { backgroundColor: 'blue', color: 'white' },
            secondary: { backgroundColor: 'gray', color: 'black' },
        },
        size: {
            small: { fontSize: '12px' },
            large: { fontSize: '18px' },
        },
    },
    compoundVariants: [
        {
            variants: { colorSchema: 'primary', size: 'small' },
            style: { backgroundColor: 'darkblue' }, // primary + small 조합일 때
        },
        {
            variants: { colorSchema: 'secondary', size: 'large' },
            style: { backgroundColor: 'darkgray' }, // secondary + large 조합일 때
        },
    ],
    defaultVariants: {
        colorSchema: 'primary',
        size: 'small',
    },
});

위 compoundVariants 를 자세히 살펴보자
variants 값으로 즉 props 로 colorSchema : primary , size : 'small' 일때 아래에 있는 style 에 작성된 스타일이 적용된다.

즉 Variants 조합이 너무 많아지거나 특정한 조건의 경우 다른 스타일이 필요하다면 적절히 사용해 볼 수 있을것 같다.
(근데 너무 남발하면 어려울듯...)

Recipes 타입으로 사용하기

Recipes 를 쓸때 해당 컴포넌트의 props 의 타입과 Recipes 를 같이 관리해주기가 귀찮을 수 도 있다.
이럴땐 Recipes 에 variants 의 값들도 타입으로 끌어다가 쓸 수 있는데 이러면 우리가 컴포넌트에다가 props 의 타입을 따로 관리 안해줘도
Recipes 에 variants 로 추가되면 자동으로 타입이 되니 이 얼마나 편한가?

바로 예시 소스를 보자

import { ButtonHTMLAttributes, ReactNode } from 'react';
import { RecipeVariants } from '@vanilla-extract/recipes';
import { button } from './button.css.ts';

type ButtonVariants = RecipeVariants<typeof button>;

type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
    ButtonVariants & {
        label?: string;
        icon?: ReactNode;
    };

const Button = ({
                    label = '',
                    onClick,
                    icon,
                    ...variant
                }: ButtonProps) => {
    return (
        <button
            className={button(variant)}
            onClick={onClick}
            style={{ width }}
        >
            {icon}
            {label}
        </button>
    );
};

export default Button;


/// 스타일 
// button.css.ts
import { recipe } from '@vanilla-extract/recipes';

export const button = recipe({
    base: {
        padding: '10px 20px',
        borderRadius: '4px',
        fontWeight: 'bold',
        cursor: 'pointer',
    },
    variants: {
        variant: {
            primary: { backgroundColor: 'blue', color: 'white' },
            secondary: { backgroundColor: 'gray', color: 'black' },
        },
        size: {
            small: { fontSize: '12px' },
            medium: { fontSize: '16px' },
            large: { fontSize: '20px' },
        },
    },
    defaultVariants: {
        variant: 'primary',
        size: 'medium',
    },
});

보면

type ButtonProps = ButtonHTMLAttributes &
ButtonVariants & {
label?: string;
icon?: ReactNode;
};

이것처럼 ButtonVariants 타입을 가져와 사용해서
Variants 로 선언한 스타일 구조를 바로 타입으로 사용할 수 있게 된다.

날먹 가능!

profile
개발도 잘하고픈 행복한 개발자

0개의 댓글