컴포넌트는 className을 안 만든다 — prop을 그대로 DOM 어트리뷰트로 흘리고, variant 분기는 CSS 어트리뷰트 셀렉터가 담당한다.

// 컴포넌트 전체 = h(tag, { ...props, k: name }) 한 줄
export const Badge = createSimpleComponent<'span', BadgeOwnProps>(
  'badge',
  'span',
)
// → <span k="badge" variant="secondary">   (variant가 class가 아니라 attr로 그대로 나감)
[k="badge"] { /* default */ }
[k="badge"][variant="secondary"] { … }
[k="badge"][variant="destructive"]:hover { … }

createSimpleComponent 가 하는 일

/**
 * - defaults를 props 아래에 깔고
 * - k={name} 할당하고
 * - h(tag, normalizedProps)
 */
createSimpleComponent<T, P>(name, tag)
  • k?: never 로 소비자가 셀렉터 키 k 를 덮어쓰는 걸 타입 레벨에서 차단.
  • Props = Omit<JSX.IntrinsicElements[T], keyof P> & P → own prop이 동명 intrinsic 어트리뷰트를 덮어씀 (Badge의 variant 가 우선).
  • tag(props) => T 함수로도 받아 폴리모픽 (href 있으면 <a>, 없으면 <button>). 단 분기에 들어가는 건 defaults 적용 원본 props.

대가: 런타임 동적 variant 계산·조건부 로직은 포기 (CSS 셀렉터로 표현 가능한 범위만).

참고

풀스택 경계의 통합 단위 = reactive state 그 자체. 서버의 signal/model이 클라이언트에서 같은 객체로 보인다.

// 서버 model이 클라이언트에 그대로 비친다
const todos = createReflectedModel('Todos')
todos.add('buy milk') // RPC인지 로컬 메서드인지 의식 안 함
todos.all.value[0].done.value = true // signal 토글 = 서버 상태 토글

통합 단위가 컴포넌트(RSC)도 함수 호출(server functions)도 DOM diff(LiveView)도 아니다. 다른 진영이 “서버 코드를 어떻게 부를까”를 풀 때, 여기는 “서버 상태를 어떻게 살게 할까”를 푼다. 태그라인 “as if they lived on the client”이 그 직접 표현.

참고