라이브러리/React Hook Form

[React Hook Form] useFieldArray에 폼 상태 연결이 안될 때

눙엉 2024. 2. 27. 23:21

React Hook Form을 통해 동적으로 변하는 폼 상태를 관리하기 위해 useFieldArray를 사용하고 있었습니다. 문제없이 폼 상태를 동적으로 잘 관리한다고 생각했는데 막상 제대로 동작하지 않는 이슈를 발견했습니다. 해당 이슈를 기반으로 간단한 예시를 만들어서 설명하겠습니다.

 

요구 사항

1. 회원의 이름과 나이를 작성할 수 있는 폼

2. 추가 버튼 클릭 시 회원을 여러 명 등록하도록 input 요소 추가

3. 더미로 변환 버튼 클릭 시 해당 요소의 이름은 dummy로 변경, 나이는 입력 값 그대로 유지

 

예상 구현 방법

1. 회원의 이름과 나이를 작성할 수 있는 폼  -> input, button 요소와 React Hook Form 연동

2. 추가 버튼 클릭 시 회원을 여러 명 등록하도록 input 요소 추가 -> useFieldArray 사용

3. 더미로 변환 버튼 클릭 시 해당 요소의 이름은 dummy로 변경, 나이는 입력 값 그대로 유지 -> useFieldArray의 update 메서드 사용

 

예상 구현 방법을 기반으로 작성한 코드 먼저 확인하겠습니다.

interface Form {
  person: { name: string; age: string }[];
}

function App() {
  const { control, register } = useForm<Form>({
    defaultValues: {
      person: [{ name: "", age: "" }],
    },
  });
  const { fields, append, update } = useFieldArray({
    control,
    name: "person",
  });

  const handleAddPerson = (): void => {
    append({ name: "", age: "" }, { shouldFocus: false });
  };

  const handleChangeDummy =
    (field: FieldArrayWithId<Form, "person", "id">, index: number) =>
    (): void => {
      update(index, { name: "dummy", age: field.age });
    };

  return (
    <>
      <button type="button" onClick={handleAddPerson}>
        추가
      </button>
      {fields.map((field, i) => (
        <>
          <input key={field.id + 1} {...register(`person.${i}.name`)} />
          <input key={field.id + 2} {...register(`person.${i}.age`)} />
          <button type="button" onClick={handleChangeDummy(field, i)}>
            더미로 변환
          </button>
        </>
      ))}
    </>
  );
}

1. 회원의 이름과 나이를 작성 할 수 있는 폼 -> 성공

2. 추가 버튼 클릭 시 회원을 여러명 등록하도록 input 요소 추가 -> 성공

3. 더미로 변환 버튼 클릭 시 해당 요소의 이름은 dummy로 변경, 나이는 입력 값 그대로 유지 -> 실패

 

 

 

예시 코드에서 더미로 변환 버튼을 클릭하게 되면 해당하는 name은 dummy로 변하게 되고 age는 입력해 둔 값을 그대로 유지하도록 동작하기를 예상하고 작성하였습니다. 하지만 막상 버튼을 클릭하게 되면 name은 dummy로 변하게 되지만 age는 빈 문자열로 변경이 되고 있습니다.

 

이슈를 해결한 코드를 확인하고 이어서 설명하겠습니다.

interface Form {
  person: { name: string; age: string }[];
}

function App() {
  const { control, register, watch } = useForm<Form>({
    defaultValues: {
      person: [{ name: "", age: "" }],
    },
  });
  const { fields, append, update } = useFieldArray({
    control,
    name: "person",
  });
  const controlledFields = fields.map((field, index) => {
    return {
      ...field,
      ...watch("person")[index],
    };
  });

  const handleAddPerson = (): void => {
    append({ name: "", age: "" }, { shouldFocus: false });
  };

  const handleChangeDummy =
    (field: FieldArrayWithId<Form, "person", "id">, index: number) =>
    (): void => {
      update(index, { name: "dummy", age: field.age });
    };

  return (
    <>
      <button type="button" onClick={handleAddPerson}>
        추가
      </button>
      {controlledFields.map((field, i) => (
        <>
          <input key={field.id + 1} {...register(`person.${i}.name`)} />
          <input key={field.id + 2} {...register(`person.${i}.age`)} />
          <button type="button" onClick={handleChangeDummy(field, i)}>
            더미로 변환
          </button>
        </>
      ))}
    </>
  );
}

useForm의 watch를 사용해 useFieldArray의 fields와 현재 상태 값을 연동시켜 주니 원하는 대로 동작했습니다.

 

원인 분석

첫번째, 예상 구현 코드에서 useFieldArray의 fields를 사용해 map 메서드를 사용하고 있었습니다. fields가 어떤 타입으로 이루어져 있는지 알기 위해 React Hook Form의 useFieldArray 공식문서를 확인해보겠습니다.

 

fields란 이 객체에는 컴포넌트에 대한 기본값과 키가 포함되어 있다고 설명되어 있습니다.

 

설명을 읽은 후, 두 번째 코드의 handleChangeDummy 함수를 사용하는 부분을 살펴보면 첫 번째 인자로 field를 전달하고 있습니다. 여기서 field는 공식 문서에 나온 것처럼 기본값을 나타내고 있습니다. 그러므로 내가 원하는 입력값이 아닌 빈 문자열을 전달하고 있었기 때문에 원하는 대로 동작하지 않았습니다.

 

두 번째로, controlledFields를 만들어서 fields와 교체해 주었습니다. controlledFields의 코드를 확인하면 field의 map 메서드를 사용하여 useForm의 watch와 결합해 상태를 연동시켜주고 있습니다. watch는 register로 작성한 값을 실시간으로 확인할 수 있는 메서드이며, field는 기본 값을 가지고 있다면 해당 값에 watch 메서드의 실시간 값으로 덮어씌워주고 있습니다. 따라서 handleChange의 field는 더 이상 기본값이 아닌 입력한 값을 가지고 있습니다.

 

 


이 방법을 통해 React Hook Form을 효과적으로 동적으로 관리하고, 런타임 중 발생하는 이슈를 위에서 설명한 방법으로 적절히 해결할 수 있습니다. 만약 설명이 부족하거나 수정이 필요한 부분이 있다면 댓글로 알려주시면 감사하겠습니다 :)