Skip to Content

Checkbox

체크 가능한 옵션을 선택하는 폼 컴포넌트


개요

Checkbox 컴포넌트는 Radix UI의 Checkbox Primitive를 기반으로 구축된 체크박스 컴포넌트입니다.

주요 특징

  • Radix UI 기반: 접근성이 검증된 Headless UI
  • Indeterminate 상태: 부분 선택 상태 지원
  • 폼 통합: React Hook Form과 완벽 호환
  • 키보드 네비게이션: Space 키로 토글
  • WCAG 2.1 AA: 접근성 표준 준수

설치

CLI로 추가 (권장)

npx @vortex/cli add checkbox

자동으로 설치되는 것들:

  • src/components/ui/checkbox.tsx
  • @radix-ui/react-checkbox 의존성
  • Tailwind CSS 스타일

수동 설치

pnpm add @radix-ui/react-checkbox

기본 사용법

Default

import { Checkbox } from "@/components/ui/checkbox"; export default function CheckboxDemo() { return ( <div className="flex items-center space-x-2"> <Checkbox id="terms" /> <label htmlFor="terms" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" > 이용약관에 동의합니다 </label> </div> ); }

Checked

<Checkbox id="terms" defaultChecked />

Disabled

<Checkbox id="terms" disabled />

Indeterminate 상태

부분 선택 상태를 표시할 때 사용합니다.

예시: 전체 선택/해제

"use client"; import { Checkbox } from "@/components/ui/checkbox"; import { useState } from "react"; export default function IndeterminateDemo() { const [items, setItems] = useState([ { id: "item1", checked: false }, { id: "item2", checked: false }, { id: "item3", checked: false }, ]); const allChecked = items.every((item) => item.checked); const someChecked = items.some((item) => item.checked); const indeterminate = someChecked && !allChecked; return ( <div className="space-y-4"> <div className="flex items-center space-x-2"> <Checkbox id="all" checked={allChecked} // @ts-ignore - Radix UI supports indeterminate indeterminate={indeterminate} onCheckedChange={(checked) => { setItems(items.map((item) => ({ ...item, checked: !!checked }))); }} /> <label htmlFor="all" className="text-sm font-medium"> 전체 선택 </label> </div> <div className="ml-6 space-y-2"> {items.map((item) => ( <div key={item.id} className="flex items-center space-x-2"> <Checkbox id={item.id} checked={item.checked} onCheckedChange={(checked) => { setItems( items.map((i) => i.id === item.id ? { ...i, checked: !!checked } : i ) ); }} /> <label htmlFor={item.id} className="text-sm"> {item.id} </label> </div> ))} </div> </div> ); }

언제 사용하는가

✅ 권장 사용 사례

  • 다중 선택 폼: 여러 옵션을 동시에 선택 가능할 때
  • 설정 토글: 기능 활성화/비활성화
  • 동의 확인: 약관 동의, 정책 확인
  • 필터링: 검색 결과 필터 옵션
  • 권한 관리: 역할별 권한 선택

예시

// 관심 분야 선택 <div className="space-y-2"> <Checkbox id="tech" /> <label htmlFor="tech">기술</label> <Checkbox id="design" /> <label htmlFor="design">디자인</label> <Checkbox id="business" /> <label htmlFor="business">비즈니스</label> </div>

언제 사용하지 말아야 하는가

❌ 피해야 할 사용 사례

  • 단일 선택: Radio 사용 권장
  • 즉시 실행 액션: Switch 또는 Button 사용
  • 5개 이상의 옵션: Select 또는 Multi-select 고려
  • 복잡한 계층 구조: Tree view 컴포넌트 사용

대안

상황대안이유
단일 옵션 선택Radio명확한 단일 선택 의도 표현
즉시 실행 필요Switch상태 변경 즉시 적용
많은 옵션 (5개 이상)Select공간 절약 및 검색 가능
ON/OFF 토글Switch즉각적인 상태 변경 시각화
트리 구조 선택Tree계층적 관계 표현

Advanced Usage

React Hook Form 통합

"use client"; import { Checkbox } from "@/components/ui/checkbox"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; const formSchema = z.object({ terms: z.boolean().refine((val) => val === true, { message: "이용약관에 동의해야 합니다", }), marketing: z.boolean().optional(), }); export default function FormDemo() { const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { terms: false, marketing: false, }, }); return ( <form onSubmit={form.handleSubmit((data) => console.log(data))}> <div className="space-y-4"> <div className="flex items-center space-x-2"> <Checkbox id="terms" {...form.register("terms")} onCheckedChange={(checked) => form.setValue("terms", !!checked)} /> <label htmlFor="terms">이용약관 동의 (필수)</label> </div> {form.formState.errors.terms && ( <p className="text-sm text-destructive"> {form.formState.errors.terms.message} </p> )} <div className="flex items-center space-x-2"> <Checkbox id="marketing" {...form.register("marketing")} onCheckedChange={(checked) => form.setValue("marketing", !!checked)} /> <label htmlFor="marketing">마케팅 정보 수신 동의 (선택)</label> </div> <button type="submit">제출</button> </div> </form> ); }

Controlled Component

"use client"; import { Checkbox } from "@/components/ui/checkbox"; import { useState } from "react"; export default function ControlledDemo() { const [checked, setChecked] = useState(false); return ( <div className="space-y-4"> <div className="flex items-center space-x-2"> <Checkbox id="controlled" checked={checked} onCheckedChange={setChecked} /> <label htmlFor="controlled">제어된 체크박스</label> </div> <p className="text-sm text-muted-foreground"> 현재 상태: {checked ? "체크됨" : "해제됨"} </p> <button onClick={() => setChecked(!checked)}>토글</button> </div> ); }

체크박스 그룹

"use client"; import { Checkbox } from "@/components/ui/checkbox"; import { useState } from "react"; const frameworks = [ { id: "react", label: "React" }, { id: "vue", label: "Vue" }, { id: "angular", label: "Angular" }, { id: "svelte", label: "Svelte" }, ]; export default function CheckboxGroup() { const [selected, setSelected] = useState<string[]>([]); return ( <div className="space-y-4"> <h3 className="text-sm font-medium">선호하는 프레임워크를 선택하세요</h3> <div className="space-y-2"> {frameworks.map((framework) => ( <div key={framework.id} className="flex items-center space-x-2"> <Checkbox id={framework.id} checked={selected.includes(framework.id)} onCheckedChange={(checked) => { if (checked) { setSelected([...selected, framework.id]); } else { setSelected(selected.filter((id) => id !== framework.id)); } }} /> <label htmlFor={framework.id} className="text-sm"> {framework.label} </label> </div> ))} </div> <p className="text-sm text-muted-foreground"> 선택됨: {selected.join(", ") || "없음"} </p> </div> ); }

접근성 (Accessibility)

ARIA Attributes

Radix UI Checkbox가 자동으로 제공:

<Checkbox // aria-checked="true" | "false" | "mixed" // role="checkbox" // data-state="checked" | "unchecked" | "indeterminate" />

키보드 네비게이션

  • Tab: 체크박스로 포커스 이동
  • Space: 체크/해제 토글
  • Shift + Tab: 이전 요소로 포커스 이동

스크린 리더

<div className="flex items-center space-x-2"> <Checkbox id="terms" aria-describedby="terms-description" /> <label htmlFor="terms">이용약관 동의</label> </div> <p id="terms-description" className="sr-only"> 서비스 이용약관 및 개인정보 처리방침에 동의합니다 </p>

포커스 관리

"use client"; import { Checkbox } from "@/components/ui/checkbox"; import { useRef } from "react"; export default function FocusDemo() { const checkboxRef = useRef<HTMLButtonElement>(null); return ( <div className="space-y-4"> <Checkbox ref={checkboxRef} id="focus" /> <button onClick={() => checkboxRef.current?.focus()}> 체크박스에 포커스 </button> </div> ); }

Best Practices

1. Label 연결 필수

// ✅ 좋은 예 <div className="flex items-center space-x-2"> <Checkbox id="terms" /> <label htmlFor="terms">이용약관 동의</label> </div> // ❌ 나쁜 예 <Checkbox /> <span>이용약관 동의</span>

2. 명확한 라벨 텍스트

// ✅ 좋은 예 <label htmlFor="marketing"> 마케팅 정보 수신에 동의합니다 (선택) </label> // ❌ 나쁜 예 <label htmlFor="marketing">동의</label>

3. 필수/선택 표시

<div className="space-y-2"> <div className="flex items-center space-x-2"> <Checkbox id="required" /> <label htmlFor="required"> 필수 항목 <span className="text-destructive">*</span> </label> </div> <div className="flex items-center space-x-2"> <Checkbox id="optional" /> <label htmlFor="optional"> 선택 항목 <span className="text-muted-foreground">(선택)</span> </label> </div> </div>

4. 그룹 제목 제공

<fieldset> <legend className="text-sm font-medium mb-2">알림 설정</legend> <div className="space-y-2"> <div className="flex items-center space-x-2"> <Checkbox id="email" /> <label htmlFor="email">이메일 알림</label> </div> <div className="flex items-center space-x-2"> <Checkbox id="sms" /> <label htmlFor="sms">SMS 알림</label> </div> </div> </fieldset>

5. 에러 상태 표시

"use client"; import { Checkbox } from "@/components/ui/checkbox"; import { useState } from "react"; export default function ErrorDemo() { const [checked, setChecked] = useState(false); const [error, setError] = useState(""); return ( <div className="space-y-2"> <div className="flex items-center space-x-2"> <Checkbox id="terms" checked={checked} onCheckedChange={(checked) => { setChecked(!!checked); if (checked) setError(""); }} aria-invalid={!!error} className={error ? "border-destructive" : ""} /> <label htmlFor="terms">이용약관 동의 (필수)</label> </div> {error && ( <p className="text-sm text-destructive" role="alert"> {error} </p> )} </div> ); }

디자인 가이드라인

크기 및 간격

// 체크박스 크기: 16x16px (default) // Label과의 간격: 8px (space-x-2) // 체크박스 간 간격: 8px (space-y-2) <div className="space-y-2"> <div className="flex items-center space-x-2"> <Checkbox id="item1" /> <label htmlFor="item1">항목 1</label> </div> </div>

터치 영역

모바일 접근성을 위해 최소 44x44px 터치 영역 확보:

<div className="flex items-center space-x-2 min-h-[44px]"> <Checkbox id="touch" /> <label htmlFor="touch" className="cursor-pointer flex-1"> 터치 최적화 라벨 </label> </div>

색상 대비

WCAG 2.1 AA 기준 4.5:1 이상:

/* Checkbox 체크 상태 */ background: hsl(var(--primary)); /* 충분한 대비 */ color: hsl(var(--primary-foreground)); /* Focus ring */ outline: 2px solid hsl(var(--ring)); outline-offset: 2px;

TypeScript

Props 타입

import type { ComponentPropsWithoutRef } from "react"; import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; type CheckboxProps = ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>; // 사용 예시 const CustomCheckbox = (props: CheckboxProps) => { return <Checkbox {...props} />; };

Form 데이터 타입

import { z } from "zod"; const formSchema = z.object({ terms: z.boolean().refine((val) => val === true, { message: "이용약관에 동의해야 합니다", }), marketing: z.boolean().optional(), notifications: z.object({ email: z.boolean(), sms: z.boolean(), push: z.boolean(), }), }); type FormData = z.infer<typeof formSchema>;

Indeterminate 타입

type CheckedState = boolean | "indeterminate"; interface CheckboxGroupProps { items: Array<{ id: string; checked: boolean }>; onItemsChange: (items: Array<{ id: string; checked: boolean }>) => void; }

성능 최적화

많은 체크박스 렌더링

"use client"; import { Checkbox } from "@/components/ui/checkbox"; import { memo, useCallback, useState } from "react"; const CheckboxItem = memo( ({ id, label, checked, onChange, }: { id: string; label: string; checked: boolean; onChange: (id: string, checked: boolean) => void; }) => { return ( <div className="flex items-center space-x-2"> <Checkbox id={id} checked={checked} onCheckedChange={(checked) => onChange(id, !!checked)} /> <label htmlFor={id}>{label}</label> </div> ); } ); export default function OptimizedList() { const [items, setItems] = useState( Array.from({ length: 100 }, (_, i) => ({ id: `item-${i}`, label: `Item ${i}`, checked: false, })) ); const handleChange = useCallback((id: string, checked: boolean) => { setItems((prev) => prev.map((item) => (item.id === id ? { ...item, checked } : item)) ); }, []); return ( <div className="space-y-2"> {items.map((item) => ( <CheckboxItem key={item.id} id={item.id} label={item.label} checked={item.checked} onChange={handleChange} /> ))} </div> ); }

관련 컴포넌트

  • Radio: 단일 선택 옵션
  • Switch: 즉시 실행되는 토글
  • Select: 드롭다운 선택

참고 자료


지원 및 피드백

문제가 발생하거나 개선 제안이 있으신가요?

Last updated on