본문 바로가기
카테캠/2단계

[7/9] 중복 전화번호 검사, 모달 취소 시 되돌리기

by 쪼꼬에몽 2025. 7. 9.
<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