슬롯 레시피

슬롯 레시피 (Slot Recipes)

여러 부분으로 구성된 컴포넌트에 스타일 변형을 적용해야 할 때 슬롯 레시피가 아주 유용해요.

출처: 문서

본문

개요

슬롯 레시피는 컴포넌트의 여러 부분에 스타일 변형을 적용할 필요가 있을 때 유용해요.

슬롯 레시피는 다음 속성들로 구성돼요.

  • className: 컴포넌트 슬롯에 붙일 className 접두사
  • slots: 스타일을 적용할 컴포넌트 부분들의 배열
  • base: 각 슬롯별 기본 스타일
  • variants: 각 슬롯별 다양한 시각적 스타일
  • defaultVariants: 컴포넌트의 기본 변형
  • compoundVariants: 컴포넌트의 복합 변형 조합과 각 슬롯별 스타일 오버라이드

레시피 정의하기

defineSlotRecipe 항등 함수로 슬롯 레시피를 만들어요.

import { defineSlotRecipe } from "@chakra-ui/react"

export const checkboxSlotRecipe = defineSlotRecipe({
  slots: ["root", "control", "label"],
  base: {
    root: { display: "flex", alignItems: "center", gap: "2" },
    control: { borderWidth: "1px", borderRadius: "sm" },
    label: { marginStart: "2" },
  },
  variants: {
    size: {
      sm: {
        control: { width: "8", height: "8" },
        label: { fontSize: "sm" },
      },
      md: {
        control: { width: "10", height: "10" },
        label: { fontSize: "md" },
      },
    },
  },
})

레시피 사용하기

컴포넌트에서 레시피를 사용하는 방법은 두 가지가 있어요.

  • 컴포넌트에서 직접 useSlotRecipe로 사용하기
  • createSlotRecipeContext로 컴파운드 컴포넌트를 만드는 방법 (권장)

useSlotRecipe 훅이나 createSlotRecipeContext 함수를 사용하려면 "use client" 지시어를 추가해야 해요. 내부적으로 useContext, useInsertionEffect 같은 react 훅에 의존하기 때문이에요.

컴포넌트에서 직접 사용하기

useSlotRecipe 훅으로 컴포넌트의 레시피를 가져와요. 그런 다음 레시피에 변형 props를 호출해 스타일을 얻어요.

"use client"

import { chakra, useSlotRecipe } from "@chakra-ui/react"
import { checkboxSlotRecipe } from "./checkbox.recipe"

export const Checkbox = (props) => {
  const { size, ...restProps } = props

  const recipe = useSlotRecipe({ recipe: checkboxSlotRecipe })
  const styles = recipe({ size })

  return (
    <chakra.label css={styles.root}>
      <chakra.input type="checkbox" css={styles.control} {...restProps} />
      <chakra.span css={styles.label}>Checkbox Label</chakra.span>
    </chakra.label>
  )
}
splitVariantProps

size prop을 props에서 구조 분해해 레시피에 전달한 걸 볼 수 있어요. 더 똑똑한 방법은 레시피 props를 컴포넌트 props에서 자동으로 분리하는 거예요.

recipe.splitVariantProps 함수로 레시피 props를 컴포넌트 props에서 분리할 수 있어요.

"use client"

import { chakra, useSlotRecipe } from "@chakra-ui/react"
import { checkboxSlotRecipe } from "./checkbox.recipe"

export const Checkbox = (props) => {
  const recipe = useSlotRecipe({ recipe: checkboxSlotRecipe })
  const [recipeProps, restProps] = recipe.splitVariantProps(props)
  const styles = recipe(recipeProps)

  //...
}
TypeScript

레시피 변형 prop 타입을 추론하려면 RecipeVariantProps 타입 헬퍼를 사용해요.

import type { RecipeVariantProps } from "@chakra-ui/react"
import { checkboxSlotRecipe } from "./checkbox.recipe"

type CheckboxVariantProps = RecipeVariantProps<typeof checkboxSlotRecipe>

export interface CheckboxProps extends React.PropsWithChildren<CheckboxVariantProps> {}

컴파운드 컴포넌트 만들기

createSlotRecipeContext 함수에 레시피를 전달해 슬롯 레시피 컨텍스트를 만들어요.

그런 다음 withProvider와 withContext 함수로 같은 컨텍스트를 공유하는 컴파운드 컴포넌트를 만들어요.

withProvider와 withContext의 제네릭을 수동으로 타입 지정해야 해요. 이 방식은 TypeScript 성능을 최적화하기 위해 설계됐어요. 자동 추론은 편리하지만, 관련된 타입이 복잡해 TypeScript 컴파일이 느려질 수 있어요.

"use client"

import { createSlotRecipeContext } from "@chakra-ui/react"
import { checkboxSlotRecipe } from "./checkbox.recipe"

const { withProvider, withContext } = createSlotRecipeContext({
  recipe: checkboxSlotRecipe,
})

interface CheckboxRootProps extends HTMLChakraProps<
  "label",
  RecipeVariantProps<typeof checkboxSlotRecipe>
> {}
export const CheckboxRoot = withProvider<HTMLLabelElement, CheckboxRootProps>(
  "label",
  "root",
)

interface CheckboxControlProps extends HTMLChakraProps<"input"> {}
export const CheckboxControl = withContext<
  HTMLInputElement,
  CheckboxControlProps
>("input", "control")

interface CheckboxLabelProps extends HTMLChakraProps<"span"> {}
export const CheckboxLabel = withContext<HTMLSpanElement, CheckboxLabelProps>(
  "span",
  "label",
)

"root" 컴포넌트에 변형 props를 전달해 스타일을 적용해요.

Note: root 컴포넌트가 withProvider 함수를 사용한 그 컴포넌트예요.

const App = () => {
  return (
    <CheckboxRoot size="md">
      <CheckboxControl />
      <CheckboxLabel />
    </CheckboxRoot>
  )
}
unstyled prop

이 방식은 unstyled prop을 지원해 레시피가 적용한 스타일을 제거할 수 있어요.

<CheckboxRoot unstyled>
  <CheckboxControl />
  <CheckboxLabel />
</CheckboxRoot>
TypeScript

레시피 변형 prop 타입을 추론하려면 RecipeVariantProps 타입 헬퍼를 사용해요.

import type { RecipeVariantProps, UnstyledProp } from "@chakra-ui/react"
import { checkboxSlotRecipe } from "./checkbox.recipe"

type CheckboxVariantProps = RecipeVariantProps<typeof checkboxSlotRecipe>

export interface CheckboxProps
  extends React.PropsWithChildren<CheckboxVariantProps>, UnstyledProp {}

복합 변형 (Compound Variants)

compoundVariants 속성으로 다른 변형들의 조합에 기반해 적용되는 변형 집합을 정의할 수 있어요.

import { defineSlotRecipe } from "@chakra-ui/react"

export const checkboxRecipe = defineSlotRecipe({
  slots: ["root", "control", "label"],
  base: {},
  variants: {
    size: {
      sm: {},
      md: {},
    },
    visual: {
      contained: {},
      outline: {},
    },
  },
  compoundVariants: [
    {
      size: "sm",
      visual: "outline",
      css: {
        control: { borderWidth: "1px" },
        label: { color: "green.500" },
      },
    },
  ],
})

슬롯 타겟팅하기

때로는 className으로 특정 슬롯을 타겟팅해야 할 때가 있어요.

  • 설정의 className 속성을 정한다.
  • 네이밍 규칙은 ${className}__${slot} 이에요.
import { defineSlotRecipe } from "@chakra-ui/react"

export const checkboxRecipe = defineSlotRecipe({
  className: "checkbox",
  slots: ["root", "control", "label"],
  base: {
    root: {
      bg: "blue.500",
      _hover: {
        "& .checkbox__label": { color: "white" },
      },
    },
  },
})

테마에서 사용하기

레시피를 재사용 가능한 형태로 쓰려면 시스템 테마로 옮기고 theme.slotRecipes 속성에 추가해요.

테마에서 레시피를 사용할 때는 "use client" 지시어를 추가할 필요가 없어요.

import { createSystem, defaultConfig, defineConfig } from "@chakra-ui/react"
import { checkboxSlotRecipe } from "./checkbox.recipe"

const config = defineConfig({
  theme: {
    slotRecipes: {
      checkbox: checkboxSlotRecipe,
    },
  },
})

export default createSystem(defaultConfig, config)

TypeScript

CLI를 사용해 레시피용 타입을 생성한 뒤 컴포넌트에서 import해요. postinstall, CI, 모노레포에서 typegen을 실행하는 방법은 CLI 문서를 참고하세요.

npx @chakra-ui/cli typegen ./theme.ts

그런 다음 생성된 타입을 컴포넌트에서 import해요.

import type { SlotRecipeProps, UnstyledProp } from "@chakra-ui/react"

export interface CheckboxProps
  extends SlotRecipeProps<"checkbox">, UnstyledProp {}

코드 업데이트

레시피를 컴포넌트에서 직접 사용하고 있다면, useRecipe가 key 속성을 사용해 테마에서 레시피를 가져오도록 업데이트해요.

const Checkbox = () => {
-  const recipe = useRecipe({ recipe: checkboxRecipe })
+  const recipe = useRecipe({ key: "checkbox" })
  // ...
}

컴파운드 컴포넌트를 만든 경우에는 createSlotRecipeContext가 key 속성을 사용하도록 업데이트해요.

const { withProvider, withContext } = createSlotRecipeContext({
-  recipe: checkboxRecipe,
+  key: "checkbox",
})

더 알아보기 (Learn more)

  • 레시피 문서에서 단일 부분 컴포넌트의 다중 변형 스타일을 알아보세요.
  • 테마 개요에서 Chakra UI 테마 시스템 전반을 알아보세요.