Featuring Design System
Utils

useRenderComponent

rainbowSprinkles + state 콜백 + base-ui useRender를 통합하는 Featuring 디자인 시스템 전용 렌더링 훅.

개요

useRenderComponent는 Featuring 디자인 시스템 레이아웃 컴포넌트(Box, Flex, HStack 등)의 내부를 구동하는 훅입니다. base-ui의 useRendermergeProps, rainbowSprinkles를 하나로 합쳐 다음 기능을 동시에 처리합니다.

  • $css prop → rainbowSprinkles 처리 → atomic class + inline style 분리
  • className / style → state 콜백((state) => value) 지원
  • render prop → 기본 HTML 요소를 다른 요소나 컴포넌트로 교체
  • statedata-* 속성 자동 변환 + render 콜백 두 번째 인자로 전달
  • ref 병합, 이벤트 핸들러 체이닝
import { useRenderComponent } from '@featuring-corp/components';

언제 사용하나요

  • 레이아웃 프리미티브나 합성 컴포넌트를 직접 만들 때 — $css, render, state 콜백 className을 한 번에 지원하고 싶은 경우
  • base-ui useRender와 rainbowSprinkles를 따로 연결하는 보일러플레이트를 제거하고 싶을 때

언제 사용하면 안 되나요

  • 단순히 요소를 교체하기만 하면 될 때 — useRender를 직접 사용하세요
  • $css prop이 필요 없는 컴포넌트 — rainbowSprinkles 처리 오버헤드가 불필요합니다

Usage

기본 사용 — 커스텀 레이아웃 컴포넌트 만들기

useRenderComponent를 이용해 $css, render, state 콜백 className을 모두 지원하는 Card 컴포넌트를 만듭니다.

import { forwardRef } from 'react';
import { useRenderComponent } from '@featuring-corp/components';
import type { RainbowSprinkles, RenderProp, ClassNameProp, StyleProp } from '@featuring-corp/components';
import { cardRecipe } from './Card.css';

interface CardState {
  elevated: boolean;
}

interface CardProps
  extends Omit<React.ComponentPropsWithoutRef<'div'>, 'className' | 'style'> {
  $css?: RainbowSprinkles;
  elevated?: boolean;
  render?: RenderProp<CardState>;
  className?: ClassNameProp<CardState>;
  style?: StyleProp<CardState>;
}

export const Card = forwardRef<HTMLElement, CardProps>(
  ({ render, className, style, elevated = false, $css, children, ...restProps }, ref) => {
    return useRenderComponent({
      ref,
      defaultTagName: 'div',
      state: { elevated },
      render,
      className,
      style,
      baseClassName: cardRecipe({ elevated }),
      children,
      restProps,
      cssProps: $css,
    });
  }
);

$css prop으로 토큰 기반 스타일링

$css에 rainbowSprinkles 속성을 전달하면 토큰 값과 임의 CSS 값을 모두 사용할 수 있습니다.

<Box
$css={{
  padding: '$spacing-400',
  bgColor: '$background-1',
  borderRadius: '$radius-200',
  display: 'flex',
  flexDirection: 'column',
  gap: '$spacing-200',
}}
>
<Box $css={{ color: '$text-1', fontWeight: '600' }}>제목</Box>
<Box $css={{ color: '$text-3', fontSize: '14px' }}>본문 내용이 여기에 들어갑니다.</Box>
</Box>

render prop으로 요소 교체

render에 ReactElement를 전달하면 기본 태그를 다른 요소로 교체합니다. 내부 ref, 이벤트 핸들러, className은 모두 안전하게 병합됩니다.

<Box
$css={{ padding: '$spacing-400', bgColor: '$background-2', borderRadius: '$radius-200' }}
render={<section />}
>
이 Box는 section 태그로 렌더링됩니다
</Box>

state 콜백 className

className에 함수를 전달하면 컴포넌트 state에 따라 동적으로 클래스를 계산합니다.

<Card
  elevated={isHovered}
  className={(state) => state.elevated ? 'card--elevated' : 'card--flat'}
>
  상태 기반 스타일
</Card>

render prop 함수 콜백 — state 접근

render에 함수를 전달하면 병합된 props와 state를 인자로 받습니다.

<Card
  elevated={isActive}
  render={(props, state) => (
    <article {...props}>
      {state.elevated ? '활성 카드' : '기본 카드'}
    </article>
  )}
/>

state → data-* 자동 변환

state에 전달한 객체의 boolean 값이 true이면 data-{key} 속성이 자동으로 붙습니다. CSS 셀렉터에서 바로 활용할 수 있습니다.

// state = { disabled: true, open: false }
// 렌더링 결과: <div data-disabled="" ...>
// CSS: &[data-disabled] { opacity: 0.5; }

Box가 useRenderComponent를 사용하는 방식

실제 Box 컴포넌트 구현은 이 훅에 모든 역할을 위임합니다.

export const Box = forwardRef<HTMLElement, BoxProps>(
  ({ render, className, style, $css, children, ...restProps }, ref) => {
    return useRenderComponent({
      ref,
      defaultTagName: 'div',
      state: {},
      render,
      className,
      style,
      children,
      restProps,
      cssProps: $css,
    });
  }
);

Props 병합 우선순위

useRenderComponent 내부에서 mergeProps를 통해 4개 레이어의 props가 병합됩니다. 우측(높은 번호)이 좌측을 override하며, className은 연결되고, 이벤트 핸들러는 체이닝됩니다.

1. baseClassName + sprinklesStyle   ← 컴포넌트 기본 스타일 (최저 우선순위)
2. sprinklesClassName               ← $css에서 생성된 atomic CSS 클래스
3. extraProps + otherProps + restProps ← 내부 props + 남은 HTML props
4. className + style + children     ← 소비자가 전달한 값 (최고 우선순위)

소비자의 classNamestyle은 항상 최종적으로 적용되고, 이벤트 핸들러는 어느 레이어에서든 덮어쓰이지 않고 모두 실행됩니다.

API

Prop

Type

useRender (base-ui)

useRender는 base-ui에서 제공하는 low-level 훅으로 Featuring 패키지에서 re-export합니다. useRenderComponent가 필요 없고 rainbowSprinkles 없이 render prop + ref 병합만 필요한 경우에 사용합니다.

import { useRender, mergeProps } from '@featuring-corp/components';
import type { UseRender } from '@featuring-corp/components';

interface TextProps extends UseRender.ComponentProps<'p'> {}

function Text({ render, ...otherProps }: TextProps) {
  return useRender({
    defaultTagName: 'p',
    render,
    props: mergeProps<'p'>({ className: 'my-text' }, otherProps),
  });
}

Prop

Type