React-Hook-Form에 대해서 알아보기 전에 이를 이해하기 위한 비제어 컴포넌트와 제어 컴포넌트에 대해서 먼저 알아보자
공식문서의 설명
제어 컴포넌트는 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트합니다. React에서는 변경할 수 있는 state가 일반적으로 컴포넌트의 state 속성에 유지되며 setState()에 의해 업데이트됩니다.
(...)
이러한 방식으로 React에 의해 값이 제어되는 입력 폼 엘리먼트를 “제어 컴포넌트 (controlled component)“라고 합니다.
state로 값을 저장하는 방식코드 예시
import React, { useState } from 'react';
function MyInput() {
const [inputValue, setInputValue] = useState(null);
const handleChange = (e) => {
setInputValue(e.target.value);
};
return <input onChange={(e) => handleChange(e)} value={inputValue} />;
}
ref로 값을 저장하는 방식 코드 예시
import React, { useRef } from 'react';
function MyInput() {
const inputNode = useRef(null);
return <input ref={inputNode} />;
}
export default MyInput;
| 기능 | 제어 컴포넌트 | 비제어 컴포넌트 |
|---|---|---|
| 일회성 정보 검색 (예: 제출) | O | O |
| 제출 시 값 검증 | O | O |
| 실시간으로 필드값의 유효성 검사 | O | X |
| 조건부로 제출 버튼 비활성화 (disabled) | O | X |
| 실시간으로 입력 형식 적용하기 (숫자만 가능하게 등) | O | X |
| 동적 입력 | O | X |
비제어 컴포넌트 기반의 라이브러리이다.npm install react-rook-form
import React from 'react';
import { useForm } from 'react-hook-form';
function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = data => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email", { required: true })} placeholder="First Name" />
{errors.firstName && <p>email name is required</p>}
<input {...register("password", { required: true })} placeholder="Last Name" />
{errors.lastName && <p>password is required</p>}
<button type="submit">Submit</button>
</form>
);
}
export default MyForm;

'use client';
const ProfileEditPage = () => {
const { editInputs, name, state, email, phone, setName, setState, setEmail, setPhone } = useInput();
const { openSnackBar } = useSnackBar();
const [isOpen, setIsOpen] = useState<boolean>(false);
const userId = useUserStore((state) => state.userId);
const workspaceId = useWorkspaceId();
const router = useRouter();
const [image, setImage] = useState<File | null>();
const [imageURL, setImageURL] = useState<string | ArrayBuffer | null>();
const params = useParams();
const workspaceUserId = params.targetWorkspaceUserId as string;
const { workspaceUser, isPending, isError, updateWorkspaceUser } = useWorkspaceUser(workspaceUserId);
const profileImage = workspaceUser && workspaceUser.profile_image;
const workspaceName = workspaceUser && workspaceUser.name;
const workspaceEmail = workspaceUser && workspaceUser.email;
const workspacePhone = workspaceUser && workspaceUser.phone;
const workspaceState = workspaceUser && workspaceUser.state;
const workspaceIsOpen = workspaceUser && workspaceUser.is_open;
const setEmptyStr = (category: string | null | undefined): string => {
if (!category) return '';
else return category;
};
useEffect(() => {
setImageURL(profileImage);
setName(setEmptyStr(workspaceName));
setState(setEmptyStr(workspaceState));
setEmail(setEmptyStr(workspaceEmail));
setPhone(setEmptyStr(workspacePhone));
setIsOpen(workspaceIsOpen ? workspaceIsOpen : false);
}, [profileImage, workspaceName, workspaceEmail, workspacePhone, workspaceState, workspaceIsOpen]);
const handleIsOpenClick = () => {
setIsOpen((prev) => !prev);
};
const handleProfileImageChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files) return;
const file = e.target.files[0];
if (!file) {
openSnackBar({ message: '파일이 존재하지 않습니다' });
return;
}
if (file.size > 3000000) {
openSnackBar({ message: '이미지는 3MB를 넘길 수 없어요' });
return;
}
setImage(file);
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
setImageURL(reader.result);
};
};
const handleEdit = async () => {
if (!(userId && workspaceId)) return;
if (!name) {
openSnackBar({ message: '이름이 존재하지 않아요' });
return;
}
if (image) {
const filename = crypto.randomUUID();
await api.storageProfile.postStorageProfile(image, filename);
const profile_image = await api.storageProfile.getStorageProfile(filename);
const workspaceUser = {
id: workspaceUserId,
user_id: userId,
workspace_id: workspaceId,
name,
email,
phone,
state,
is_open: isOpen,
profile_image
};
await updateWorkspaceUser(workspaceUser);
router.back();
return;
} else {
const workspaceUser = {
id: workspaceUserId,
user_id: userId,
workspace_id: workspaceId,
name,
email,
phone,
is_open: isOpen,
state
};
await updateWorkspaceUser(workspaceUser);
router.back();
return;
}
};
if (isPending) return <Loading />;
if (isError) return <NotFoundError />;
return (
<div>
<Header title="내 프로필 편집" type="edit" />
<main>
<div className="flex flex-col w-full items-center px-5 pb-[36px]">
<ProfileImgButton imageURL={imageURL} handleProfileImageChange={handleProfileImageChange} />
<div className="flex flex-col w-full gap-[16px] mb-[30px]">
{editInputs.map((editInput) => {
if (editInput.label === '활동상태')
return (
<div key={editInput.label}>
<BottomSheetModal isUp={true}>
<InputBottomSheet editInput={editInput} />
</BottomSheetModal>
<TextFieldButton
LabelColor="grey400"
label={editInput.label}
value={editInput.value}
onClick={() => editInput.handleFn(editInput.value)}
/>
<div className="lg:border-grey50 lg:border-b-[1px]" />
</div>
);
else
return (
<div key={editInput.label}>
<EditTextfield
label={editInput.label}
labelColor="grey400"
value={editInput.value}
onChange={editInput.handleFn}
isRequired={editInput.isRequeired}
/>
<div className="lg:border-grey50 lg:border-b-[1px]" />
</div>
);
})}
<IsOpenInput isOpen={isOpen} handleIsOpenClick={handleIsOpenClick} />
</div>
<Button theme="primary" isFullWidth onClick={handleEdit}>
수정하기
</Button>
</div>
</main>
</div>
);
};
export default ProfileEditPage;
import useBottomsheetModalBackDropStore from '@/store/bottomsheetModalBackDropStore';
import { useState } from 'react';
const useInput = () => {
const [name, setName] = useState<string | undefined>('');
const [state, setState] = useState<string | undefined>('');
const [email, setEmail] = useState<string | undefined>('');
const [phone, setPhone] = useState<string | undefined>('');
const handleOpen = useBottomsheetModalBackDropStore((state) => state.handleOpen);
const handleNameChange = (value: string | undefined) => {
setName(value);
};
const handleStateChange = (value: string | undefined) => {
setState(value);
handleOpen();
};
const handleEmailChange = (value: string | undefined) => {
setEmail(value);
};
const handlePhoneChange = (value: string | undefined) => {
setPhone(value);
};
const editInputs = [
{
label: '이름',
value: name,
isRequeired: true,
handleFn: handleNameChange
},
{
label: '활동상태',
value: state,
isRequeired: false,
handleFn: handleStateChange
},
{
label: '이메일',
value: email,
isRequeired: false,
handleFn: handleEmailChange
},
{
label: '전화번호',
value: phone,
isRequeired: false,
handleFn: handlePhoneChange
}
];
return {
editInputs,
name,
state,
email,
phone,
setName,
setState,
setEmail,
setPhone
};
};
export default useInput;