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에 조금은 더 익숙해진 것 같다!
'개발 공부 > React' 카테고리의 다른 글
무한스크롤 스크롤 위치 기억하기 (0) | 2023.07.10 |
---|---|
color-thief 라이브러리 type 지정하기 (0) | 2023.07.02 |
[프로젝트 고민]useMutation에서 async/await 적용하기 (6) | 2023.03.30 |
input type='file' 동일 파일 재 선택 시 오류 (0) | 2023.03.22 |
리액트 429 에러 해결(useEffect 무한루프) (1) | 2022.10.05 |