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.

allowed_tools: Read, Write, Edit, MultiEdit, Bash, Glob, Grep

$ 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

๋‹ค์Œ ์ƒํ™ฉ์—์„œ ์ด ์Šคํ‚ฌ์„ ์‚ฌ์šฉํ•˜์„ธ์š”:

  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 ๋ชจ๋‘ ์ง€์›
    • value์™€ defaultValue ํŒจํ„ด ์‚ฌ์šฉ
  3. ์ ‘๊ทผ์„ฑ ์šฐ์„ :

    • ARIA attributes ํ•ญ์ƒ ํฌํ•จ
    • ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ณ ๋ ค
    • ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ํ˜ธํ™˜์„ฑ ํ™•๋ณด
  4. ํƒ€์ž… ์•ˆ์ •์„ฑ:

    • Props์™€ Return ํƒ€์ž… ๋ช…ํ™•ํžˆ
    • Generic ํƒ€์ž… ์ ๊ทน ํ™œ์šฉ
    • JSDoc ์ฃผ์„์œผ๋กœ ๋ฌธ์„œํ™”
  5. ํ…Œ์ŠคํŠธ ์ž‘์„ฑ:

    • ์ƒํƒœ ๋ณ€ํ™” ํ…Œ์ŠคํŠธ
    • ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ํ…Œ์ŠคํŠธ
    • ์—ฃ์ง€ ์ผ€์ด์Šค ์ปค๋ฒ„
  6. Performance:

    • useCallback, useMemo ํ™œ์šฉ
    • ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง ๋ฐฉ์ง€
    • ์˜์กด์„ฑ ๋ฐฐ์—ด ์ •ํ™•ํžˆ ๊ด€๋ฆฌ