Claude Code Plugins

Community-maintained marketplace

Feedback

react-headless-dev

@daangn/seed-design
799
0

SEED React Headless component development specialist. Use when developing unstyled, logic-only components in packages/react-headless folder. Focuses on data-driven primitives, custom hooks, and state management without styling concerns.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name react-headless-dev
description SEED React Headless component development specialist. Use when developing unstyled, logic-only components in packages/react-headless folder. Focuses on data-driven primitives, custom hooks, and state management without styling concerns.
allowed-tools Read, Write, Edit, MultiEdit, Bash, Glob, Grep

React Headless Component Developer

Develop unstyled React components following SEED headless architecture patterns.

Purpose

이 스킬은 SEED Design System의 React Headless 컴포넌트를 개발합니다. Headless 컴포넌트는 스타일 없이 순수한 데이터 로직과 상태 관리만 제공하며, @seed-design/react 패키지에서 스타일을 입힐 수 있는 기반을 제공합니다.

When to Use

다음 상황에서 이 스킬을 사용하세요:

  1. 새 Headless 컴포넌트 생성: packages/react-headless/ 폴더에 새로운 컴포넌트 추가
  2. Headless 로직 리팩토링: 기존 컴포넌트의 비즈니스 로직 개선 또는 분리
  3. Custom Hook 구현: 컴포넌트의 상태 관리와 이벤트 핸들링 로직 작성
  4. Primitive 조합: React 기본 요소들을 조합한 컴포지션 패턴 구현
  5. Data Attributes 정의: 컴포넌트 상태를 표현하는 data attributes 설계

트리거 키워드: "headless component", "unstyled component", "custom hook", "primitive composition", "packages/react-headless"

Architecture Principles

1. Style-Free Logic

원칙: 스타일 관련 로직 없이 순수 컴포넌트 데이터 로직만 제공

// ❌ Bad: 스타일 관련 로직 포함
const Button = () => {
  const className = size === 'large' ? 'btn-lg' : 'btn-sm'
  return <button className={className} />
}

// ✅ Good: 데이터만 제공, 스타일은 @seed-design/react에서 처리
const Button = () => {
  return <button data-size={size} />
}

스타일 관련 컴포넌트 로직 및 옵션은 @seed-design/react 패키지에서 제공합니다.

2. Custom Hook Pattern

원칙: 중요 비즈니스 로직은 커스텀 훅 파일에 작성

// use{Component}.ts
export function useCheckbox(props: UseCheckboxProps) {
  const [checked, setChecked] = useState(props.defaultChecked)
  const [focused, setFocused] = useState(false)

  const handleChange = useCallback(() => {
    setChecked(prev => !prev)
    props.onChange?.(!checked)
  }, [checked, props.onChange])

  return {
    rootProps: {
      'data-checked': checked,
      'data-focused': focused,
      onClick: handleChange,
    },
    inputProps: {
      type: 'checkbox',
      checked,
      onChange: handleChange,
      onFocus: () => setFocused(true),
      onBlur: () => setFocused(false),
    },
  }
}

가이드라인:

  • 파일명: use{Component}.ts (예: useCheckbox.ts, useRadio.ts)
  • 컴포넌트 복잡도에 따라 여러 개의 커스텀 훅 파일 작성 가능
  • 각 hook은 parts별 props를 반환 (rootProps, inputProps, labelProps 등)
  • 상태 관리, 이벤트 핸들링, 접근성 로직을 캡슐화

3. Primitive Composition

원칙: 컴포넌트 파일은 커스텀 훅의 parts를 spread하여 조합된 Primitive 컴포넌트들을 내보냄

// {Component}.tsx
import { useCheckbox } from './useCheckbox'

export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
  (props, ref) => {
    const { rootProps, inputProps } = useCheckbox(props)

    return (
      <button ref={ref} {...rootProps}>
        <input {...inputProps} />
        {props.children}
      </button>
    )
  }
)

가이드라인:

  • 파일명: {Component}.tsx (예: Checkbox.tsx, Radio.tsx)
  • 단순히 커스텀 훅에서 반환된 props를 spread
  • DOM 요소 조합 및 children 배치만 담당
  • 복잡한 로직은 hook에 위임

4. State-Driven Data Attributes

원칙: Data attributes는 컴포넌트의 상태를 나타내는 데이터 위주로 작성

// ✅ Good: 상태를 나타내는 data attributes
<button
  data-checked={checked}
  data-disabled={disabled}
  data-invalid={invalid}
  data-required={required}
  data-focused={focused}
/>

// ❌ Bad: 스타일을 위한 computed prop
<button
  data-button-color="red"
  data-button-size="large"
  data-should-have-shadow={true}
/>

일반적인 Data Attributes:

  • data-checked: 선택 상태 (checkbox, radio, switch)
  • data-disabled: 비활성 상태
  • data-invalid: 유효하지 않은 상태 (form fields)
  • data-required: 필수 입력 (form fields)
  • data-focused: 포커스 상태
  • data-pressed: 눌린 상태 (button)
  • data-selected: 선택된 상태 (list items, tabs)
  • data-expanded: 확장된 상태 (accordion, dropdown)
  • data-loading: 로딩 상태

5. Namespace Pattern (Multi-Part Components)

원칙: Parts가 여러 개인 경우 {Component}.namespace.ts barrel file을 정의하여 내보냄

// Dialog.namespace.ts
export { Dialog as Root } from './Dialog'
export { DialogTrigger as Trigger } from './DialogTrigger'
export { DialogContent as Content } from './DialogContent'
export { DialogHeader as Header } from './DialogHeader'
export { DialogTitle as Title } from './DialogTitle'
export { DialogDescription as Description } from './DialogDescription'
export { DialogFooter as Footer } from './DialogFooter'
export { DialogClose as Close } from './DialogClose'
// index.ts
import * as Dialog from './Dialog.namespace'
export { Dialog }

사용 예시:

import { Dialog } from '@seed-design/react-headless'

<Dialog.Root>
  <Dialog.Trigger>Open</Dialog.Trigger>
  <Dialog.Content>
    <Dialog.Header>
      <Dialog.Title>Title</Dialog.Title>
    </Dialog.Header>
  </Dialog.Content>
</Dialog.Root>

Development Workflow

Step 1: Requirements Analysis

사용자에게 다음 정보를 요청합니다:

필수 정보:

  • Component Name: 예) Checkbox, Radio, Dialog
  • Component Type:
    • Single: 단일 컴포넌트 (예: Checkbox, Switch)
    • Multi-Part: 여러 parts로 구성 (예: Dialog, Dropdown)
  • State Requirements: 관리할 상태 목록 (checked, open, selected 등)
  • Event Handlers: 필요한 이벤트 핸들러 (onChange, onOpen, onClose 등)

선택 정보:

  • Data Attributes: 제공할 data attributes 목록
  • Accessibility: ARIA attributes 요구사항
  • Controlled vs Uncontrolled: 제어 컴포넌트 vs 비제어 컴포넌트

Step 2: Package Structure Setup

Headless 컴포넌트는 packages/react-headless/ 폴더 내에 위치합니다:

packages/react-headless/
├── checkbox/
│   ├── src/
│   │   ├── useCheckbox.ts        # Custom hook
│   │   ├── Checkbox.tsx           # Component
│   │   └── index.ts               # Public exports
│   ├── package.json
│   └── tsconfig.json
├── dialog/
│   ├── src/
│   │   ├── useDialog.ts           # Main hook
│   │   ├── Dialog.tsx             # Root component
│   │   ├── DialogTrigger.tsx      # Trigger part
│   │   ├── DialogContent.tsx      # Content part
│   │   ├── Dialog.namespace.ts    # Namespace barrel
│   │   └── index.ts
│   ├── package.json
│   └── tsconfig.json

디렉토리 생성:

mkdir -p packages/react-headless/{component-name}/src

Step 3: Implement Custom Hook

Step 3-1: use{Component}.ts 파일 생성

import { useCallback, useState } from 'react'

export interface Use{Component}Props {
  defaultValue?: boolean
  value?: boolean
  disabled?: boolean
  onChange?: (value: boolean) => void
}

export interface Use{Component}Return {
  // Part별 props 반환
  rootProps: {
    'data-checked': boolean
    'data-disabled': boolean
    onClick: () => void
  }
  inputProps: {
    type: 'checkbox'
    checked: boolean
    disabled: boolean
    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
  }
}

export function use{Component}(props: Use{Component}Props): Use{Component}Return {
  // 1. State management (controlled vs uncontrolled)
  const [internalValue, setInternalValue] = useState(props.defaultValue ?? false)
  const checked = props.value ?? internalValue

  // 2. Event handlers
  const handleChange = useCallback(() => {
    if (props.disabled) return

    const newValue = !checked
    setInternalValue(newValue)
    props.onChange?.(newValue)
  }, [checked, props.disabled, props.onChange])

  // 3. Return parts props
  return {
    rootProps: {
      'data-checked': checked,
      'data-disabled': props.disabled ?? false,
      onClick: handleChange,
    },
    inputProps: {
      type: 'checkbox',
      checked,
      disabled: props.disabled ?? false,
      onChange: handleChange,
    },
  }
}

Hook 작성 가이드:

  1. Controlled & Uncontrolled 모두 지원:
    • defaultValue + value props 제공
    • value가 있으면 controlled, 없으면 uncontrolled
  2. 이벤트 핸들러 최적화:
    • useCallback으로 메모이제이션
    • 의존성 배열 정확히 명시
  3. 접근성 고려:
    • ARIA attributes 포함 (aria-checked, aria-disabled 등)
  4. 타입 안정성:
    • Props와 Return 타입 명확히 정의
    • Generic 타입 활용 가능

Step 4: Implement Component

Step 4-1: {Component}.tsx 파일 생성

import { forwardRef } from 'react'
import { use{Component}, Use{Component}Props } from './use{Component}'

export interface {Component}Props extends Use{Component}Props {
  children?: React.ReactNode
  className?: string
}

export const {Component} = forwardRef<HTMLButtonElement, {Component}Props>(
  (props, ref) => {
    const { children, className, ...hookProps } = props
    const { rootProps, inputProps } = use{Component}(hookProps)

    return (
      <button
        ref={ref}
        className={className}
        {...rootProps}
      >
        <input {...inputProps} />
        {children}
      </button>
    )
  }
)

{Component}.displayName = '{Component}'

Component 작성 가이드:

  1. Props Spreading:
    • Hook props와 DOM props 분리
    • Hook에서 반환된 props를 spread
  2. Ref Forwarding:
    • forwardRef 사용하여 ref 전달
    • 적절한 DOM 요소에 ref 연결
  3. Children Composition:
    • children의 위치와 렌더링 방식 고려
  4. DisplayName:
    • 디버깅을 위해 displayName 설정

Step 5: Multi-Part Components (선택)

Parts가 여러 개인 경우:

Step 5-1: 각 Part별 파일 생성

// DialogTrigger.tsx
export const DialogTrigger = forwardRef<HTMLButtonElement, DialogTriggerProps>(
  (props, ref) => {
    const { triggerProps } = useDialogContext()
    return <button ref={ref} {...triggerProps} {...props} />
  }
)

Step 5-2: Context 생성 (필요 시)

// DialogContext.tsx
const DialogContext = createContext<UseDialogReturn | null>(null)

export function useDialogContext() {
  const context = useContext(DialogContext)
  if (!context) throw new Error('Dialog parts must be used within Dialog.Root')
  return context
}

Step 5-3: Namespace 파일 생성

// Dialog.namespace.ts
export { Dialog as Root } from './Dialog'
export { DialogTrigger as Trigger } from './DialogTrigger'
export { DialogContent as Content } from './DialogContent'
// ... 다른 parts

Step 6: Public Exports

Step 6-1: index.ts 파일 작성

Single Component:

// index.ts
export { Checkbox } from './Checkbox'
export type { CheckboxProps } from './Checkbox'
export { useCheckbox } from './useCheckbox'
export type { UseCheckboxProps, UseCheckboxReturn } from './useCheckbox'

Multi-Part Component:

// index.ts
import * as Dialog from './Dialog.namespace'
export { Dialog }

export type { DialogProps } from './Dialog'
export type { DialogTriggerProps } from './DialogTrigger'
// ... 다른 types

export { useDialog } from './useDialog'
export type { UseDialogProps, UseDialogReturn } from './useDialog'

Step 7: Package Configuration

Step 7-1: package.json 확인

{
  "name": "@seed-design/react-headless-{component-name}",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.js"
    }
  },
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  }
}

Examples

Example 1: Simple Checkbox Component

// useCheckbox.ts
export function useCheckbox(props: UseCheckboxProps) {
  const [checked, setChecked] = useState(props.defaultChecked ?? false)
  const isChecked = props.checked ?? checked

  const handleChange = useCallback(() => {
    if (props.disabled) return
    const newValue = !isChecked
    setChecked(newValue)
    props.onChange?.(newValue)
  }, [isChecked, props.disabled, props.onChange])

  return {
    rootProps: {
      'data-checked': isChecked,
      'data-disabled': props.disabled ?? false,
      role: 'checkbox',
      'aria-checked': isChecked,
      onClick: handleChange,
    },
    inputProps: {
      type: 'checkbox',
      checked: isChecked,
      disabled: props.disabled,
      onChange: handleChange,
    },
  }
}

// Checkbox.tsx
export const Checkbox = forwardRef<HTMLDivElement, CheckboxProps>(
  (props, ref) => {
    const { children, ...hookProps } = props
    const { rootProps, inputProps } = useCheckbox(hookProps)

    return (
      <div ref={ref} {...rootProps}>
        <input {...inputProps} />
        {children}
      </div>
    )
  }
)

Example 2: Multi-Part Dialog Component

// useDialog.ts
export function useDialog(props: UseDialogProps) {
  const [open, setOpen] = useState(props.defaultOpen ?? false)
  const isOpen = props.open ?? open

  const handleOpenChange = useCallback((newOpen: boolean) => {
    setOpen(newOpen)
    props.onOpenChange?.(newOpen)
  }, [props.onOpenChange])

  return {
    isOpen,
    triggerProps: {
      'data-state': isOpen ? 'open' : 'closed',
      onClick: () => handleOpenChange(true),
    },
    contentProps: {
      'data-state': isOpen ? 'open' : 'closed',
      hidden: !isOpen,
    },
    closeProps: {
      onClick: () => handleOpenChange(false),
    },
  }
}

// Dialog.tsx
export const Dialog = (props: DialogProps) => {
  const dialog = useDialog(props)

  return (
    <DialogContext.Provider value={dialog}>
      {props.children}
    </DialogContext.Provider>
  )
}

// DialogTrigger.tsx
export const DialogTrigger = forwardRef<HTMLButtonElement, DialogTriggerProps>(
  (props, ref) => {
    const { triggerProps } = useDialogContext()
    return <button ref={ref} {...triggerProps} {...props} />
  }
)

// Dialog.namespace.ts
export { Dialog as Root } from './Dialog'
export { DialogTrigger as Trigger } from './DialogTrigger'
export { DialogContent as Content } from './DialogContent'
export { DialogClose as Close } from './DialogClose'

Testing Guidelines

Headless 컴포넌트는 스타일이 없으므로 데이터 로직과 상태 관리를 테스트합니다:

describe('useCheckbox', () => {
  it('should toggle checked state', () => {
    const { result } = renderHook(() => useCheckbox({}))

    expect(result.current.rootProps['data-checked']).toBe(false)

    act(() => {
      result.current.rootProps.onClick()
    })

    expect(result.current.rootProps['data-checked']).toBe(true)
  })

  it('should call onChange callback', () => {
    const onChange = vi.fn()
    const { result } = renderHook(() => useCheckbox({ onChange }))

    act(() => {
      result.current.rootProps.onClick()
    })

    expect(onChange).toHaveBeenCalledWith(true)
  })
})

테스트 항목:

  • 상태 변화 (checked, open, selected 등)
  • 이벤트 핸들러 호출
  • Controlled vs Uncontrolled 모드
  • Data attributes 정확성
  • 접근성 attributes (ARIA)

Checklist

컴포넌트 개발 후 다음 사항을 확인합니다:

  • 스타일 관련 로직이 없는가?
  • 커스텀 훅이 올바른 parts props를 반환하는가?
  • Data attributes가 상태를 정확히 표현하는가?
  • Controlled & Uncontrolled 모드를 모두 지원하는가?
  • Ref forwarding이 올바르게 구현되었는가?
  • Multi-part 컴포넌트의 경우 namespace 파일이 있는가?
  • 접근성 attributes (ARIA)가 포함되었는가?
  • TypeScript 타입이 정확하게 정의되었는가?
  • Public exports (index.ts)가 올바르게 설정되었는가?
  • 테스트가 작성되었는가?

Reference

기존 Headless 컴포넌트:

  • packages/react-headless/ 폴더의 다른 컴포넌트들 참조
  • 유사한 컴포넌트의 패턴 활용

외부 라이브러리 참고:

  • Radix UI Primitives
  • React Aria Components
  • Headless UI

Tips

  1. 로직 분리:

    • 비즈니스 로직은 hook에
    • DOM 조합은 컴포넌트에
    • 스타일은 @seed-design/react
  2. 상태 관리:

    • Controlled와 Uncontrolled 모두 지원
    • valuedefaultValue 패턴 사용
  3. 접근성 우선:

    • ARIA attributes 항상 포함
    • 키보드 네비게이션 고려
    • 스크린 리더 호환성 확보
  4. 타입 안정성:

    • Props와 Return 타입 명확히
    • Generic 타입 적극 활용
    • JSDoc 주석으로 문서화
  5. 테스트 작성:

    • 상태 변화 테스트
    • 이벤트 핸들러 테스트
    • 엣지 케이스 커버
  6. Performance:

    • useCallback, useMemo 활용
    • 불필요한 리렌더링 방지
    • 의존성 배열 정확히 관리