Featuring Design System
Utils

mergeProps

여러 React props 객체를 안전하게 병합하는 유틸리티. 이벤트 핸들러는 체이닝, className은 연결, style은 병합됩니다.

개요

mergePropsbase-ui에서 제공하는 유틸리티로, 여러 React props 객체를 안전하게 병합합니다. Object.assign이나 스프레드({...a, ...b})와 달리, 이벤트 핸들러·className·style을 특별하게 처리합니다.

import { mergeProps } from '@featuring-corp/components';

왜 mergeProps인가?

스프레드의 문제점

// ❌ 위험: onClick이 덮어쓰기됨
const merged = { ...internalProps, ...externalProps };
// externalProps.onClick만 실행됨, internalProps.onClick은 유실

// ❌ 위험: className이 덮어쓰기됨
const merged = { ...{ className: 'internal' }, ...{ className: 'external' } };
// className: 'external' — 'internal'은 유실

mergeProps의 해결

// ✅ 안전: 양쪽 onClick 모두 실행
const merged = mergeProps(internalProps, externalProps);
// externalProps.onClick 먼저 실행 → internalProps.onClick 이어서 실행

// ✅ 안전: className이 연결됨
const merged = mergeProps({ className: 'internal' }, { className: 'external' });
// className: 'external internal'

병합 규칙

일반 props — 우측 우선

mergeProps({ id: 'a', dir: 'ltr' }, { id: 'b' });
// → { id: 'b', dir: 'ltr' }

className — 연결 (우측이 앞)

mergeProps({ className: 'base' }, { className: 'override' });
// → { className: 'override base' }

mergeProps(
  { className: 'a' },
  { className: 'b' },
  { className: 'c' },
);
// → { className: 'c b a' }

style — 병합 (우측 우선)

mergeProps(
  { style: { color: 'red', fontSize: 14 } },
  { style: { color: 'blue', padding: 8 } },
);
// → { style: { color: 'blue', fontSize: 14, padding: 8 } }

이벤트 핸들러 — 체이닝 (우측 먼저 실행)

mergeProps(
  { onClick: () => console.log('internal') },
  { onClick: () => console.log('external') },
);
// 클릭 시: 'external' 출력 → 'internal' 출력 (양쪽 모두 실행)

ref — 병합하지 않음

refmergeProps에서 병합되지 않습니다. ref 병합이 필요하면 useRenderref 옵션을 사용하세요.

// ❌ ref는 mergeProps로 병합하지 마세요
mergeProps({ ref: refA }, { ref: refB });
// refB만 적용됨

// ✅ useRender의 ref 옵션 사용
useRender({
  ref: [refA, refB],  // 양쪽 모두 적용
  // ...
});

Usage

컴포넌트 내부에서 props 병합

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

function Button({ render, variant, ...externalProps }) {
  const internalProps = {
    className: `btn btn-${variant}`,
    type: 'button' as const,
    onClick: () => console.log('button clicked'),
  };

  return useRender({
    defaultTagName: 'button',
    render,
    props: mergeProps<'button'>(internalProps, externalProps),
  });
}

// 사용: 외부 onClick이 내부 onClick을 덮어쓰지 않음
<Button.Root variant="primary" onClick={() => analytics.track('click')}>
  Submit
</Button.Root>
// 클릭 시: analytics.track 실행 → console.log 실행

preventBaseUIHandler — 내부 핸들러 차단

외부에서 내부 핸들러의 실행을 선택적으로 차단할 수 있습니다:

<Button
  render={(props) => (
    <button
      {...mergeProps<'button'>(props, {
        onClick(event) {
          if (isLocked) {
            event.preventBaseUIHandler(); // 내부 onClick만 차단
            // preventDefault()와 달리 다른 이벤트 전파에는 영향 없음
          }
        },
      })}
    />
  )}
>
  Conditional Button
</Button.Root>

함수형 props (지연 평가)

props 대신 함수를 전달하면, 이전까지 병합된 props를 인자로 받아 새 props를 반환합니다:

const merged = mergeProps(
  { onClick: handleA, className: 'base' },
  (prevProps) => ({
    // prevProps = { onClick: handleA, className: 'base' }
    'aria-label': prevProps.className?.includes('base') ? 'Base Button' : 'Button',
  }),
);

여러 props 소스 병합 (최대 5개)

mergeProps(
  defaultProps,      // 1. 기본값
  sprinklesProps,    // 2. 토큰 기반 스타일
  behaviorProps,     // 3. behavior hook 결과
  nativeProps,       // 4. 네이티브 HTML props
  consumerProps,     // 5. 소비자 전달 (최고 우선순위)
);

6개 이상이 필요하면 mergePropsN (배열 기반)을 사용할 수 있습니다. 단, 성능이 약간 낮으므로 5개 이하일 때는 mergeProps를 사용하세요.

실전: 디자인 시스템에서의 활용

render prop 콜백에서 사용

import { Box, mergeProps } from '@featuring-corp/components';

<Box
  $css={{ padding: '$spacing-400', bgColor: '$background-2' }}
  render={(props) => (
    <nav
      {...mergeProps(props, {
        className: 'custom-nav',
        'aria-label': 'Main navigation',
      })}
    />
  )}
>
  Navigation content
</Box>

behavior hook과 조합

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

function useTooltipTrigger() {
  return {
    triggerRef: useRef(null),
    getTriggerProps: () => ({
      'aria-describedby': 'tooltip-1',
      onMouseEnter: () => setOpen(true),
      onMouseLeave: () => setOpen(false),
    }),
  };
}

function MyButton({ render, ...props }) {
  const { triggerRef, getTriggerProps } = useTooltipTrigger();

  return useRender({
    defaultTagName: 'button',
    render,
    ref: [forwardedRef, triggerRef],
    // behavior hook의 props가 안전하게 병합됨
    props: mergeProps<'button'>(getTriggerProps(), props),
  });
}

API Reference

mergeProps

최대 5개의 props 객체(또는 함수)를 병합합니다.

Prop

Type

반환값: 병합된 props 객체

병합 규칙 요약

props 종류병합 방식예시
일반 props우측 우선 덮어쓰기id: 'b'
className연결 (우측이 앞)'external internal'
style객체 병합 (우측 우선){ color: 'blue', fontSize: 14 }
이벤트 핸들러 (on*)체이닝 (우측 먼저 실행)양쪽 모두 실행
ref병합하지 않음마지막 ref만 적용