useFieldArray와 UI 렌더링 오류 개선기

gydotb·2일 전
2

troubleshooting

목록 보기
2/2
post-thumbnail

☹️ 이슈 설명

React Hook Form을 이용해 기능을 개발하던 중, 약 3개의 Fields를 관리하는 과정에서 useFieldArray를 이용하는 상황이었다. 기존에는 Form 내에서 appendremove를 진행했다면 이번에는 분리된 컴포넌트(모달 창) 내에서 appendremove를 진행해야했다.

그 과정에서 아래와 같이 설계한 코드가 모달 창에서 필드 내 새로운 아이템 추가 시에도 렌더가 되지 않는 문제가 생겼고, 어떻게든 해결은 했으나 그 이유가 궁금해 React Hook Form 코드를 더 자세히 살펴보고 더 나은 해결 방법이 있는지 알아보고자 해당 글을 작성하게 되었다.

설계

  • useFormContext를 이용해 props drilling 해소
  • 모달과 내부 Form 각각의 컴포넌트에서 useFieldArray 호출, 함수 수행
  • 서버 데이터의 경우 reset을 통해 해결

코드 예시

간단하게 App.tsx - Form.tsx - Modal.tsx 세 가지의 예제를 준비하였다.

// App.tsx
export default function App() {
  const methods = useForm({
    defaultValues: {
      name: [],
    },
  });
  return (
    <FormProvider {...methods}>
      <Form />
    </FormProvider>
  );
}
// styled-components 코드 생략
// Form.tsx
export default function Form() {
  const { control, register, getValues } = useFormContext();
  const { fields } = useFieldArray({
    control,
    name: "name",
  });

  const [isOpen, setIsOpen] = useState(false); //modal

  return (
    <ColumnList>
      {isOpen && <Modal setIsOpen={setIsOpen} />}
      <form>
        {fields.map((v) => (
          <input {...register(`name.${v.id}`)} />
        ))}
      </form>
      <button onClick={() => setIsOpen(true)}>추가 모달 띄우기</button>
    </ColumnList>
  );
}
// styled-components 코드 생략
export default function Modal ({
  setIsOpen,
}: {
  setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
}) {
  const { control } = useFormContext();
  const { append } = useFieldArray({
    control,
    name: "name",
  });

  const handleAppend = () => {
    append("new name");
    setIsOpen(false);
  };
 
  return (
    <Wrapper>
      <Content>
        <button onClick={handleAppend}>추가하기</button>
        <button onClick={() => setIsOpen(false)}>모달 끄기</button>
      </Content>
    </Wrapper>
  );
};
// styled-components 코드 생략


🔎 이슈 파악

먼저 정확히 어떤 부분에서 이슈가 발생했는지 파악하기 위해 여러가지 콘솔 테스트를 해본 결과, 문제가 발생한 부분은 useFieldArray 부분으로, getValues()나 watch()를 이용해 검사 시에는 추가한 값이 들어오는 것을 확인할 수 있었다.

코드 샌드박스에서 코드 보기

즉, append 과정에서 useFieldArray hook을 통해 선언한 fields에 모달에서 추가된 값이 반영이 되지 않은 것이다.

{fields.map((v) => (
  <input {...register(`name.${v.id}`)} />
))}

이 부분에서 fields를 이용해 map을 돌린 결과가 보여지게 된다. 결국 이 코드에서는 fields 상태가 변경되어야 UI도 변경되는 셈!

💥 React Hook Form 동작 원리

그래서 useFieldArray의 state 관리 방법과, getValues, watch와 어떻게 다른지 알아보기 위해 공식 코드를 뒤져보았다.

useFieldArray가 동작하는 방법

공식 repository 내의 useFieldArray.ts 파일 내에서 fields state 관리 방법은 아래와 같다.

const [fields, setFields] = React.useState(control._getFieldArray(name));

useFormContext의 control에서 field name에 해당하는 value를 가져와 선언하게 된다.

사실 여기서부터 불길함을 느꼈다… 설마?

Append method

const append = (value, options) => {
  const appendValue = convertToArrayPayload(cloneObject(value));
  const updatedFieldArrayValues = appendAt(control._getFieldArray(name), appendValue);
  ids.current = appendAt(ids.current, appendValue.map(generateId));
  updateValues(updatedFieldArrayValues);
  setFields(updatedFieldArrayValues);
  control._setFieldArray(name, updatedFieldArrayValues, appendAt, {
    argA: fillEmptyArray(value),
  });
};

실제 append method 이용 시에도 setFields를 이용해 fields를 변경하는 것을 볼 수 있다.

😱 그러니까, 절대 useFieldArray를 중복 선언하지 마…

결론부터 이야기하자면, useFieldArray중복 선언해서 사용한 것이 문제였다. 실제로 선언을 App.tsx(부모 컴포넌트)에서 진행하고, props로 append methods를 넘겨준 경우에는 멀쩡히 작동한다.

(야매로 만든 플로우 죄송합니다)

결국 위처럼, useForm 내부에서는 업데이트가 발생하게 되지만 useFieldArray를 별도로 선언해서 이용하는 Form.tsx 내부에서는 이미 state가 선언되어서 별도로 초기값을 initial value로 지정, 동작하고 있기 때문에 업데이트가 발생하지 않은 것이다.

⚡️ 어떻게 해결했냐면…

방법 1 : props를 통해 append method 넘겨주기

props를 통해 method나 field를 넘겨주는게 가장 단순한 방식이지만, 실제 코드는 props drilling이 너무 심하게 일어나기도 했고 type 선언부가 상당히 길고 직관적이지 않았기 때문에 해당 방법은 사용하지 않았다.

방법 2 : useFieldArray 한 곳에서만 선언하기(w. setValues(), getValues())

appendremove가 modal 내에서만 수행될 경우, 해당 메소드를 이용하는 부분에서만 useFieldArray를 선언하고 다른 부분에서는 getValues()를 이용해 key값으로 쉽게 렌더링이 가능하다. 물론 이 방법도 사용할 수 없었다…

그래서useFieldArray method를 많이 사용하는 곳(Form.tsx)에서는 useFieldArray를 선언해 이용하였다. 그리고 Modal.tsx의 경우 일부 메소드만 한정적으로 이용하였기 때문에 useFieldArray 내의 method를 이용하는 것이 아닌, useForm 자체의 setValues를 사용하여 아예 Form 내부의 요소를 업데이트시켰다.

결론

라이브러리를 이용하는 것까지는 좋은데, 로직이 어려워지고 복잡해질수록 라이브러리를 제대로 이해하지 못하고 사용하는 사실이 뽀록이 난다…😅 이 문제도 한 4시간 붙잡고 있었던 것 같은데, 덕분에 완벽 마스터해서 다시는 헷갈리지 않을 듯

그니까 절대 useFieldArray를 중복해서 선언하고 사용하지 않도록!!!
ㄴ 이 사건을 아는 사람 : 너무 무서워…

profile
프론트엔드 개발이 좋다 왜냐하면 프론트엔드 개발이 좋기 때문이다.

0개의 댓글

관련 채용 정보