optimistic update란?
optimistic, 말 그대로 낙관적이라는 뜻으로 서버의 요청이 처리되기 전에 미리 성공함을 가정하고 UI를 빠르게 업데이트 하는 방식이다.
만약 서버의 요청이 실패하면 롤백을 통해 동작을 철회한다.
! 예를 들면 페이스북 같은 경우 좋아요 버튼을 눌렀을 때 매우 빠르게 동작하는데, 이는 optimistic update를 통해 구현 할 수 있다.
# 적용
마침 하고 있는 프로젝트에서 태그를 추가하는 기능이 있는데, 서버의 응답을 기다렸다 태그를 생성하자니 UI가 약간 느리게 동작하여 답답함을 유발했다.
onMutate 함수를 이용하여 빠른 동작을 할 수 있도록 해보자!
아래는 원래의 코드이다.
// 태그 추가 api
const {mutate: addFavorite} = useAddFavorite({
onMutate: () => onAddSuccess('favorites'),
});
const getId = data => {
if (data.length === 0) return 0;
else return data[data.length - 1].selfIntroductionAnswerId + 1;
};
const onAddSuccess = category => {
queryClient.setQueryData(
['getFamilyInfo', route.params.userId],
oldData => {
let data = oldData.data.data[category];
oldData.data.data[category] = [
...data,
{
selfIntroductionAnswerId: getId(data),
selfIntroductionAnswerContent: tagValue,
},
];
setModiCategory('');
},
);
};
addFavorite이 성공하면 onAddSuccess 함수가 동작한다.
onAddSuccess 함수는 사용자가 방금 추가한 태그를 태그리스트에 추가하여 보여준다.
(위에서는 좋아하는 것을 추가하는 기능밖에 없지만 실제로는 여러 카테고리에 태그를 추가할 수 있기 때문에 성공 후 이루어지는 동작을 함수화 하여 category에 따라 동작하게 했다.)
위와 같이 구현 했을 때 사용자가 태그를 작성한 후 작성한 태그가 보여지기까지 약간의 딜레이가 생겼다.
위의 문제를 해결하기 위해 아래와 같이 onMutate함수를 만들었다.
// 태그 추가 api
const {mutate: addFavorite} = useAddFavorite({
onMutate: () => onAddMutate('favorites'),
onError: (err, value, context) => {
// 실패할 경우 onMutate에서 반환한 값이 context로 들어옴
// 기존 data로 rollback
queryClient.setQueryData(['getFamilyInfo', route.params.userId], context);
alert('태그 생성에 실패했습니다');
},
});
const getId = data => {
if (data.length === 0) return 0;
else return data[data.length - 1].selfIntroductionAnswerId + 1;
};
const onAddMutate = category => {
// 이전에 자동 요청된 데이터 등으로 overwrite 되지 않도록
queryClient.cancelQueries(['getFamilyInfo', route.params.userId]);
// 실패했을 경우 롤백을 해야 하므로 원래의 데이터를 저장한다.
const context = queryClient.getQueryData([
'getFamilyInfo',
route.params.userId,
]);
// 성공 가정하고 UI 미리 적용
queryClient.setQueryData(
['getFamilyInfo', route.params.userId],
oldData => {
return {
...oldData,
[category]: [
...oldData[category],
{
selfIntroductionAnswerId: getId(oldData[category]),
selfIntroductionAnswerContent: tagValue,
},
],
};
},
);
setModiCategory('');
// 실패시 되돌릴 데이터 리턴
return context;
};
onMutate 함수에서는 먼저 queryClient에서 제공하는 cancelQueries를 이용해 이전에 날아갔던 쿼리들을 삭제해준다.
위 작업을 통해 이 전에 요청된 쿼리가 나중에 도착하여 optimistic data를 덮어 쓸 경우를 방지할 수 있다. (cancelQueries를 써야하는 자세한 이유는 글 맨 아래 링크를 걸어놓았다.)
그 다음으로 롤백할 데이터를 저장해 놓은 뒤, 요청이 성공했다는 가정 하에 쿼리 데이터를 업데이트 한다. 마지막으로 만약 실패했을 경우를 위해 롤백할 데이터를 리턴한다.
이렇게 하면 onError에서는 3번째 파라미터를 통해 onMutate에서 리턴한 데이터를 받을 수 있다. 받은 데이터로 다시 쿼리 데이터를 이전 데이터로 업데이트 해주면 된다.
(여기서 onError의 3번째 파라미터인 context를 2번째에 놓는 바람에 에러가 났다ㅜㅜ 이 사소한 오류를 찾아내느라 몇시간이 걸렸다... 파라미터의 순서를 맘대로 바꾸지 말자.. 안쓰는 파라미터도 순서를 맞추기 위해 꼭 써주자..)
# 후기
이제 optimistic update 적용이 끝났다! 테스트 해 본 결과 성공인 경우 이전보다 매우 빠르게 태그가 생성되었고, 실패인 경우에는 아주 잠깐 생겼던 태그가 다시 사라지는 효과가 나타났다. 이상한데서 고생하느라 벌써 새벽 4시지만 결국 성공했으니까 만족!
# 참고
cancelQueries를 써야하는 이유 => react-query optimistic update시 데이터 꼬임 방지 (velog.io)
onMutate 적용 예시 => Optimistic Updates | TanStack Query Docs
'react-native' 카테고리의 다른 글
[react-native/recoil] atom을 서버와 연동하기 (0) | 2023.01.06 |
---|---|
[react-native] 로딩 바 만들기 (0) | 2023.01.02 |
[react-native] recoil 데이터 유지하기 (async storage) (0) | 2022.12.28 |
[react-native & Spring Boot] react-native-image-picker 사용시 null로 보내지는 현상 (0) | 2022.11.13 |
[react-native] formData보낼 때 boundary자동으로 등록 안되는 경우 (1) | 2022.11.13 |