useRenderComponent
rainbowSprinkles + state 콜백 + base-ui useRender를 통합하는 Featuring 디자인 시스템 전용 렌더링 훅.
개요
useRenderComponent는 Featuring 디자인 시스템 레이아웃 컴포넌트(Box, Flex, HStack 등)의 내부를 구동하는 훅입니다. base-ui의 useRender와 mergeProps, rainbowSprinkles를 하나로 합쳐 다음 기능을 동시에 처리합니다.
$cssprop → rainbowSprinkles 처리 → atomic class + inline style 분리className/style→ state 콜백((state) => value) 지원renderprop → 기본 HTML 요소를 다른 요소나 컴포넌트로 교체state→data-*속성 자동 변환 + render 콜백 두 번째 인자로 전달- ref 병합, 이벤트 핸들러 체이닝
import { useRenderComponent } from '@featuring-corp/components';언제 사용하나요
- 레이아웃 프리미티브나 합성 컴포넌트를 직접 만들 때 —
$css,render, state 콜백 className을 한 번에 지원하고 싶은 경우 - base-ui
useRender와 rainbowSprinkles를 따로 연결하는 보일러플레이트를 제거하고 싶을 때
언제 사용하면 안 되나요
- 단순히 요소를 교체하기만 하면 될 때 —
useRender를 직접 사용하세요 $cssprop이 필요 없는 컴포넌트 — 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 ← 소비자가 전달한 값 (최고 우선순위)소비자의 className과 style은 항상 최종적으로 적용되고, 이벤트 핸들러는 어느 레이어에서든 덮어쓰이지 않고 모두 실행됩니다.
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