Skip to content

JiinSeok/formkit-react

Repository files navigation

@jiin.seok/formkit-react

πŸš€ Compound Component Patternκ³Ό λ‚΄μž₯ 검증, TypeScript 지원을 κ°–μΆ˜ κ°•λ ₯ν•œ React 폼 라이브러리

npm version License: MIT

English Documentation

✨ μ£Όμš” κΈ°λŠ₯

  • 🎯 Compound Component Pattern - κΉ”λ”ν•˜κ³  μ‘°ν•© κ°€λŠ₯ν•œ API
  • πŸ”„ React Hook Form 톡합 - μ„±λŠ₯ μ΅œμ ν™”
  • πŸ›‘οΈ Zod μŠ€ν‚€λ§ˆ 지원 - νƒ€μž… μ•ˆμ „ 검증
  • β™Ώ μ ‘κ·Όμ„± μš°μ„  - ARIA μ€€μˆ˜
  • 🎨 Tailwind CSS μŠ€νƒ€μΌλ§ - 기본적으둜 μ•„λ¦„λ‹€μš΄ λ””μžμΈ
  • πŸ“ TypeScript - μ™„μ „ν•œ νƒ€μž… μ•ˆμ •μ„±
  • πŸ”’ λΉ„λ°€λ²ˆν˜Έ ν† κΈ€ - λ‚΄μž₯ κ°€μ‹œμ„± ν† κΈ€
  • πŸŽ›οΈ Select μ»΄ν¬λ„ŒνŠΈ - Radix UI 기반

πŸ“¦ μ„€μΉ˜

npm install @jiin.seok/formkit-react
# λ˜λŠ”
yarn add @jiin.seok/formkit-react
# λ˜λŠ”
pnpm add @jiin.seok/formkit-react

Peer Dependencies

npm install react react-dom

react-hook-form, zod, @hookform/resolvers 등은 νŒ¨ν‚€μ§€ μ˜μ‘΄μ„±μœΌλ‘œ μžλ™ μ„€μΉ˜λ©λ‹ˆλ‹€.

μŠ€νƒ€μΌ 뢈러였기

μ•± μ§„μž…μ (예: main.tsx, layout.tsx)μ—μ„œ κΈ°λ³Έ μŠ€νƒ€μΌμ„ ν•œ 번 λΆˆλŸ¬μ˜΅λ‹ˆλ‹€.

import '@jiin.seok/formkit-react/styles.css'

색상은 CSS λ³€μˆ˜ 기반이라 :rootμ—μ„œ λ³€μˆ˜λ§Œ μ˜€λ²„λΌμ΄λ“œν•˜λ©΄ ν…Œλ§ˆλ₯Ό λ°”κΏ€ 수 μžˆμŠ΅λ‹ˆλ‹€.

:root {
  --primary: 222.2 47.4% 11.2%; /* hsl κ°’ (μ‰Όν‘œ 없이) */
  --destructive: 0 84.2% 60.2%;
}

πŸš€ λΉ λ₯Έ μ‹œμž‘

기본 예제

import FormKit from '@jiin.seok/formkit-react'

function LoginForm() {
  const handleSubmit = (data) => {
    console.log('폼 데이터:', data)
  }

  return (
    <FormKit.Root formId="login" onSubmit={handleSubmit}>
      <FormKit.Title>둜그인</FormKit.Title>
      
      <FormKit.Field>
        <FormKit.Label>이메일</FormKit.Label>
        <FormKit.Input name="email" type="email" required />
      </FormKit.Field>
      
      <FormKit.Field>
        <FormKit.Label>λΉ„λ°€λ²ˆν˜Έ</FormKit.Label>
        <FormKit.Input name="password" type="password" required />
      </FormKit.Field>
      
      <FormKit.SubmitButton>둜그인</FormKit.SubmitButton>
    </FormKit.Root>
  )
}

Zod 검증과 ν•¨κ»˜ μ‚¬μš©

import FormKit from '@jiin.seok/formkit-react'
import { z } from 'zod'

const loginSchema = z.object({
  email: z.string().email('μœ νš¨ν•˜μ§€ μ•Šμ€ 이메일 μ£Όμ†Œμž…λ‹ˆλ‹€'),
  password: z.string().min(8, 'λΉ„λ°€λ²ˆν˜ΈλŠ” μ΅œμ†Œ 8자 이상이어야 ν•©λ‹ˆλ‹€')
})

function LoginForm() {
  const handleSubmit = (data) => {
    console.log('κ²€μ¦λœ 데이터:', data)
  }

  return (
    <FormKit.Root 
      formId="login" 
      schema={loginSchema} 
      onSubmit={handleSubmit}
    >
      <FormKit.Field>
        <FormKit.Label>이메일</FormKit.Label>
        <FormKit.Input name="email" type="email" />
      </FormKit.Field>
      
      <FormKit.Field>
        <FormKit.Label>λΉ„λ°€λ²ˆν˜Έ</FormKit.Label>
        <FormKit.Input name="password" type="password" />
      </FormKit.Field>
      
      <FormKit.SubmitButton>둜그인</FormKit.SubmitButton>
    </FormKit.Root>
  )
}

Selectλ₯Ό ν¬ν•¨ν•œ κ³ κΈ‰ 폼

import FormKit from '@jiin.seok/formkit-react'

function RegistrationForm() {
  const countries = [
    { value: 'kr', label: 'λŒ€ν•œλ―Όκ΅­' },
    { value: 'us', label: 'λ―Έκ΅­' },
    { value: 'jp', label: '일본' },
  ]

  return (
    <FormKit.Root formId="registration" onSubmit={handleSubmit}>
      <FormKit.Fieldset>
        <FormKit.Legend required>개인 정보</FormKit.Legend>
        
        <FormKit.Field>
          <FormKit.Label>이름</FormKit.Label>
          <FormKit.Input name="fullName" required />
        </FormKit.Field>
        
        <FormKit.Field>
          <FormKit.Label>κ΅­κ°€</FormKit.Label>
          <FormKit.Select 
            name="country" 
            options={countries}
            placeholder="κ΅­κ°€λ₯Ό μ„ νƒν•˜μ„Έμš”"
            required
          />
        </FormKit.Field>
        
        <FormKit.Field>
          <FormKit.Label>μžκΈ°μ†Œκ°œ</FormKit.Label>
          <FormKit.Textarea 
            name="bio" 
            placeholder="κ°„λ‹¨ν•œ μžκΈ°μ†Œκ°œλ₯Ό μž‘μ„±ν•΄μ£Όμ„Έμš”"
            maxLength={500}
          />
        </FormKit.Field>
      </FormKit.Fieldset>
      
      <FormKit.SubmitButton>κ°€μž…ν•˜κΈ°</FormKit.SubmitButton>
    </FormKit.Root>
  )
}

πŸ“¦ μ‚¬μš© κ°€λŠ₯ν•œ μ»΄ν¬λ„ŒνŠΈ

FormKit은 포괄적인 폼 μ»΄ν¬λ„ŒνŠΈ μ„ΈνŠΈλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€:

πŸ“ 핡심 μ»΄ν¬λ„ŒνŠΈ

  • FormKit.Root - 검증 μ»¨ν…μŠ€νŠΈλ₯Ό ν¬ν•¨ν•œ 메인 폼 μ»¨ν…Œμ΄λ„ˆ
  • FormKit.Field - λ ˆμ΄λΈ”-μž…λ ₯ 연결이 μžˆλŠ” ν•„λ“œ 래퍼
  • FormKit.Fieldset - κ΄€λ ¨λœ ν•„λ“œ κ·Έλ£Ήν™”
  • FormKit.Legend - ν•„μˆ˜ ν‘œμ‹œκ°€ μ„ νƒμ μœΌλ‘œ ν¬ν•¨λœ ν•„λ“œμ…‹ 제λͺ©

🎨 μž…λ ₯ μ»΄ν¬λ„ŒνŠΈ

  • FormKit.Input - λΉ„λ°€λ²ˆν˜Έ ν† κΈ€, 이메일, 숫자 등을 μ§€μ›ν•˜λŠ” ν…μŠ€νŠΈ μž…λ ₯
  • FormKit.Textarea - μ—¬λŸ¬ 쀄 ν…μŠ€νŠΈ μž…λ ₯
  • FormKit.Select - 검색 κΈ°λŠ₯이 μžˆλŠ” λ“œλ‘­λ‹€μš΄ 선택 (Radix UI 기반)

🏷️ ν‘œμ‹œ μ»΄ν¬λ„ŒνŠΈ

  • FormKit.Label - ν•„λ“œ λ ˆμ΄λΈ”
  • FormKit.Title - 폼 제λͺ©
  • FormKit.Wrapper - μ»€μŠ€ν…€ λ ˆμ΄μ•„μ›ƒμš© μ»¨ν…Œμ΄λ„ˆ
  • FormKit.Unit - λ‹¨μœ„ ν‘œμ‹œ (예: "원", "kg")
  • FormKit.Error - 였λ₯˜ λ©”μ‹œμ§€ ν‘œμ‹œ

🎯 μ•‘μ…˜ μ»΄ν¬λ„ŒνŠΈ

  • FormKit.SubmitButton - λ‘œλ”© μƒνƒœκ°€ μžˆλŠ” 제좜 λ²„νŠΌ
  • FormKit.ResetButton - 폼을 μ΄ˆκΈ°κ°’μœΌλ‘œ μž¬μ„€μ •

πŸ“š API 레퍼런슀

FormKit.Root

λͺ¨λ“  μžμ‹ μ»΄ν¬λ„ŒνŠΈμ— μ»¨ν…μŠ€νŠΈλ₯Ό μ œκ³΅ν•˜λŠ” 메인 폼 μ»¨ν…Œμ΄λ„ˆμž…λ‹ˆλ‹€.

Prop Type ν•„μˆ˜ μ„€λͺ…
formId string βœ… 폼의 고유 μ‹λ³„μž
onSubmit (data) => void βœ… 폼 제좜 ν•Έλ“€λŸ¬
schema ZodSchema ❌ 검증을 μœ„ν•œ Zod μŠ€ν‚€λ§ˆ
defaultValues object ❌ κΈ°λ³Έ 폼 κ°’

FormKit.Field

μžλ™ λ ˆμ΄λΈ”-μž…λ ₯ 연결이 μžˆλŠ” 폼 μž…λ ₯용 μ»¨ν…Œμ΄λ„ˆμž…λ‹ˆλ‹€.

Prop Type κΈ°λ³Έκ°’ μ„€λͺ…
isInline boolean false λ ˆμ΄λΈ”κ³Ό μž…λ ₯을 κ°€λ‘œλ‘œ ν‘œμ‹œ
hidden boolean false ν•„λ“œ 숨기기
htmlFor string auto λ ˆμ΄λΈ”-μž…λ ₯ 연결을 μœ„ν•œ μ»€μŠ€ν…€ ID

FormKit.Input

λ‚΄μž₯ κΈ°λŠ₯이 μžˆλŠ” ν–₯μƒλœ μž…λ ₯ μ»΄ν¬λ„ŒνŠΈμž…λ‹ˆλ‹€.

Prop Type ν•„μˆ˜ μ„€λͺ…
name string βœ… ν•„λ“œ 이름
type string ❌ μž…λ ₯ νƒ€μž… (text, email, password λ“±)
required boolean ❌ ν•„λ“œλ₯Ό ν•„μˆ˜λ‘œ ν‘œμ‹œ
minLength number ❌ μ΅œμ†Œ 문자 길이
maxLength number ❌ μ΅œλŒ€ 문자 길이

κΈ°λŠ₯:

  • πŸ”’ type="password"에 λŒ€ν•œ μžλ™ λΉ„λ°€λ²ˆν˜Έ κ°€μ‹œμ„± ν† κΈ€
  • βœ… 확인 ν•„λ“œμ— λŒ€ν•œ μžλ™ 검증 (예: confirmPassword)
  • 🎯 μ™„μ „ν•œ TypeScript 지원

FormKit.Select

Radix UIλ₯Ό μ‚¬μš©ν•œ λ“œλ‘­λ‹€μš΄ 선택 μ»΄ν¬λ„ŒνŠΈμž…λ‹ˆλ‹€.

Prop Type ν•„μˆ˜ μ„€λͺ…
name string βœ… ν•„λ“œ 이름
options Array<{value, label}> βœ… 선택 μ˜΅μ…˜
placeholder string ❌ ν”Œλ ˆμ΄μŠ€ν™€λ” ν…μŠ€νŠΈ
required boolean ❌ ν•„λ“œλ₯Ό ν•„μˆ˜λ‘œ ν‘œμ‹œ

FormKit.Textarea

μ—¬λŸ¬ 쀄 ν…μŠ€νŠΈ μž…λ ₯ μ»΄ν¬λ„ŒνŠΈμž…λ‹ˆλ‹€.

Prop Type ν•„μˆ˜ μ„€λͺ…
name string βœ… ν•„λ“œ 이름
required boolean ❌ ν•„λ“œλ₯Ό ν•„μˆ˜λ‘œ ν‘œμ‹œ
minLength number ❌ μ΅œμ†Œ 문자 길이
maxLength number ❌ μ΅œλŒ€ 문자 길이
rows number ❌ ν‘œμ‹œλ˜λŠ” ν…μŠ€νŠΈ 쀄 수 (κΈ°λ³Έκ°’: 4)

FormKit.Fieldset

κ΄€λ ¨λœ 폼 ν•„λ“œλ₯Ό ν•¨κ»˜ κ·Έλ£Ήν™”ν•©λ‹ˆλ‹€.

Prop Type ν•„μˆ˜ μ„€λͺ…
className string ❌ μ»€μŠ€ν…€ CSS 클래슀
children ReactNode βœ… μžμ‹ μ»΄ν¬λ„ŒνŠΈ

FormKit.Legend

ν•„μˆ˜ ν‘œμ‹œκ°€ μ„ νƒμ μœΌλ‘œ μžˆλŠ” ν•„λ“œμ…‹ 제λͺ©μž…λ‹ˆλ‹€.

Prop Type ν•„μˆ˜ μ„€λͺ…
required boolean ❌ 빨간색 λ³„ν‘œ(*) ν‘œμ‹œ
className string ❌ μ»€μŠ€ν…€ CSS 클래슀
children ReactNode βœ… λ²”λ‘€ ν…μŠ€νŠΈ

FormKit.Label

폼 μž…λ ₯을 μœ„ν•œ μ ‘κ·Ό κ°€λŠ₯ν•œ λ ˆμ΄λΈ”μž…λ‹ˆλ‹€.

Prop Type ν•„μˆ˜ μ„€λͺ…
className string ❌ μ»€μŠ€ν…€ CSS 클래슀
children ReactNode βœ… λ ˆμ΄λΈ” ν…μŠ€νŠΈ

FormKit.Title

폼 제λͺ©/헀더 μ»΄ν¬λ„ŒνŠΈμž…λ‹ˆλ‹€.

Prop Type ν•„μˆ˜ μ„€λͺ…
className string ❌ μ»€μŠ€ν…€ CSS 클래슀
children ReactNode βœ… 제λͺ© ν…μŠ€νŠΈ

FormKit.SubmitButton

λ‚΄μž₯ λ‘œλ”© μƒνƒœκ°€ μžˆλŠ” 제좜 λ²„νŠΌμž…λ‹ˆλ‹€.

Prop Type ν•„μˆ˜ μ„€λͺ…
variant string ❌ λ²„νŠΌ μŠ€νƒ€μΌ λ³€ν˜•
disabled boolean ❌ λ²„νŠΌ λΉ„ν™œμ„±ν™”
className string ❌ μ»€μŠ€ν…€ CSS 클래슀
children ReactNode βœ… λ²„νŠΌ ν…μŠ€νŠΈ

FormKit.ResetButton

폼을 μ΄ˆκΈ°κ°’μœΌλ‘œ μž¬μ„€μ •ν•©λ‹ˆλ‹€.

Prop Type ν•„μˆ˜ μ„€λͺ…
onClick function ❌ μΆ”κ°€ 클릭 ν•Έλ“€λŸ¬
className string ❌ μ»€μŠ€ν…€ CSS 클래슀
children ReactNode βœ… λ²„νŠΌ ν…μŠ€νŠΈ

FormKit.Wrapper

μ»€μŠ€ν…€ λ ˆμ΄μ•„μ›ƒμš© μ»¨ν…Œμ΄λ„ˆ μ»΄ν¬λ„ŒνŠΈμž…λ‹ˆλ‹€.

Prop Type ν•„μˆ˜ μ„€λͺ…
className string ❌ μ»€μŠ€ν…€ CSS 클래슀
children ReactNode βœ… μžμ‹ μ»΄ν¬λ„ŒνŠΈ

FormKit.Unit

μž…λ ₯ ν•„λ“œ μ˜†μ— λ‹¨μœ„λ₯Ό ν‘œμ‹œν•©λ‹ˆλ‹€.

Prop Type ν•„μˆ˜ μ„€λͺ…
unit string βœ… λ‹¨μœ„ ν…μŠ€νŠΈ (예: "원", "kg", "%")

FormKit.Error

검증 였λ₯˜ λ©”μ‹œμ§€λ₯Ό ν‘œμ‹œν•©λ‹ˆλ‹€.

Prop Type ν•„μˆ˜ μ„€λͺ…
error FieldError ❌ react-hook-form의 였λ₯˜ 객체

🎨 μŠ€νƒ€μΌλ§

FormKit은 ν…Œλ§ˆλ₯Ό μœ„ν•΄ CSS λ³€μˆ˜μ™€ ν•¨κ»˜ Tailwind CSSλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€. CSS에 λ‹€μŒ λ³€μˆ˜λ₯Ό μΆ”κ°€ν•˜μ„Έμš”:

:root {
  --border: 214.3 31.8% 91.4%;
  --input: 214.3 31.8% 91.4%;
  --ring: 222.2 84% 4.9%;
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;
  --secondary: 210 40% 96.1%;
  --secondary-foreground: 222.2 47.4% 11.2%;
  --destructive: 0 84.2% 60.2%;
  --destructive-foreground: 210 40% 98%;
  --muted: 210 40% 96.1%;
  --muted-foreground: 215.4 16.3% 46.9%;
  --accent: 210 40% 96.1%;
  --accent-foreground: 222.2 47.4% 11.2%;
  --popover: 0 0% 100%;
  --popover-foreground: 222.2 84% 4.9%;
}

.dark {
  --border: 217.2 32.6% 17.5%;
  --input: 217.2 32.6% 17.5%;
  --ring: 212.7 26.8% 83.9%;
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  /* ... λ‹€λ₯Έ 닀크 λͺ¨λ“œ λ³€μˆ˜λ“€ */
}

πŸ§ͺ ν…ŒμŠ€νŒ…

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import FormKit from '@jiin.seok/formkit-react'

test('폼 데이터 제좜', async () => {
  const handleSubmit = jest.fn()
  
  render(
    <FormKit.Root formId="test" onSubmit={handleSubmit}>
      <FormKit.Field>
        <FormKit.Input name="username" />
      </FormKit.Field>
      <FormKit.SubmitButton>제좜</FormKit.SubmitButton>
    </FormKit.Root>
  )
  
  await userEvent.type(screen.getByRole('textbox'), 'john')
  await userEvent.click(screen.getByRole('button'))
  
  expect(handleSubmit).toHaveBeenCalledWith({ username: 'john' })
})

πŸ“„ λΌμ΄μ„ μŠ€

MIT Β© [Jiin Seok]

πŸ”— 링크

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors