본문 바로가기
개발개발/WorkRoot_워크루트

리액트 훅 폼 (React Hook Form) 트러블 슈팅 1 - 여러 탭에 걸친 폼 데이터 제출하기(watch, setValue 사용하기, getValues)

by yelimu 2025. 1. 3.

 

 

 

012
세 개 탭에 걸쳐있는 폼 작성 페이지

 

위 슬라이드는 사용자가 알바 공고를 작성하기 위한 페이지이다.

1. 모집 내용 2.모집 조건 3. 근무 조건 총 세가지 탭을 작성해서 전체 폼 데이터를 제출해야 한다. 

 

🤔

간단한 게시글이나 댓글 정도는 구현해본 적이 있는데 사용자 입력을 받아서 폼을 제출하는 로직을 처음 구현해보았다.

게다가 캡쳐한 이미지를 보면 각각의 필드에서 받는 데이터의 유형도 매우 다양하다는걸 알수있다^^

리액트훅폼을 제대로 안써봐서 해당 페이지를 맡으면서 제대로 써볼 기회인것 같아 잘됐다고 생각했다


🙄 페이지 구조

우선 페이지 구조는 아래와 같이 구성했다. 

- 좌측에 보이는 TabMenu + 임시저장/제출 버튼

- 우측에 보이는 Form section 

 

TabMenu 에서 선택한 값에 따라 router.push(/add?tab=query) 로 url 경로를 이동시키고 

query값에 따라 매칭되는 Form section 이 렌더되는 구조이다.

이제보니 query랑 param을 헷갈렸네,, 

//리액트 훅폼 관련 코드
const methods = useForm<SubmitFormDataType>({
    mode: "onChange",
    defaultValues: {초기값}

  const {
    setValue,
    handleSubmit,
    watch,
    formState: { isDirty, isValid },
  } = methods;

// 훅폼에서 관리하는 전체 데이터를 가져오는 함수
const currentValues: SubmitFormDataType = watch();

// 탭 클릭 시 option을 받아 url로 보내주는 함수 
const handleOptionChange = (option: string) => { 
    if (option !== currentParam && isDirty) {
      onTempSave();
    }
    const params = {
      "모집 내용": "recruit-content",
      "모집 조건": "recruit-condition",
      "근무 조건": "work-condition",
    }[option];
    router.replace(`/addform?tab=${params}`);
  };
  
  // query에 따라 매칭되는 JSX 반환하는 함수
  const renderChildren = () => { 
    switch (currentParam) {
      case "recruit-content":
        return <RecruitContentSection key="recruitContent" />;
      case "recruit-condition":
        return <RecruitConditionSection key="recruitCondition" />;
      case "work-condition":
        return <WorkConditionSection key="workCondition" />;
      default:
        return <RecruitContentSection key="recruitContent" />;
    }
  };
  return (
    <FormProvider {...methods}>
        <TabMenuDropdown
        options={[
        {
        label: "모집 내용",
        isEditing: isEditingRecruitContent || currentParam === "recruit-content",
        },
        { label: "모집 조건", isEditing: isEditingRecruitCondition || currentParam === "recruit-condition" },
        { label: "근무 조건", isEditing: isEditingWorkCondition || currentParam === "work-condition" },
        ]}
        onChange={handleOptionChange}
        currentParam={currentParam || ""}
        />
        // ... buttons
        {renderChildren()} // query에 따라 매칭되는 JSX 반환하는 함수
    </FormProvider>
      )

 

1. 리액트훅폼에서 제공하는 hook인 useForm 을 호출

리액트 훅폼에서 관리할 데이터 타입 지정 및 메서드 호출

=> 유효성 검사에 대한 mode, 초기값을 설정한다. useform 공식문서를 보면 이외에도 다른 많은 prop이 있다.

const  methods = useForm();   
 
const  {
    setValue, // 임시저장 데이터가 있을 경우 각 필드마다 데이터를 저장
    handleSubmit, // 제출 핸들러
    watch, // 입력값 실시간 업데이트
    formState: { isDirty, isValid }, // 값 변경 여부, 폼 완성 여부를 검사
  } = methods;

useForm()으로 호출한 methods 객체에서 필요한 메서드를 구조분해로 가져온다.

 

2. 리액트 훅폼에서 제공하는 FormProvider 로 전체 JSX를 감싸주었다. 

FormProvider는 리액트 context API 기반으로 만들어져 컴포넌트 트리에서 prop drilling 없이 데이터를 전달해줄 수 있다.

FormProvider로 감싼 하위 컴포넌트 안에서는 useFormContext를 사용해 method를 호출할 수 있다. 

 

React Hook Form은 비제어 컴포넌트를 기반으로 동작하여 상태 업데이트 시 불필요한 리렌더링을 최소화하도록 설계되었지만, useFormContext를 남용하거나 상태를 과도하게 업데이트하면 성능 이슈가 발생할 수 있다. 성능 최적화를 위해 필요한 부분에만 데이터를 전달하고, context 사용을 최소화하는 것이 중요하다. 고 한다 ㅎㅎ

 

3. 작성이 완료된 데이터는 watch() 함수로 가져와서 그대로 제출해주었다. 

이것도 다사다난.. 했지만 결론은 아주 간단한 코드가 되었다. 대략 이런 식으로,, 

  const currentValues: SubmitFormDataType = watch();
...
    try{
         const response = await axios.post("/api/forms", currentValues);
    }

 

4. 임시저장 데이터 가져오기


  // 임시저장 데이터 로드 함수
  const loadTempData = () => {
    const tempData = localStorage.getItem("tempAddFormData");
    if (tempData) {
      const parsedData: SubmitFormDataType = JSON.parse(tempData);
      // 기본 필드들 설정
      Object.entries(parsedData).forEach(([key, value]) => {
        if (value !== undefined && value !== null) {
          setValue(key as keyof SubmitFormDataType, value);
        }
      });
    }
  };

임시저장은 로컬 스토리지를 활용했다. 

저장된 데이터는 문자열(String) 형식으로 저장되므로, JSON.parse()를 사용해 객체로 변환한다. 

저장 시: 객체를 문자열로 변환 → JSON.stringify(data)

읽을 시: 문자열을 다시 객체로 변환 → JSON.parse(data)

 

* 임시저장 

  window.localStorage.setItem(name, JSON.stringify(data));
 

🧩 하위 컴포넌트

 const {
    register,
    setValue,
    watch,
    control,
    trigger,
    formState: { errors, isDirty },
  } = useFormContext();

하위 컴포넌트인 각 section에서는 useFormContext() 로부터 method를 가져왔다.


처음에는 각 section마다 useForm을 사용해서 개별 폼으로 관리되는 문제가 있었는데 

 상위 컴포넌트는 `<FormProvider>`로 감싸주고 하위 컴포넌트에서 useFormContext 사용함으로써 

하나의 폼데이터로 관리할 수 있었다.


 

이 컴포넌트들은 공고 수정 페이지에서도 같이 사용하기 때문에 

공고 조회 -> 수정 시 조회한 데이터를 watch 로 값을 가져와 필드에 지정해주었다.

전체 데이터를 조회할때는 watch() 를 사용했고, 특정 필드의 값을 가져올때는 ("저장한 이름")을 넣어 불러왔다.

  const workStartDate: string = watch("workStartDate");
 

 

getValues() 와 watch() 는 모두 현재 입력된 폼 값을 즉시 가져오는 메서드이다.

watch만의 특징은 

1. input 입력값을 구독하는가 (값 변경 시 실시간 업데이트)  

2. 리렌더를 유발하는가 (watch는 값의 변경에 따라 리렌더를 유발한다.) 

나는 변경되는 입력값을 실시간으로 가져오기 위해 watch를 사용했다. 

(포스팅 하면서 배우는 리액트훅폼 ^^^ ;.. )

 

getValues는 호출 시점의 값을 즉시 가져오는건 동일하지만, 입력값이 변경될때 자동으로 업데이트된 값을 가져오지 않는다.

따라서 리렌더도 유발하지 않는다 

=> 폼 값의 일괄적인 유효성 검사를 할때 사용 


내용이 길어져 

유형별 input 값 리액트 훅폼에 등록하기 포스팅은 ▼

 

리액트 훅 폼 (React Hook Form) 트러블 슈팅 2 - 여러 유형의 폼 데이터 제출하기

리액트 훅 폼 (React Hook Form) 트러블 슈팅 1 - 여러 탭에 걸친 폼 데이터 제출하기(watch, setValue) 리액트 훅 폼 (React Hook Form) 트러블 슈팅 1 - 여러 탭에 걸친 폼 데이터 제출하기(watch, setValue)위 슬라

memoryelim.tistory.com