프로젝트를 진행하면서 팔로우/좋아요 버튼에 낙관적 업데이트를 처음 적용해보았고, 그 과정에서 관련 자료들을 찾아보고, 낙관적 업데이트에 대한 생각들이나 초기 코드를 리팩토링하는 과정을 적어보았다.
const useToggleFollow = ({
initFollowState,
}: {
initFollowState: boolean | null;
}) => {
...
const mutation = useMutation<
void,
Error,
{ userId: number; following: boolean }
>({
mutationFn: async ({ userId, following }) => {
following ? await deleteFollow(userId) : await postFollow(userId);
},
onSuccess: (_, { following }) => {
const currentUserId = TokenHandler.getUserIdFromToken();
if (currentUserId) {
[
ARTWORK.followedArtists,
USER.followerList(currentUserId),
USER.followingList(currentUserId),
].forEach((queryKey) => {
queryClient.invalidateQueries({ queryKey: [queryKey] });
});
}
setIsFollowing(!following); // 성공 시 데이터 변경
},
onError: (error) => {
console.error('팔로우 상태 변경 에러: ', error.message);
},
});
...
return { isFollowing, toggleFollow };
};
export default useToggleFollow;
useMutation의 동작에 따라 onSuccess는 mutateFn이 정상적으로 마쳐야 동작. 따라서 follow 상태를 변경하는 로직은 api 로직이 모두 끝난 다음에야 동작.
→ UI 갱신이 follow/unfollow의 로직 처리 시간에 영향을 받음.
에러가 발생했을 때, 함수 자체가 미실행 된 것인지, 버튼 이벤트가 실패한 것인지에 대한 피드백이 없음 (다른 UI 장치가 없다고 가정.)
사용자 경험(UX)를 증진시키기 위해 적용하는 개념으로, 상태, 데이터 업데이트와 관련해서 우선 데이터 변경이 정상적으로 이루어진 다는 것을 가정하여 요청이 마무리 되기전에 상태를 변경하고, 추후 실패 상태에 대해서는 다시 원래 상태로 복구 시켜 에러 이벤트에 대한 피드백을 전달하는 방식
기존 업데이트 플로우 | 낙관적 업데이트 플로우 |
---|---|
1. 사용자 동작 수행 2. 서버에 변경 사항 전달 3. 전달 성/패에 따라 UI 업데이트 |
1. 사용자 동작 수행 2. 성공 케이스로 UI 업데이트 3. 서버에 변경 사항 전달 4. 실패 시 기존 상태로 롤백 |
낙관적 업데이트를 적용하는 기준에 대해서 프로젝트 데모 발표 때에도 받았었다. 개인적으로, 이번에 적용한 팔로우, 좋아요 버튼과 같이 사용자가 api 요청에 대한 결과를 사용하지 않아 사용자 액션에 영향을 주지 않는 UI의 UX를 개선하기 위해 사용하려고 했다.
반면에 갱신된 서버 데이터로 인해서 이후 다른 인터렉션에 영향이 미치는 경우나 중요한 비즈니스 로직의 경우 빠른 반응보다 정확한 처리가 더 중요하기 때문에 이런 경우에는 지양하는 것이 좋겠다라는 생각을 했다.
단순히 ‘상태를 미리 갱신한다.’는 점에서 setState를 통해서 구현이 가능하다.
mutateFn 내에서 api 요청을 실행하기 전에 state를 수정하고, 성공 시에 상태는 그대로 두고, 에러 발생 시 mutateFn의 인자로 전달받은 값을 통해 이전 값으로 갱신한다.
```jsx
const useToggleFollow = ({
initFollowState,
}: {
initFollowState: boolean | null;
}) => {
...
const mutation = useMutation<
void,
Error,
TogglePropsType,
MutationContextType
>({
mutationFn: async ({ userId, following }) => {
// 상태 먼저 갱신
setState(!following);
following ? await deleteFollow(userId) : await postFollow(userId);
},
onSuccess: () => {
const currentUserId = TokenHandler.getUserIdFromToken();
if (currentUserId) {
[
ARTWORK.followedArtists,
USER.followerList(currentUserId),
USER.followingList(currentUserId),
].forEach((queryKey) => {
queryClient.invalidateQueries({ queryKey: [queryKey] });
});
}
// 성공시 변경된 상태 그대로 유지
},
onError: (error, { following }) => {
// 에러 발생시 상태 롤백
setState(following);
console.error('팔로우 상태 변경 에러: ', error.message);
},
});
...
};
```
이것도 충분히 잘 작동한다.
tanstack query에서는 낙관적 업데이트를 위한 useMutation의 onMutate 옵션을 제공하고 있다. 공식 문서에서는 다음과 같이 onMutate에 대해서 설명하고 있다.
const useToggleFollow = ({
initFollowState,
}: {
initFollowState: boolean | null;
}) => {
...
const mutation = useMutation<
void,
Error,
TogglePropsType,
MutationContextType
>({
mutationFn: async ({ userId, following }) => {
following ? await deleteFollow(userId) : await postFollow(userId);
},
onMutate: async ({ following }) => {
await queryClient.cancelQueries();
// 에러 케이스를 대비하여 기존 상태 값 저장
const prevFollowStatus = isFollowing;
// 일단 상태 변경
setIsFollowing(!following);
return { prevFollowStatus };
},
onSuccess: () => {
const currentUserId = TokenHandler.getUserIdFromToken();
if (currentUserId) {
[
ARTWORK.followedArtists,
USER.followerList(currentUserId),
USER.followingList(currentUserId),
].forEach((queryKey) => {
queryClient.invalidateQueries({ queryKey: [queryKey] });
});
}
// 성공시 변경된 상태 그대로 유지
},
onError: (error, _, context) => {
console.error('팔로우 상태 변경 에러: ', error.message);
// 에러가 발생한 경우 원래 상태로 롤백
if (context?.prevFollowStatus !== undefined) {
setIsFollowing(context.prevFollowStatus);
}
},
});
...
};
내 생각 )
낙관적 업데이트가 필요한 경우는 대부분 서버 상태 갱신을 위해 로직을 처리하는 과정에서 딜레이가 발생하기 때문이다. setState로 낙관적 업데이트를 구현하게 되면 상태를 업데이트하는 과정이 마치 클라이언트 상태 갱신과 같이 동작하게 된다.
따라서, tanstack query는 서버 상태 관리를 위한 라이브러리로서 낙관적 업데이트 관련한 onMutate 옵션을 제공함으로서 보다 서버 상태와 클라이언트 상태를 명확하게 구분하기 위함이 아닐까..? 🧐
GPT )
낙관적 업데이트가 필요한 이유는 대부분 서버 상태 갱신 과정에서의 딜레이 때문입니다.
setState
로 낙관적 업데이트를 구현할 수도 있지만, 이는 클라이언트 상태를 관리하는 방식과 동일하게 동작하게 됩니다.
하지만 TanStack Query는 서버 상태 관리 라이브러리이므로, 클라이언트 상태와 서버 상태를 명확하게 구분하기 위해 useMutation
을 제공합니다.
또한 useMutation
은 단순히 상태를 변경하는 것을 넘어서, 낙관적 업데이트, 캐싱, 에러 핸들링, 롤백 기능까지 포함하여 서버 상태를 더욱 안정적으로 관리할 수 있도록 도와줍니다.
따라서, 서버 상태를 보다 효과적으로 관리하기 위해 setState
대신 useMutation
을 사용하는 것이 더 적절합니다. ✅
→ 내가 적은 생각에 보완한 느낌이라 유사한 점이 많다. 캐싱과 에러 핸들링 부분을 추가로 말해주면서 보다 서버 / 클라이언트 상태 갱신을 분리한다는 점에 더해 편리함에 있어서도 이점을 가진다고 말해주고 있다.
https://tecoble.techcourse.co.kr/post/2023-08-15-how-to-improve-ux-with-optimistic-update/
https://tanstack.com/query/v4/docs/framework/react/guides/optimistic-updates
https://tanstack.com/query/v4/docs/framework/react/reference/useMutation
내 생각과 GPT에 물어본 답을 비교한 점이 인상적이네요! 좋은 공부 방향인 것 같아요 👍🏻