개발 공부/React

[프로젝트 고민]input 불필요한 렌더링 줄이기

YJzero 2023. 4. 15. 21:16

wumo 프로젝트를 진행하면서 고민했던 부분을 기록하고자 한다.


wumo 서비스(06.23 현재 서버 종료): https://5yes-wumo.vercel.app/
wumo 깃헙: https://github.com/prgrms-web-devcourse/Team-5YES-WuMo-FE
wumo 회고: https://yj-zero.tistory.com/176

 

react hook form에 가격 formetter 적용하기

사용금액 입력하는 부분에서 입력 즉시 포맷터를 적용해야 했다.

 

기존에 진행했던 방법은 react-hook-form고 별개로 state로 input을 관리하였다. 포맷터를 별도의 라이브러리가 아니라 자바스크립트에 내장되어 있는 Intl을 사용하였다. 따라서 포맷터를 적용한 입력값을 input의 value로 설정하고 onChange 함수에서 다시 ','를 제거한 후 setState해주는 과정이 필요했다.

 

[기존] State를 사용하는 경우

const PlaceAmountField = ({ spending }: { spending: number }) => {
  const [numberValue, setNumberValue] = useState(String(formatPrice(spending)));
  const { handleSubmit } = useForm<AmountType>();

  const onSubmitAmount = ({ amount }: AmountType) => {
    amount = Number(numberValue.replaceAll(',', ''));
    const amountBody: ChangeAmountType = {
      locationId: state.locationId,
      spending: Number(amount),
    };
    changeAmount(amountBody);
    Toast.show({
      message: `${numberValue}원으로 변경되었습니다.`,
      type: 'success',
    });
  };

  return (
    <Container as='form' onSubmit={handleSubmit(onSubmitAmount)} p='0.5rem 0'>
     ...
          <Input
            type='text'
            value={numberValue}
            borderRadius='0.9375rem'
            placeholder='사용 금액을 입력하세요'
            onChange={(e) => {
              const prev = e.target.value.replaceAll(',', '');
              setNumberValue(formatPrice(Number(prev)));
            }}
          />
          <InputRightElement>원</InputRightElement>
	...
      </Flex>
    </Container>
  );
};

 

state 사용 시 문제점

하지만 이 경우 input 값이 변경될 때마다 전체 컴포넌트가 렌더링된다는 문제가 있다. 지금은 현재 컴포넌트에 많은 내용이 없어서 크게 문제 되지 않겠지만 렌더링 될 내용이 많아진다거나 하위 컴포넌트가 많다면 문제가 될 수 있다. 따라서 전체 컴포넌트에서 input을 분리할 필요가 있었다.

 

해결방법1. Controller 사용

그래서 서버와 통신할 수 있도록 값 자체에는 포맷터를 적용하지 않으면서 input 입력값에는 포맷터를 적용해야 하므로 input만 리렌더링 해야한다는 것이 계획이었다. 어떻게 할지 고민을 하다가 때마침 이전 기수분의 도움을 받아 react-hook-form의 Controller를 활용하여 input만 렌더링 되도록 할 수 있었다.

 

[수정] Controller 사용

const PlaceAmountField = ({ spending }: { spending: number }) => {
  const { mutate: changeAmount } = useMutation(patchChangeAmount, {
    onSuccess: () => {
      Toast.show({
        message: `변경이 완료되었어요.`,
        type: 'success',
      });
    },
    onError: () => {
      Toast.show({
        message: `사용금액 변경에 실패했어요! 다시 시도해주세요.`,
        type: 'error',
      });
    },
  });

  const { handleSubmit, control } = useForm<AmountType>({
    defaultValues: {
      amount: spending,
    },
  });

  const onSubmitAmount = ({ amount }: AmountType) => {
    const amountBody: ChangeAmountType = {
      locationId: state.locationId,
      spending: Number(amount),
    };
    changeAmount(amountBody);
  };

  return (
    <Container as='form' onSubmit={handleSubmit(onSubmitAmount)} p='0.5rem 0'>
    ...
        <Controller
          control={control}
          name='amount'
          render={({ field: { onChange, value } }) => {
            return (
             ...
                <Input
                  type='text'
                  value={formatPrice(Number(value))}
                  borderRadius='0.9375rem'
                  placeholder='사용 금액을 입력하세요'
                  onChange={({ target }) => {
                    const value = target.value.replace(/[^0-9]/g, '');
                    onChange(value);
                  }}
                />
                <InputRightElement>원</InputRightElement>
              ...
            );
          }}
        />
   ...
    </Container>
  );
};

 

Controller를 사용하면서 input 폼만 리렌더링되고 전체 컴포넌트는 리렌더링 되지 않는다. 또한 기존에는 api 호출을 할 경우 무조건 성공 Toast UI를 띄웠는데 이젠 onSucess와 onError로 에러핸들링을 할 수 있도록 추가하였다.

 

 

state 사용할 때 리렌더링 하이라이팅

 

Controller 사용할 때 리렌더링

 

입력할 때 하이라이팅 되는 부분이 리렌더링 되는 부분인데 리렌더링 되는 컴포넌트의 범위가 다른 것을 볼 수 있다.

 

 

해결방법2. register 사용하기

Controller 아닌 register를 적용하여 input의 리렌더링도 없앨 수 있을 것 같다는 팀원의 의견을 반영하여 Controller가 아닌 register를 적용해보기로 했다.

 

첫 번째 시도

const PlaceAmountField = ({ spending }: { spending: number }) => {
 ...
  const onSubmitAmount = ({ amount }: AmountType) => {
    const amountBody: ChangeAmountType = {
      locationId: state.locationId,
      spending: Number(amount),
    };
    changeAmount(amountBody);
  };

  return (
    <Container as='form' onSubmit={handleSubmit(onSubmitAmount)} p='0.5rem 0'>
     ...
          <Input
            type='text'
            value={formatPrice(Number(watch('amount')))}
            borderRadius='0.9375rem'
            placeholder='사용 금액을 입력하세요'
            {...register('amount', {
              onChange: ({ target }) => {
                const value = target.value.replace(/[^0-9]/g, '');
                setValue('amount', value);
              },
            })}
          />
     ...
    </Container>
  );
};

 

값을 실시간으로 포맷터하기 위해 watch를 사용하여 value로 걸어주었다. 하지만 watch로 input 값을 감시하게 되면 이러면 state를 쓰는 것과 같아지기 때문에 watch를 안쓰는 방법을 찾아야했다. 고민을 하다가 팀원분의 조언으로 해결하였는데 그 방법은 아예 value를 사용하지 않고 onChange에서 사용하던 setValue에서 바로 포맷터를 적용하는 것이다. setValue로 바로 적용하는 방법이 있었다니 생각도 못했다! 이래서 공식문서랑 예시를 잘 알아둬야 하나보다! 도움을 준 팀원에게 감사를...👍

 

 

✅사용한 방법

  return (
   ...
          <Input
            type='text'
            borderRadius='0.9375rem'
            placeholder='사용 금액을 입력하세요'
            {...register('amount', {
              onChange: ({ target }) => {
                const value = target.value.replace(/[^0-9]/g, '');
                setValue('amount', formatPrice(Number(value)));
              },
            })}
          />
          <InputRightElement>원</InputRightElement>
   ...
  );

 

이렇게 register로 관리하면서 비제어 컴포넌트로 input을 관리하고 포맷터도 적용할 수 있게 되었다! 팀원의 도움으로 register로 연결할 수 있어서 다행인 것 같다.

 

register로 관리할 때 리렌더링

 

위의 gif에서 볼 수 있듯이 위의 두 방법과 달리 입력을 해도 리렌더링이 이루어지지 않아 하이라이팅되지 않는 모습을 볼 수 있었다. Controller로 input만 리렌더링 시키는 게 최선일 줄 알았는데 이렇게 비제어로 포맷터와 함께 관리가 될 수 있다니! 


react-hook-form에서 Controller를 써본 건 처음인데 이렇게 사용할 수 있구나 하고 배웠다.  또 다시 register로 적용해보면서 역시 다양한 방법이 있구나라고 느꼈다. 공식문서를 더 열심히 봐야지! 주변사람들의 도움으로 react-hook-form에 조금은 더 익숙해진 것 같다!