리액트 쿼리를 약간 적용하면서 우리가 사용한 useQuery는 api에 get메서드로 보낼때 사용한다. 그래서 enabled옵션을 넣기도 하고 쿼리를 통해 얻은 데이터를 활용하기도 한다. 하지만 모든 api요청을 컴포넌트가 마운트될때 실행된다면 문제가 생긴다.
get이외에도 post,delete,put등은 어떤 동작 이후에 요청되는 경우가 많다. 그래서 항상 실행되는 것이 아니라 특정 시점에 요청되도록 설정해줘야한다. 그래서 쿼리의 기능인 useMutation을 알아보고 적용해보겠다.
mutation의 의미는 변화시키다의 의미를 가지고 있다. 그래서 useMutatioin은 사이드 이펙트가 발생하는 곳에서 사용하면 효과적이다. 그래서 post나 delete와 같은 api 메소드에 사용하는 것이다. 그래서 useMutation을 사용하면 refetch를 통해 데이터를 수정하고 바로 적용하는 등의 기능을 추가로 구현할 수 있다.
하지만 지금은 useMutation과 친해지기 위해 데이터가 변동될 필요가 없는 로그인과 회원가입에 먼저 적용시켜보겠다.
export async function postSignIn(id: string, password: string) {
const { data } = await axios.post('/sign-in', {
email: id,
password: password,
});
localStorage.setItem('token', data.data.accessToken);
return data;
}
우선 로그인 api함수이다. email과 password를 받아서 성공하면 토큰을 저장한다. 그리고 api를 사용하는 코드를 보면
const formAction = async (data: FormValueType) => {
const result = await postSignIn(data.id, data.password);
if (result) {
window.location.href = '/folder';
}
};
이렇게 간단하게 작성되어 있었다. 로그인 폼을 submit하게 되면 폼 데이터를 받아서 요청을 보내고 성공하면 folder페이지로 이동한다. 이제 이걸 바꿔보도록 하겠다.
const { mutate } = useMutation({
mutationFn: (user: { id: string; password: string }) =>
postSignIn(user.id, user.password),
});
우선 useMutation도 객체로 이뤄져 다양한 옵션을 가지고 있다. 그중에 mutate는 리액트 쿼리를 요청하는 함수이다. 나중에 폼 이벤트에 적용시켜 보겠다.
그리고 동작할 함수를 넣어준다. useQuery에서는 queryFn으로 지정했는데 useMutation에서는 mutationFn으로 지정해준다. 그리고 로그인하는데 필요한 데이터를 파라미터로 받아 api요청을 보내면 된다.
이제 등록한 mutate함수를 적용시켜보겠다.
const formAction = async (data: FormValueType) => {
mutate(data);
};
매우 간단하다. 함수에 데이터만 넣어주면 된다. 이제 리액트 쿼리를 이용해 로그인을 구현한 것이다.
기존의 코드에서는 조건문을 활용해 로그인에 성공하면 /folder페이지로 이동했다. 하지만 useMutation에서는 조건문을 사용할 필요없이 옵션을 지정해주면된다.
우선 로그인에 성공했을때이다.
onSuccess: () => (window.location.href = '/folder'),
onSuccess으로 api리퀘스트가 정상적으로 동작했을때 실행할 함수를 등록해주면 된다. 그러면 원래 코드와 같이 조건문 없이 동일한 동작이 가능하다.
실패했을때에도 간단하다.
onError: () => {
setError('id', { type: 'manual', message: '다시 입력해주세요!' });
setError('password', { type: 'manual', message: '다시 입력해주세요!' });
},
onError를 사용해서 에러가 발생했을때 동작할 함수를 지정할 수 있다. 전에는 에러 처리를 어떻게 했는지 보자면
...
} catch (error) {
console.error('Error fetching sign-in:', error);
alert('로그인할 수 없습니다! 아이디와 비밀번호를 확인해주세요!');
}
api에서 try...catch문으로 에러가 발생하면 alert을 보여줬다. 하지만 그 당시에는 임시방편으로 만들어둔 기능이라 리팩토링이 필수적이였다.
우선 전에 로그인 폼에 react-hook-form을 사용해서 에러처리를 했다. 당시에는 rules로 지정한 에러만 사용이 가능한 줄 알았는데 직접 메뉴얼로 에러를 발생시킬수 있다는 것을 알았다.
const { handleSubmit, control, setError } = useForm<FormValueType>();
그래서 useForm을 통해 setError를 추가적으로 받아왔다. 이제 useMutation의 onError에서 설정해주면 된다.
onError: () => {
setError('id', { type: 'manual', message: '아이디를 다시 입력해주세요!' });
setError('password', { type: 'manual', message: '비밀번호를 다시 입력해주세요!' });
},
이렇게 에러를 발생시킬 controller의 이름을 첫번째로 지정하고 두번째로 에러 타입과 메세지를 지정해주면 된다. 로그인에 실패했을때 자세한 상황은 api에서 지원하지 않는다.(비밀번호 오류, 없는 계정 등) 그래서 에러가 발생하면 일괄적으로 인풋에 다시 입력해달라는 에러 문구를 설정해줬다.

이렇게 잘못된 비밀번호를 입력하면 인풋에 에러 메세지가 잘 설정된다.
로그인을 하고 서버에서 리스폰스를 받는 시간이 약간 있어서 그 사이에 로딩 효과를 넣기로 했다.
const { mutate, isPending } = useMutation({
...
useQuery와 동일하게 isPending을 사용할 수 있다.
{isPending && <Loading />}
이제 로그인 되는 것을 사용자도 파악할 수 있다.

로그인 요청도 잘 되고 있고 캡쳐에는 안나왔지만 로딩도 잘 구현됬다.
로그인 동작은 솔직히 큰 어려움이 없었다. 왜냐하면 데이터가 변동되야하는 부분도 없었고 그냥 버튼을 눌렀을때 리액트 쿼리가 동작하도록 만들면 되기 때문이다. 오히려 배운것은 react-hook-form의 사용법을 좀더 익혔달까...
문제는 회원가입 페이지이다. 우선 거기는 중복 이메일을 검사하는 함수도 같이 있다. 근데 내가 공부하면서 useMutation은 useQuery같이 enabled옵션을 사용할 수 없다는 것이다. 그래서 동작 순서를 조금더 잘 설계해야한다.
추가적으로 지금 버튼에 disabled옵션이 안되있어서 지금 인풋에 넣은 값이 제출이 되는건지 안되는건지 사용자가 파악하기 어렵다. 그래서 그 부분도 회원가입하면서 리팩토링을 조금 진행해야겠다.