복잡한 컴포넌트 안쪽에 여러 필드가 있을 때
외부에서 이 컴포넌트의 상태를 가져오는 가장 좋은 방법은?
✅ react-hook-form + Controller 를 사용해서
✅ 컴포넌트 전체를 하나의 Controlled 컴포넌트로 만드는 것
부모 Form
└ 복잡한 컴포넌트 (Controlled 컴포넌트)
├ TextField
├ Select
├ ToggleButtonGroup
└ 기타 입력 요소들
import { useForm, Controller } from 'react-hook-form';
import ComplexFormGroup from './ComplexFormGroup';
interface ParentFormValues {
group: {
name: string;
category: string;
coin: number;
};
}
function ParentForm() {
const { control, handleSubmit } = useForm<ParentFormValues>({
defaultValues: {
group: {
name: '',
category: '',
coin: 1,
},
},
});
const onSubmit = (data: ParentFormValues) => {
console.log('Form 전체 데이터:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller // 주목!!
name="group"
control={control}
render={({ field }) => (
<ComplexFormGroup
value={field.value}
onChange={field.onChange}
/>
)}
/>
<button type="submit">제출</button>
</form>
);
}
interface ComplexFormGroupProps {
value: {
name: string;
category: string;
coin: number;
};
onChange: (value: { name: string; category: string; coin: number }) => void;
}
function ComplexFormGroup({ value, onChange }: ComplexFormGroupProps) {
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange({ ...value, name: e.target.value });
};
const handleCategoryChange = (e: React.ChangeEvent<{ value: unknown }>) => {
onChange({ ...value, category: e.target.value as string });
};
const handleCoinChange = (e: React.MouseEvent<HTMLElement>, newCoin: number | null) => {
if (newCoin !== null) {
onChange({ ...value, coin: newCoin });
}
};
return (
<>
<TextField
label="메뉴명"
value={value.name}
onChange={handleNameChange}
/>
<Select
value={value.category}
onChange={handleCategoryChange}
>
{/* 메뉴 아이템 뿌리기 */}
</Select>
<ToggleButtonGroup
value={value.coin}
exclusive
onChange={handleCoinChange}
>
<ToggleButton value={1}>1</ToggleButton>
<ToggleButton value={2}>2</ToggleButton>
<ToggleButton value={4}>4</ToggleButton>
</ToggleButtonGroup>
</>
);
}
import { useForm, Controller } from 'react-hook-form';
import { Button, Dialog, DialogActions, DialogContent, TextField } from '@mui/material';
import CoinToggleGroup from '@/apps/menu/components/CoinToggleGroup';
import CategorySelect from '@/apps/menu/components/CategorySelect';
interface CreateMenuFormValues {
menuName: string;
category: string;
coin: number;
}
function CreateMenuDialog({ open, onClose }: { open: boolean; onClose: () => void }) {
const { register, control, handleSubmit } = useForm<CreateMenuFormValues>({
defaultValues: {
menuName: '',
category: '',
coin: 1,
},
});
const onSubmit = (data: CreateMenuFormValues) => {
console.log('Form Data:', data);
onClose();
};
return (
<Dialog open={open} onClose={onClose}>
<form onSubmit={handleSubmit(onSubmit)}>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* 메뉴명 */}
<TextField
{...register('menuName')}
placeholder="메뉴명"
size="small"
fullWidth
/>
{/* 카테고리 */}
<TextField
{...register('category')}
select
label="카테고리"
fullWidth
size="small"
>
{/* 여기에 MenuCategoryKeys.map으로 MenuItem 뿌리면 됨 */}
</TextField>
{/* 코인 선택 */}
<Controller
name="coin"
control={control}
render={({ field }) => (
<CoinToggleGroup
value={field.value}
onChange={(_, newValue) => field.onChange(newValue)}
/>
)}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>취소</Button>
<Button type="submit">확인</Button>
</DialogActions>
</form>
</Dialog>
);
}
export default CreateMenuDialog;
MealMenus.tsx (상위 컴포넌트)import { useForm } from 'react-hook-form';
import CreateMenuDialog from './CreateMenuDialog';
import { CreateMenuFormValues } from '@/apps/menu/types/menuForm';
function MealMenus() {
const { control, handleSubmit } = useForm<CreateMenuFormValues>({
defaultValues: {
menuName: '',
category: '',
coin: 1,
},
});
const handleCreateMenu = (data: CreateMenuFormValues) => {
console.log('추가할 메뉴:', data);
// 서버에 전송하거나 상태 업데이트
};
return (
<>
{/* 메뉴 목록 */}
{/* Add 버튼 */}
<CreateMenuDialog
control={control}
onSubmit={handleSubmit(handleCreateMenu)}
/>
</>
);
}
CreateMenuDialog.tsx (하위컴포넌트)import { Control, Controller } from 'react-hook-form';
import { CreateMenuFormValues } from '@/apps/menu/types/menuForm';
interface CreateMenuDialogProps {
control: Control<CreateMenuFormValues>;
onSubmit: () => void; // react-hook-form의 handleSubmit로 넘겨받음
}
function CreateMenuDialog({ control, onSubmit }: CreateMenuDialogProps) {
return (
<Dialog open>
<form onSubmit={onSubmit}>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Controller
name="menuName"
control={control}
render={({ field }) => (
<TextField {...field} placeholder="메뉴명" size="small" fullWidth />
)}
/>
<Controller
name="category"
control={control}
render={({ field }) => (
<Select {...field} label="카테고리" fullWidth size="small">
{/* MenuCategoryKeys.map */}
</Select>
)}
/>
<Controller
name="coin"
control={control}
render={({ field }) => (
<CoinToggleGroup
value={field.value}
onChange={(_, newCoin) => field.onChange(newCoin)}
/>
)}
/>
</DialogContent>
<DialogActions>
<Button type="submit">확인</Button>
</DialogActions>
</form>
</Dialog>
);
}