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

[7/8] React Hook Form 사용하기 w. useFieldArray

by 쪼꼬에몽 2025. 7. 9.
import { useEffect, useState } from 'react';
import {
  BaseContainer,
  BlurContainer,
  ModalAddBtn,
  ModalBottomBtn,
  ModalCancleBtn,
  ModalFinishBtn,
  ModalText,
  ModalTitle,
  ReceiverAddBtn,
  ReceiverInfo,
  ReceiverWrapper,
  RecevierTitle,
  Title_Btn,
} from '@/components/Order/Receiver2/Receiver2.style.ts';
import { useFieldArray, useForm } from 'react-hook-form';
import { isValidPhoneFlexible } from '@/utils/phoneValidation.ts';

export default function Receiver2() {
  const [modal, setModal] = useState(false);

  const { register, control, handleSubmit, formState: { errors, isSubmitted } } = useForm({
    defaultValues: {
      receiverInfo: [{ name: "", phone: "", count: 1}],
    },
  });

  const { fields , append, remove } = useFieldArray({
    control,
    name: "receiverInfo",
  });

  const onSubmit = data => {
    console.log(data)
    setModal(false);
  };

  // 모달 상태 스크롤 제어
  useEffect(() => {
    if (modal) { // 스크롤 막기
      document.body.style.overflow = 'hidden';
    } else { // 다시 하용
      document.body.style.overflow = 'auto';
    }

    // 컴포넌트가 사라질 때도 스크롤 생성
    return () => {
      document.body.style.overflow = 'auto';
    };
  }, [modal]);

  return (
    <>
      <ReceiverWrapper>
        <Title_Btn>
          <RecevierTitle>받는 사람</RecevierTitle>
          <ReceiverAddBtn onClick={() => setModal(true)}>추가</ReceiverAddBtn>
        </Title_Btn>
        <ReceiverInfo>
          <p>받는 사람이 없습니다.</p>
          <p>받는 사람을 추가해주세요.</p>
        </ReceiverInfo>
      </ReceiverWrapper>

      {modal && (
        <form onSubmit={handleSubmit(onSubmit)}>
          <BlurContainer onClick={() => setModal(false)} />
          <BaseContainer>
            <ModalTitle>받는 사람</ModalTitle>
            <ModalText>* 최대 10명까지 추가할 수 있어요.</ModalText>
            <ModalText>* 받는 사람의 전화번호를 중복으로 입력할 수 없어요.</ModalText>
            <ModalAddBtn onClick={() => append({ name: "", phone: "", count: 1 })}>추가하기</ModalAddBtn>

            {fields.map((field, index) => (
              <div key={field.id}>
                <div>받는 사람 {index + 1}</div>
                <button type="button" onClick={() => remove(index)}> 삭제 </button>

                <input
                  {...register(`receiverInfo.${index}.name`, {
                    required: "이름을 입력해주세요",
                  })}
                  placeholder="이름을 입력하세요."
                />
                {isSubmitted && errors.receiverInfo?.[index]?.name?.message && <p>{errors.receiverInfo?.[index]?.name?.message}</p>}

                <input
                  {...register(`receiverInfo.${index}.phone`, {
                    required: "전화번호를 입력해주세요",
                    validate: value => isValidPhoneFlexible(value) || "전화번호 형식이 올바르지 않습니다"
                  })}
                  placeholder="전화번호를 입력하세요."
                />
                {errors.receiverInfo?.[index]?.phone?.message && <p>{errors.receiverInfo?.[index]?.phone?.message}</p>}

                <input
                  type="number"
                  {...register(`receiverInfo.${index}.count`, {
                    required: "수량을 선택해주세요",
                    validate: value => value > 0 || "구매 수량은 1개 이상이어야 해요."
                  })}
                />
                {errors.receiverInfo?.[index]?.count?.message && <p>{errors.receiverInfo?.[index]?.count?.message}</p>}
              </div>
            ))}

            <ModalBottomBtn>
              <ModalCancleBtn onClick={() => setModal(false)}>취소</ModalCancleBtn>
              <ModalFinishBtn type="submit">완료</ModalFinishBtn>
            </ModalBottomBtn>
          </BaseContainer>
        </form>
      )}
    </>
  )
}

 

모달 창을 만들고 있다.

react hoo form 부분만 발췌를 하면 

const { register, control, handleSubmit, formState: { errors, isSubmitted } } = useForm({
  defaultValues: {
    receiverInfo: [{ name: "", phone: "", count: 1}],
  },
});

const { fields , append, remove } = useFieldArray({
  control,
  name: "receiverInfo",
});

  const onSubmit = data => {
    console.log(data)
    setModal(false);
  };
   
   return (
  <form onSubmit={handleSubmit(onSubmit)}>
          <BlurContainer onClick={() => setModal(false)} />
          <BaseContainer>
            <ModalTitle>받는 사람</ModalTitle>
            <ModalText>* 최대 10명까지 추가할 수 있어요.</ModalText>
            <ModalText>* 받는 사람의 전화번호를 중복으로 입력할 수 없어요.</ModalText>
            <ModalAddBtn onClick={() => append({ name: "", phone: "", count: 1 })}>추가하기</ModalAddBtn>

            {fields.map((field, index) => (
              <div key={field.id}>
                <div>받는 사람 {index + 1}</div>
                <button type="button" onClick={() => remove(index)}> 삭제 </button>

                <input
                  {...register(`receiverInfo.${index}.name`, {
                    required: "이름을 입력해주세요",
                  })}
                  placeholder="이름을 입력하세요."
                />
                {isSubmitted && errors.receiverInfo?.[index]?.name?.message && <p>{errors.receiverInfo?.[index]?.name?.message}</p>}

                <input
                  {...register(`receiverInfo.${index}.phone`, {
                    required: "전화번호를 입력해주세요",
                    validate: value => isValidPhoneFlexible(value) || "전화번호 형식이 올바르지 않습니다"
                  })}
                  placeholder="전화번호를 입력하세요."
                />
                {errors.receiverInfo?.[index]?.phone?.message && <p>{errors.receiverInfo?.[index]?.phone?.message}</p>}

                <input
                  type="number"
                  {...register(`receiverInfo.${index}.count`, {
                    required: "수량을 선택해주세요",
                    validate: value => value > 0 || "구매 수량은 1개 이상이어야 해요."
                  })}
                />
                {errors.receiverInfo?.[index]?.count?.message && <p>{errors.receiverInfo?.[index]?.count?.message}</p>}
              </div>
            ))}

            <ModalBottomBtn>
              <ModalCancleBtn onClick={() => setModal(false)}>취소</ModalCancleBtn>
              <ModalFinishBtn type="submit">완료</ModalFinishBtn>
            </ModalBottomBtn>
          </BaseContainer>
        </form>
        )

이다. 

추가하기 버튼을 클릭할 때마다 useFieldArray를 이용하여 receiverInfo 배열의 객체를 받아온다.

현재 문제는 다음과 같다.

 

맨 처음 나올 때에는 에러 문구가 안뜨는데, 추가하기 버튼을 누르면 에러 메시지가 떠진다.

 

forState의 isSubmitted를 이용하여 완료 버튼을 눌렀을 때에만 에러 메시지가 뜨게 시도해보았는데 실패했다.

추가하기 버튼을 눌렀을 때도 form이 작동하는 것으로 보아 추가하기 버튼을 form 버튼 밖으로 빼면 어떨까 싶어서 빼보았다.

{modal && (
  <>
    <BlurContainer onClick={() => setModal(false)} />
    <BaseContainer>
      <ModalTitle>받는 사람</ModalTitle>
      <ModalText>* 최대 10명까지 추가할 수 있어요.</ModalText>
      <ModalText>* 받는 사람의 전화번호를 중복으로 입력할 수 없어요.</ModalText>
      <ModalAddBtn onClick={handleAdd}>추가하기</ModalAddBtn>

      <form onSubmit={handleSubmit(onSubmit)}>
        {fields.map((field, index) => (
          <div key={field.id}>
            <div>받는 사람 {index + 1}</div>
            <button type="button" onClick={() => remove(index)}> 삭제</button>

            <input
              {...register(`receiverInfo.${index}.name`, {
                required: '이름을 입력해주세요',
              })}
              placeholder="이름을 입력하세요."
            />
            {touchedFields.receiverInfo?.[index]?.name && errors.receiverInfo?.[index]?.name?.message &&
              <p>{errors.receiverInfo?.[index]?.name?.message}</p>}

            <input
              {...register(`receiverInfo.${index}.phone`, {
                required: '전화번호를 입력해주세요',
                validate: value => isValidPhoneFlexible(value) || '전화번호 형식이 올바르지 않습니다',
              })}
              placeholder="전화번호를 입력하세요."
            />
            {errors.receiverInfo?.[index]?.phone?.message &&
              <p>{errors.receiverInfo?.[index]?.phone?.message}</p>}

            <input
              type="number"
              {...register(`receiverInfo.${index}.count`, {
                required: '수량을 선택해주세요',
                validate: value => value > 0 || '구매 수량은 1개 이상이어야 해요.',
              })}
            />
            {errors.receiverInfo?.[index]?.count?.message &&
              <p>{errors.receiverInfo?.[index]?.count?.message}</p>}
          </div>
        ))}

        <ModalBottomBtn>
          <ModalCancleBtn onClick={() => setModal(false)}>취소</ModalCancleBtn>
          <ModalFinishBtn type="submit">완료</ModalFinishBtn>
        </ModalBottomBtn>
      </form>
    </BaseContainer>
  </>
)}

그러니 잘 작동하였다. 


전화번호는 실시간으로 변화를 확인해야 한다.

형식이 옳은지를 보기 위해서 mode: 'onChange"를 이용했다.

const { register, control, handleSubmit, formState: { errors } } = useForm({
  defaultValues: {
    receiverInfo: [{ name: "", phone: "", count: 1}],
  },
  mode: 'onChange',
});

이러면 실시간으로 유효성 검사를 할 수 있다.

<div>
  <span>전화번호</span>
  <input
    {...register(`receiverInfo.${index}.phone`, {
      required: '전화번호를 입력해주세요',
      validate: value => isValidPhoneFlexible(value) || '전화번호 형식이 올바르지 않습니다',
    })}
    placeholder="전화번호를 입력하세요."
  />
  {errors.receiverInfo?.[index]?.phone?.message &&
    <p>{errors.receiverInfo[index].phone.message}</p>}
</div>

'카테캠 > 2단계' 카테고리의 다른 글

[7/14]zod 라이브러리  (0) 2025.07.14
[7/10] useFormContext  (1) 2025.07.14
[7/10] 고차 함수  (0) 2025.07.10
[7/9] 중복 전화번호 검사, 모달 취소 시 되돌리기  (1) 2025.07.09
[7/7] hooks와 validate로 나누기  (0) 2025.07.07