<ReceiverItem>
<span>전화번호</span>
<ItemInput>
<Input
{...register(`receiverInfo.${index}.phone`, {
required: '전화번호를 입력해주세요',
validate: value => isValidPhoneFlexible(value) || '전화번호 형식이 올바르지 않습니다',
})}
hasError={!!errors.receiverInfo?.[index]?.phone}
placeholder="전화번호를 입력하세요."
/>
{errors.receiverInfo?.[index]?.phone?.message &&
<p>{errors.receiverInfo[index].phone.message}</p>}
</ItemInput>
</ReceiverItem>
중복 전화 번호를 검사하기 위해서 watch를 이용한다.
const phones = watch("receiverInfo");
const isSamePhoneNumber = (value: string, index: number) => {
const allPhones = phones?.map(item => item.phone);
return allPhones?.filter((phone, i) => phone === value && i !== index).length === 0 || "전화번호가 중복되었습니다.";
}
watch를 추가하고 중복을 찾는 함수를 만든다.
<ReceiverItem>
<span>전화번호</span>
<ItemInput>
<Input
{...register(`receiverInfo.${index}.phone`, {
required: '전화번호를 입력해주세요',
validate: value => {
const formatValid = isValidPhoneFlexible(value);
if (!formatValid) return '전화번호 형식이 올바르지 않습니다';
return isSamePhoneNumber(value, index);
},
})}
hasError={!!errors.receiverInfo?.[index]?.phone}
placeholder="전화번호를 입력하세요."
/>
{errors.receiverInfo?.[index]?.phone?.message &&
<p>{errors.receiverInfo[index].phone.message}</p>}
</ItemInput>
</ReceiverItem>
취소 버튼을 누르면 현재 작업을 모두 삭제하고 이전 배열로 되돌려야 한다.
처음에는 isNew라는 새로운 객체를 만들어서 true와 false로 구분하려고 했다.
새로 추가한 내용은 지워지지만, 기존에 존재하는 것을 삭제했을 때 복구가 되지 않았다.
한 개 한 개 다루기가 복잡하여 이전 배열을 복사해서 취소 시 이전 배열 시점으로 되돌리기로 결정했다.
// 취소 시 되돌릴 이전 배열 저장 ref
const beforeRef = useRef<{ receiverInfo: Receiver[] } | null>(null);
useRef를 이용하여 이전 시점으로 되돌릴 배열 저장소를 만든다.
const handleOpenModal = () => {
// 현재 배열 상태 저장
beforeRef.current = getValues();
setModal(true);
}
modal이 열릴 때, getValues()를 이용해 현재 배열 값을 ref의 current에 저장한다.
const handleCancle = () => {
// 취소 시, 이전 배열이 있으면 이전 시점으로 reset
if (beforeRef.current) {
reset(beforeRef.current);
}
setModal(false);
}
취소 시, ref.current 값이 있으면 그 시점으로 다시 reset 한다.
message 컴포넌트에 작성한 form을 button 컴포넌트에서 유효성 검사하기
trigger, forwardRef, useImperativeHandle, triggerValidation 사용
trigger (react-hook-form)
- 특정 필드 또는 전체 폼 유효성 검사 수동 실행
const { trigger } = useForm();
await trigger("textMessage"); // textMesage 필드만 유효성 검사
- 버튼 클릭 등 폼 제출 외 상황에서 유효성 검사가 필요할 때
- 외부 컴포넌트에서 유효성 체크하고 싶을 때
forwardRef (React)
- 부모 컴포넌트가 자식 컴포넌트의 ref에 직접 접근할 수 있게 함
const MyComponent = forwardRef((props, ref) => {
// 자식 컴포넌트
});
- 부모가 자식 내부의 함수나 DOM 요소 등에 직접 접근하고 싶을 때
useImperativeHandle (React)
- forwardRef로 받은 ref 객체애 노출하고 싶은 값, 함수을 커스터마이즈해서 전달
useImperatvieHandle(ref, () => {(
triggerValidation: () => {
return trigger("textMessage");
}
}));
- 부모가 ref.current.내가정의한함수() 처럼 호출할 수 있도록 만들고 싶을 때
triggerValidation (사용자 정의)
- useImperativeHandle 안에서 만든 사용자 정의 메서드
- 부모가 ref.current.triggerValidation()으로 호출 가능
// 부모 컴포넌트
const isValid = await ref.current?.triggerValidation();
여기서 async와 await가 사용되는 이유
useImperativeHandle(ref, () => ({
triggerValidation: async () => {
return await trigger("textMessage");
}
}));
const isValid = await messageRef.current?.triggerValidation();
if (!isValid) {
alert("error");
return;
}
trigger()는 Promise를 반환하는 비동기 함수
- 폼 필드의 유효성 검사 결과를 promise<boolean> 형태로 반환함
const result: boolean = await trigger("textMessage");
- 유효하면 true, 아니면 false
await가 필요한 이유
- trigger 결과를 기다리기 위해서
- trigger()는 즉시 결과를 반환하지 않음
- await을 쓰지 않으면 isValid가 Promise 객체가 됨
// 결과가 boolean이 아니라 Promise<boolean>
const isValid = trigger("textMessage");
if (!isValid) { // 항상 true처럼 동작
}
- isValid는 Promise<boolean> 이고
- !isValid하면 !<Promise>는 항상 false가 되어버림
!Promise<false> -> false (객체는 truthy)
- if 문 안은 실행되지 않음
항상 true처럼 동작하는 이유
- 자바스크립트의 if 조건은 truthy/falsy로 평가됨
if (Promise.resolve(false)) { console.log("실행됨"); }
- Promise.resolve(false)는 false 값을 가지는 Promise 객체
- Promise 객체는 객체이기 때문에 항상 truthy
- if 문 안은 실행됨 - 값이 false여도
async가 필요한 이유
- await는 async 함수 안에서만 쓸 수 있기 때문
const triggerValidation = async() => {
return await trigger("textMessage");
}
message 컴포넌트
type Props = {
message: string;
setMessage: (msg: string) => void;
};
export type Ref = {
triggerValidation: () => Promise<boolean>;
};
function Message2Component({ message, setMessage }: Props, ref: React.Ref<Ref>) {
const { register, handleSubmit, trigger, clearErrors, formState: { errors } } = useForm();
useImperativeHandle(ref, () => ({
triggerValidation: async () => {
return await trigger("textMessage");
}
}));
return (
<Wrapper>
<form onSubmit={handleSubmit(onSubmit)}>
<textarea
{...register("textMessage", {
required: "메시지를 입력해주세요."
})}
value={message}
onChange={(e) => {
setMessage(e.target.value);
clearErrors("textMessage");
}}
/>
{errors.textMessage && <p>{errors.textMessage.message}</p>}
</form>
</Wrapper>
);
}
// 타입을 확실히 명시하고 export
const Message2 = forwardRef<Ref, Props>(Message2Component);
export default Message2;
button 컴포넌트
const handleClick = async () => {
const isValid = await messageRef.current?.triggerValidation();
if (!isValid) {
return;
}
}
return (
<PriceButton onClick={handleClick}>{price}원 주문하기</PriceButton>
)
}
부모 컴포넌트
const messageRef = useRef(null);
return (
<>
<Message2 ref={messageRef} message={message} setMessage={setMessage} />
<OrderButton2 messageRef={messageRef} id={id} count={count} message={message} setMessage={setMessage} sender={sender} setSender={setSender} />
</>
)
}
'카테캠 > 2단계' 카테고리의 다른 글
| [7/14]zod 라이브러리 (0) | 2025.07.14 |
|---|---|
| [7/10] useFormContext (1) | 2025.07.14 |
| [7/10] 고차 함수 (0) | 2025.07.10 |
| [7/8] React Hook Form 사용하기 w. useFieldArray (0) | 2025.07.09 |
| [7/7] hooks와 validate로 나누기 (0) | 2025.07.07 |