😉 이메일 인증은 한번만..
사용자일때 이런 경우가 발생하면 굉장히 사소하지만 스트레스를 받았던 기억이 있습니다.
어떤 인증코드를 입력해야할지 감이 안잡히고
맨 마지막에 있는 메일의 인증코드가 대체로 잘 동작하지만
그렇지 않은 경우에는 귀찮은 인증코드 확인을 두번이나 해야하는 게 마음에 들지 않았어요
또한 사용자는 버튼을 친절하게 한번만 클릭해주지 않습니다.
상호작용 결과가 반환될 때 까지 버튼을 열심히 누르는 사용자가 많다고 생각해요
그래서 자연스럽게 이 문제를 해결하기 위해서 여러가지 방법을 고려해보게 되었습니다.
1. 요청을 디바운스한다.
나쁘지 않은 발상이라고 생각했습니다.
확실히 디바운싱을 걸어두면 사용자가 아무리 요청을 많이 보내려고 해도
계속 요청을 지연시키면서 한번의 요청만 보낼것으로 예상했습니다.
그런데 문제는 요청을 잘 보냈다는 toast 알림도 지연되어서
사용자가 더 많은 클릭을 하게 되는 결과를 낳기 쉬웠다는 것이었습니다.
또한 클릭을 한번만 하는 사용자의 요청도 디바운싱되어
디바운스로 인한 유예가 끝날때까지 기다린 뒤 요청이 진행되는 비효율이 있었습니다
이 방법을 사용하기 위해서는 toast 알림 지연과 실제 요청을 분리하고
실제요청에만 디바운싱을 해주는 방법이 필요했습니다.
2. 리액트쿼리를 좀 더 이용한다.
리액트 쿼리는 쿼리의 요청 상태에 따라 4가지 status를 제공합니다.
바로 useMutation 함수가 반환하는 객체의 프로퍼티 status를 통해
이 값들에 접근할 수 있는데요 idle, loading, error, success 상태가 존재합니다.
initial status 상태인 idle
요청이 진행중이면 loading
요청에 문제가 생기면 error
요청이 성공하면 success
와 같은 식이지요
제가 원하는 기능을 구현하기 위해서는 status가 loading, success일때에는 요청을 보내지 않아야합니다.
여러번의 요청을 보내고 싶지 않은 것이니까요
간단히 생각해서 loading, success 상태일때는 뮤테이션 요청을 보내는 함수를
얼리리턴해주면 될 것 같습니다. 그러나
함수형으로 작성된 리액트 컴포넌트는 그 자체로 함수입니다.
따라서 리액트 컴포넌트 내부에서
const signupMutation = useSignupMutation();
이런 형태로 호출하고 있는 훅은
리렌더링이 될 때마다 재 호출되는 문제를 가집니다.
그 과정에서 기존 상태가 초기화되는 문제가 발생하는데요
mutation 요청이 성공한 뒤에 다른 이유로 컴포넌트가 리렌더링이 되면
상태가 다시 idle로 초기화되는 문제가 있습니다.
const emailRequestMutation = React.useMemo(() => useEmailRequestMutation('signup'), []);
이런식으로 메모이제이션 하면 어떨까요?
아쉽지만 useMemo 내부에서 훅을 호출하는 것은
https://react.dev/warnings/invalid-hook-call-warning
위 링크를 참고해보면
useMemo, useReducer, useEffect 내부에서 hooks을 호출하는 것은
문제가 있다고 알려줍니다.
리액트 쿼리 내부에서 이 문제에 대한 솔루션을 제공해주고 있는지 저는 잘 모르는 상태입니다.
일단 우선은 리액트의 기본 훅을 이용해 해결할 방법을 생각해봅니다.
뮤테이션객체 내부의 상태는 렌더링에 영향을 줄 필요는 없으니
useState에 값을 저장하는 것보다는 useRef에 저장하는 것이 타당할듯합니다.
const requestRef = useRef<boolean>(false);
request가 성공한적이 있는지 체크하는 requestRef 객체를 만들어주겠습니다.
const emailCredentialRequest = () => {
if (signupForm.formState.errors.email?.message !== undefined) return;
if (signupForm.getValues('email') === '') return;
if (emailRequestMutation.status === 'loading') return;
if (emailRequestMutation.status === 'success') return;
if (requestRef.current) return;
requestRef.current = true;
emailRequestMutation
.mutateAsync(signupForm.getValues('email'))
.then((res) => {
setAuthCodeState({ disabled: false, message: '' });
})
.catch(() => {
setAuthCodeState({ disabled: true, message: '잠시 후 다시 시도해주세요' });
requestRef.current = false;
});
};
이제 버튼의 조건을 추가해주겠습니다.
만약 얼리리턴되지 않는다면 current의 상태를 미리 true로 만들어줍니다.
그 이후 요청을 보내보고 요청이 실패한 경우에만 false로 바꿔줍니다.
만약 로직이 잘 완성된다면
if (emailRequestMutation.status === 'loading') return;
if (emailRequestMutation.status === 'success') return;
이 코드를 없애주어도 문제가 없을 것입니다.
잘 동작하니 이제 다음 문제를 고민합니다.
같은 이메일에 대한 요청이라면 요청을 막아주는게 옳지만
만약 사용자가 이메일을 잘못 입력했거나
인증 요청 버튼을 잘못 눌렀을 가능성이 존재합니다.
그러한 경우에는 다시 인증 요청을 보낼 수 있어야 할 것입니다.
이를 위해 requestRef의 상태를 false로 변경시키는 onChange 이벤트를 만들어주겠습니다.
const resetMutateEmailRequestStatus = () => {
requestRef.current = false;
}
<Input
placeholder="이메일을 입력해 주세요."
className=" flex-grow"
{...signupForm.register('email', {
onChange: resetMutateEmailRequestStatus,
})}
/>
react-hook-form을 사용하고 있는 경우라면
register 함수의 두번째 인자로 옵션객체를 전달할 수 있습니다.
이 옵션객체의 onChange 프로퍼티에 함수를 할당해주면
change 이벤트가 발생할 때 함수가 실행됩니다.
이제 이메일 인풋이 바뀐 경우 다시 인증을 요청할 수 있습니다.
😐마치며
편안한 UX를 위해 고려해주어야 하는 지점들이 많은 것 같습니다.
이런식으로 하나 둘 옵션을 넣어주다보면 코드 복잡도가 올라가는게 체감되는데
로직에 따라 추가되는 코드들도 같이 깔끔하게 관리할 방법이 고민되는 것 같습니다.
'프로젝트 진행기' 카테고리의 다른 글
[연픽] interface 보강 기법을 통한 window 객체 확장 (0) | 2023.09.25 |
---|---|
[연픽] 성공적인 프론트엔드 리팩토링을 위한 사전 준비 (3) | 2023.09.24 |
[PliP] 로그인의 restful 한 설계와 토큰 관리 전략 (1) | 2023.07.19 |
너무 복잡도가 높은 컴포넌트는 어떡하면 좋을까?(답변) (0) | 2023.07.07 |
너무 복잡도가 높은 컴포넌트는 어떡하면 좋을까? (1) | 2023.07.05 |