react-headless-dev
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.
$ Installieren
git clone https://github.com/daangn/seed-design /tmp/seed-design && cp -r /tmp/seed-design/.claude/skills/react-headless-dev ~/.claude/skills/seed-design// tip: Run this command in your terminal to install the skill
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
๋ค์ ์ํฉ์์ ์ด ์คํฌ์ ์ฌ์ฉํ์ธ์:
- ์ Headless ์ปดํฌ๋ํธ ์์ฑ:
packages/react-headless/ํด๋์ ์๋ก์ด ์ปดํฌ๋ํธ ์ถ๊ฐ - Headless ๋ก์ง ๋ฆฌํฉํ ๋ง: ๊ธฐ์กด ์ปดํฌ๋ํธ์ ๋น์ฆ๋์ค ๋ก์ง ๊ฐ์ ๋๋ ๋ถ๋ฆฌ
- Custom Hook ๊ตฌํ: ์ปดํฌ๋ํธ์ ์ํ ๊ด๋ฆฌ์ ์ด๋ฒคํธ ํธ๋ค๋ง ๋ก์ง ์์ฑ
- Primitive ์กฐํฉ: React ๊ธฐ๋ณธ ์์๋ค์ ์กฐํฉํ ์ปดํฌ์ง์ ํจํด ๊ตฌํ
- 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 ์์ฑ ๊ฐ์ด๋:
- Controlled & Uncontrolled ๋ชจ๋ ์ง์:
defaultValue+valueprops ์ ๊ณตvalue๊ฐ ์์ผ๋ฉด controlled, ์์ผ๋ฉด uncontrolled
- ์ด๋ฒคํธ ํธ๋ค๋ฌ ์ต์ ํ:
useCallback์ผ๋ก ๋ฉ๋ชจ์ด์ ์ด์ - ์์กด์ฑ ๋ฐฐ์ด ์ ํํ ๋ช ์
- ์ ๊ทผ์ฑ ๊ณ ๋ ค:
- ARIA attributes ํฌํจ (aria-checked, aria-disabled ๋ฑ)
- ํ์
์์ ์ฑ:
- 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 ์์ฑ ๊ฐ์ด๋:
- Props Spreading:
- Hook props์ DOM props ๋ถ๋ฆฌ
- Hook์์ ๋ฐํ๋ props๋ฅผ spread
- Ref Forwarding:
forwardRef์ฌ์ฉํ์ฌ ref ์ ๋ฌ- ์ ์ ํ DOM ์์์ ref ์ฐ๊ฒฐ
- Children Composition:
- children์ ์์น์ ๋ ๋๋ง ๋ฐฉ์ ๊ณ ๋ ค
- 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
-
๋ก์ง ๋ถ๋ฆฌ:
- ๋น์ฆ๋์ค ๋ก์ง์ hook์
- DOM ์กฐํฉ์ ์ปดํฌ๋ํธ์
- ์คํ์ผ์
@seed-design/react์
-
์ํ ๊ด๋ฆฌ:
- Controlled์ Uncontrolled ๋ชจ๋ ์ง์
value์defaultValueํจํด ์ฌ์ฉ
-
์ ๊ทผ์ฑ ์ฐ์ :
- ARIA attributes ํญ์ ํฌํจ
- ํค๋ณด๋ ๋ค๋น๊ฒ์ด์ ๊ณ ๋ ค
- ์คํฌ๋ฆฐ ๋ฆฌ๋ ํธํ์ฑ ํ๋ณด
-
ํ์ ์์ ์ฑ:
- Props์ Return ํ์ ๋ช ํํ
- Generic ํ์ ์ ๊ทน ํ์ฉ
- JSDoc ์ฃผ์์ผ๋ก ๋ฌธ์ํ
-
ํ ์คํธ ์์ฑ:
- ์ํ ๋ณํ ํ ์คํธ
- ์ด๋ฒคํธ ํธ๋ค๋ฌ ํ ์คํธ
- ์ฃ์ง ์ผ์ด์ค ์ปค๋ฒ
-
Performance:
useCallback,useMemoํ์ฉ- ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง ๋ฐฉ์ง
- ์์กด์ฑ ๋ฐฐ์ด ์ ํํ ๊ด๋ฆฌ
Repository
