avatar
morethan-log
react-hook-form의 useWatch와 watch의 차이점
react-hook-formTroubleshooting
May 17
·
5 min read

until-47

이 글은 기본적인 react-hook-form의 사용방법을 설명하고 있지 않습니다.

리액트에서 폼을 다룰 때 빼놓을 수 없는 react-hook-form. (최근에 확인해보니 토스가 스폰서더라..)

react-hook-form을 사용하면서 watch를 통해 values를 가져와 계산하는 컴포넌트를 만들었는데, useForm을 사용하는 컴포넌트 전체가 리렌더링 되면서 퍼포먼스가 구려지는 이슈가 있어서 원인을 찾아보았다.

watch API

watch란 무엇일까? 공식문서를 확인해보자.

This method will watch specified inputs and return their values. It is useful to render input value and for determining what to render by condition.

아래와 같이 사용할 수 있다.

// ...
const Summary: FC = () => {
  const { watch } = useFormContext<Values>();

  const value = watch();

  if (values.type === 'a') return <Result {...getAResultProps(values)} />;
  if (values.type === 'b') return <Result {...getBResultProps(values)} />;
  return <Result 판매금액={0} 최종손익={0} />;
};

export default Summary;

동작은 하지만 Summary컴포넌트 상위의 Root 컴포넌트까지 전부 리렌더가 발생한다. 음... 내가 생각한 의도에 맞는 것 같은데... 좀만 더 읽어보자.

  • This API will trigger re-render at the root of your app or form, consider using a callback or the useWatch api if you are experiencing performance issues.

  • watch result is optimised for render phase instead of useEffect's deps, to detect value update you may want to use an external custom hook for value comparison.

헉... watch를 쓰면 root부터 리렌더가 발생한다고한다. 실제 사용하는 경우에는 컴포넌트의 리렌더가 발생할 수 있으니 useEffect의 deps로써 활용할 수 있도록 최적화 되어있고, 실제 예시와 같은 사용방식에서는 적합하지 않은 것 같다.

그럼 문서에 나와있는 useWatch를 알아보자.

useWatch API

문서를 읽어보면 다음과 같이 설명하고 있다.

Behaves similarly to the watch API, however, this will isolate re-rendering at the custom hook level and potentially result in better performance for your application.

음... 그럼 전체 리렌더링이 발생하는 watch를 사용했을 때 얻는 이점은 뭐지..? 일단 적용해보았다.

// ...

const Summary: FC = () => {
  const values = useWatch<Values>();

  if (values.type === 'a') return <Result {...getAResultProps(values)} />;
  if (values.type === 'b') return <Result {...getBResultProps(values)} />;
  return <Result 판매금액={0} 최종손익={0} />;
};
export default Summary; 

// ...

Root에 존재하는 form은 리렌더하지 않고 Summary rootLevel부터 리렌더되는 이슈는 해결되는 것을 확인할 수 있었다. 👍

useWatch의 deep partial type 이슈

한가지 문제가 더 있었다. useWatch를 사용한 경우 (정확히는 parmeterless한 useWatch를 사용한 경우) watch와 다르게 deepPartial이 적용된 타입이 적용된다는 것이다.

이 문제는 해당 논의이슈에서도 확인해보면 알 수 있다.

결론적으로 watch와 다르게 useWatch 의 타입이 정의되어있는 것은 v8 버전에서 resolve가 된다고 한다.

그럼 지금은 어떻게 대응해야할까?

아래와 같이 하거나... 필요한 필드만 뽑아서 사용해야할 것 같다.

const values = useWatch() as Values

결론

watchuseWatch의 네이밍만보고 동작하는 방식이 같을 것이라고 생각하고 로직안에 오류가 있다고 생각해서 해결하기까지 생각보다 시간을 썼던 것 같다. 문서를 자주 봐야겠다.


- 컬렉션 아티클







몰댄민의 기술블로그