🚀 Compound Component Pattern과 내장 검증, TypeScript 지원을 갖춘 강력한 React 폼 라이브러리
- 🎯 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-reactnpm 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>
)
}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>
)
}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 - 폼을 초기값으로 재설정
모든 자식 컴포넌트에 컨텍스트를 제공하는 메인 폼 컨테이너입니다.
| Prop | Type | 필수 | 설명 |
|---|---|---|---|
| formId | string | ✅ | 폼의 고유 식별자 |
| onSubmit | (data) => void | ✅ | 폼 제출 핸들러 |
| schema | ZodSchema | ❌ | 검증을 위한 Zod 스키마 |
| defaultValues | object | ❌ | 기본 폼 값 |
자동 레이블-입력 연결이 있는 폼 입력용 컨테이너입니다.
| Prop | Type | 기본값 | 설명 |
|---|---|---|---|
| isInline | boolean | false | 레이블과 입력을 가로로 표시 |
| hidden | boolean | false | 필드 숨기기 |
| htmlFor | string | auto | 레이블-입력 연결을 위한 커스텀 ID |
내장 기능이 있는 향상된 입력 컴포넌트입니다.
| Prop | Type | 필수 | 설명 |
|---|---|---|---|
| name | string | ✅ | 필드 이름 |
| type | string | ❌ | 입력 타입 (text, email, password 등) |
| required | boolean | ❌ | 필드를 필수로 표시 |
| minLength | number | ❌ | 최소 문자 길이 |
| maxLength | number | ❌ | 최대 문자 길이 |
기능:
- 🔒
type="password"에 대한 자동 비밀번호 가시성 토글 - ✅ 확인 필드에 대한 자동 검증 (예:
confirmPassword) - 🎯 완전한 TypeScript 지원
Radix UI를 사용한 드롭다운 선택 컴포넌트입니다.
| Prop | Type | 필수 | 설명 |
|---|---|---|---|
| name | string | ✅ | 필드 이름 |
| options | Array<{value, label}> | ✅ | 선택 옵션 |
| placeholder | string | ❌ | 플레이스홀더 텍스트 |
| required | boolean | ❌ | 필드를 필수로 표시 |
여러 줄 텍스트 입력 컴포넌트입니다.
| Prop | Type | 필수 | 설명 |
|---|---|---|---|
| name | string | ✅ | 필드 이름 |
| required | boolean | ❌ | 필드를 필수로 표시 |
| minLength | number | ❌ | 최소 문자 길이 |
| maxLength | number | ❌ | 최대 문자 길이 |
| rows | number | ❌ | 표시되는 텍스트 줄 수 (기본값: 4) |
관련된 폼 필드를 함께 그룹화합니다.
| Prop | Type | 필수 | 설명 |
|---|---|---|---|
| className | string | ❌ | 커스텀 CSS 클래스 |
| children | ReactNode | ✅ | 자식 컴포넌트 |
필수 표시가 선택적으로 있는 필드셋 제목입니다.
| Prop | Type | 필수 | 설명 |
|---|---|---|---|
| required | boolean | ❌ | 빨간색 별표(*) 표시 |
| className | string | ❌ | 커스텀 CSS 클래스 |
| children | ReactNode | ✅ | 범례 텍스트 |
폼 입력을 위한 접근 가능한 레이블입니다.
| Prop | Type | 필수 | 설명 |
|---|---|---|---|
| className | string | ❌ | 커스텀 CSS 클래스 |
| children | ReactNode | ✅ | 레이블 텍스트 |
폼 제목/헤더 컴포넌트입니다.
| Prop | Type | 필수 | 설명 |
|---|---|---|---|
| className | string | ❌ | 커스텀 CSS 클래스 |
| children | ReactNode | ✅ | 제목 텍스트 |
내장 로딩 상태가 있는 제출 버튼입니다.
| Prop | Type | 필수 | 설명 |
|---|---|---|---|
| variant | string | ❌ | 버튼 스타일 변형 |
| disabled | boolean | ❌ | 버튼 비활성화 |
| className | string | ❌ | 커스텀 CSS 클래스 |
| children | ReactNode | ✅ | 버튼 텍스트 |
폼을 초기값으로 재설정합니다.
| Prop | Type | 필수 | 설명 |
|---|---|---|---|
| onClick | function | ❌ | 추가 클릭 핸들러 |
| className | string | ❌ | 커스텀 CSS 클래스 |
| children | ReactNode | ✅ | 버튼 텍스트 |
커스텀 레이아웃용 컨테이너 컴포넌트입니다.
| Prop | Type | 필수 | 설명 |
|---|---|---|---|
| className | string | ❌ | 커스텀 CSS 클래스 |
| children | ReactNode | ✅ | 자식 컴포넌트 |
입력 필드 옆에 단위를 표시합니다.
| Prop | Type | 필수 | 설명 |
|---|---|---|---|
| unit | string | ✅ | 단위 텍스트 (예: "원", "kg", "%") |
검증 오류 메시지를 표시합니다.
| 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]