Utils
useRender
컴포넌트의 렌더링 요소를 render prop으로 교체할 수 있게 해주는 Hook. base-ui의 useRender를 기반으로 ref 병합, 이벤트 핸들러 체이닝, state → data-* 자동 변환을 제공합니다.
개요
useRender는 base-ui에서 제공하는 Hook으로, 컴포넌트의 기본 HTML 요소를 render prop을 통해 다른 요소나 컴포넌트로 교체할 수 있게 해줍니다.
Featuring 디자인 시스템은 이 Hook을 직접 re-export하여 소비자도 커스텀 컴포넌트를 만들 때 동일한 패턴을 사용할 수 있습니다.
import { useRender, mergeProps } from '@featuring-corp/components';왜 useRender인가?
기존 방식의 문제점
기존에는 일반 함수(renderElement, renderComponent)로 렌더링을 처리했습니다. 이 방식에는 다음과 같은 리스크가 있었습니다:
| 문제 | 설명 | 심각도 |
|---|---|---|
| Ref 유실 | render={<nav ref={myRef} />} 사용 시 component의 forwardedRef가 유실됨 | Critical |
| 이벤트 핸들러 덮어쓰기 | render={<a onClick={track} />}로 요소 교체 시 내부 onClick이 사라짐 | High |
| Hook 사용 불가 | 일반 함수 내부에서 useMergedRefs 등 Hook을 호출할 수 없음 | High |
| className/style 단순 스프레드 | {...props, ...existingProps}로 마지막 값만 적용됨 | Medium |
useRender가 해결하는 것
- Ref 병합:
useMergedRefs로 forwarded ref + 내부 ref + render element ref를 모두 안전하게 병합 - 이벤트 핸들러 체이닝:
mergeProps를 통해 내부/외부 핸들러가 모두 실행됨 (우측 우선) - className 연결: 여러 출처의 className이 자동으로 연결됨
- style 병합: 여러 출처의 style이 우선순위에 따라 병합됨
- state → data-* 자동 변환: state 객체가 자동으로
data-disabled,data-open등의 속성으로 변환됨
Usage
기본 사용법 — 커스텀 컴포넌트 만들기
import { useRender, mergeProps } from '@featuring-corp/components';
import type { useRender as 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),
});
}
// 사용
<Text>기본 p 태그로 렌더링</Text>
<Text render={<span />}>span으로 렌더링</Text>
<Text render={<strong />}>strong으로 렌더링</Text>Ref 병합
내부 ref와 외부 ref를 동시에 사용해야 할 때:
import React, { forwardRef, useRef } from 'react';
import { useRender, mergeProps } from '@featuring-corp/components';
const FocusableText = forwardRef<HTMLElement, TextProps>(
({ render, ...props }, forwardedRef) => {
const internalRef = useRef<HTMLElement>(null);
return useRender({
defaultTagName: 'p',
render,
ref: [forwardedRef, internalRef], // 두 ref 모두 안전하게 병합
props,
});
}
);State와 data-* 속성
state 객체를 전달하면 자동으로 data-* 속성으로 변환됩니다:
import { useRender, mergeProps } from '@featuring-corp/components';
interface ToggleState {
active: boolean;
disabled: boolean;
}
function Toggle({ render, active, disabled, ...props }: ToggleProps) {
const state: ToggleState = { active, disabled };
return useRender({
defaultTagName: 'button',
render,
state,
props: mergeProps<'button'>({ className: 'toggle' }, props),
});
}
// 렌더링 결과:
// <button class="toggle" data-active="" data-disabled="">...</button>
//
// CSS에서 활용:
// .toggle[data-active] { background: blue; }
// .toggle[data-disabled] { opacity: 0.5; }render prop 콜백 (state 접근)
render에 함수를 전달하면 props와 state에 접근할 수 있습니다:
<Toggle
active={isActive}
render={(props, state) => (
<button {...props}>
{state.active ? '활성' : '비활성'}
</button>
)}
/>useRenderComponent
useRenderComponent는 Featuring 디자인 시스템 전용 Hook으로, useRender + mergeProps + rainbowSprinkles를 통합합니다. 레이아웃 컴포넌트(Box, Flex 등)의 내부에서 사용됩니다.
import { useRenderComponent } from '@featuring-corp/components';Props 설명
Prop
Type
Props 병합 우선순위
useRenderComponent 내부에서 mergeProps를 통해 4개 레이어의 props가 병합됩니다:
우선순위 (낮음 → 높음):
1. baseClassName + sprinklesStyle → 컴포넌트 기본 스타일
2. sprinklesClassName → 토큰 기반 atomic CSS 클래스
3. extraProps + nativeProps → 내부 props + 남은 HTML props (onClick 등)
4. className + style + children → 소비자가 전달한 값 (최고 우선순위)이것은 다음을 보장합니다:
- 소비자의
className이 항상 적용됨 - 소비자의
style이 sprinkles 스타일을 override할 수 있음 - 이벤트 핸들러는 덮어쓰지 않고 체이닝됨
사용 예시 — 커스텀 레이아웃 컴포넌트
import React, { forwardRef } from 'react';
import { useRenderComponent } from '@featuring-corp/components';
import type { RainbowSprinkles } 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,
});
}
);
// 사용
<Card elevated $css={{ padding: '$spacing-400', bgColor: '$background-1' }}>
카드 내용
</Card>
// render prop으로 요소 교체
<Card render={<article />} elevated>
article로 렌더링되는 카드
</Card>
// state 기반 className
<Card
elevated={isHovered}
className={(state) => state.elevated ? 'card-elevated' : 'card-flat'}
>
상태 기반 스타일
</Card>API Reference
useRender
Prop
Type