![[TanStack Query] 옵티미스틱 업데이트 사용하여 좋아요 구현](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd2sVv%2FbtsJFKrvggF%2FDf9dk9nvYHTWU20llovaMK%2Fimg.png)
시작하며...
이번 프로젝트에서 게시물의 좋아요/취소 기능을 구현하는데, 백엔드 API 가 느려 응답을 기다리기까지 과정이 사용자 관점에서 좋지 않다는 단점을 발견하였다.
그러나 실제 SNS 에서는 네트워크 속도가 느려도 좋아요/취소 기능이 바로바로 반영이 되는 것을 볼 수 있다. 그래서 이를 보고 좋아요 버튼을 누르면 클라이언트 측에 좋아요가 바로 반영되는 기능을 구현하려고 한다.
이러한 기능을 구현하기 위해 TanStack Query 의 Optimistic Updates 를 사용하여 좋아요 기능을 구현하였다.
Optimistic Updates
Optimistic Updates 란?
단어 뜻 그대로 낙관적인 업데이트 라는 뜻이다.
서버 요청 시 클라이언트 측에서 UI 를 업데이트를 될 거라고 낙관적으로 가정하여, 미리 UI 를 업데이트하여 서버 요청 실패 시 롤백이 되는 방식이다.
그래서 좋아요를 눌렀을 때, 서버에서 응답이 올 때까지 기다리지 않고 UI 상으로 바로 업데이트 해주고 서버의 응답을 기다리고 만약 요청이 실패했을 경우 다시 원래 상태로 롤백을 해준다.
특히 인터넷이 좋지 않아 응답이 느릴 때 효율적이며, 빠른 인터랙션을 요구하는 기능이라 유저에서 좋은 사용자 경험성을 제공해 줄 수 있다.
자세한 내용은 TanStack Query 공식문서에서 확인할 수 있다.
[TanStack Query 공식문서 - Optimistic Updates]
Optimistic Updates | TanStack Query React Docs
React Query provides two ways to optimistically update your UI before a mutation has completed. You can either use the onMutate option to update your cache directly, or leverage the returned variables to update your UI from the useMutation result. Via the
tanstack.com
구현하기
1. useMutation: Mutataion 기능 설정
useMutation 훅을 사용하여 게시물 좋아요 API 호출을 하기 때문에, 옵션을 설정해준다.
유저의 액션에 따라 좋아요면 likeBoard, 취소면 unLikeBoard API 를 호출하였다.
const { mutate } = useMutation({
mutationFn: async (userAction: "UNLIKE" | "LIKE") => {
if (userAction === "LIKE") {
await likeBoard(boardId);
} else {
await unlikeBoard(boardId);
}
},
});
2. onMutation: 낙관적 업데이트 설정
서버 요청 전에 캐시 된 데이터를 즉시 업데이트해서 사용자에게 빠른 응답을 제공하는 낙관적 업데이트를 설정해야 한다.
2-1. 기존 요청 취소
이미 서버 요청이 가고 있는 도중에 또 다른 요청이 중복되면 데이터가 일관되지 않는 경우가 발생할 수 있다.
그래서 네트워크 요청이 충돌하지 않도록 cancelQueries 를 사용하여 요청을 취소하여 안전한 상태에서 낙관적 업데이트를 진행되도록 하였다.
await queryClient.cancelQueries({ queryKey: ["board", boardId] });
2-2. 이전 데이터 저장
혹시 서버 요청이 실패할 경우, 낙관적 업데이트를 롤백해야 한다.
그래서 원래 데이터를 복원하기 위해 이전 기존 데이터를 저장하여, 추후에 onError 핸들러에서 사용된다.
const prevData = queryClient.getQueryData(["board", boardId]);
2-3. 캐시 데이터 업데이트
서버 응답을 기다리지 않고 바로 UI에 즉각적인 변화를 반영하기 위해 setQueryData 를 사용해서 캐시 데이터 업데이트를 한다.
사용자의 액션이 LIKE 라면 기존 좋아요 수에서 1 증가시키고, 기존 isLiked 값에 true로 업데이트한다.
이렇게 캐시 된 데이터를 바로 업데이트하여 서버 응답이 오기 전에 사용자에게 좋아요/취소에 따른 변화가 바로 보이도록 할 수 있다.
queryClient.setQueryData(["board", boardId], (prev: BoardResponse) => ({
...prev,
likeCount: userAction === "LIKE" ? prev.likeCount + 1 : prev.likeCount - 1,
isLiked: userAction === "LIKE",
}));
2-4. 전체 코드
낙관적 업데이트 설정 전체 코드는 다음과 같다.
onMutate: async (userAction) => {
await queryClient.cancelQueries({ queryKey: ["board", boardId] });
const prevData = queryClient.getQueryData(["board", boardId]);
queryClient.setQueryData(["board", boardId], (prev: BoardResponse) => ({
...prev,
likeCount: userAction === "LIKE" ? prev.likeCount + 1 : prev.likeCount - 1,
isLiked: userAction === "LIKE",
}));
return { prevData };
}
3. onError: 에러 발생 시 롤백 처리
만약 요청 도중에 에러가 발생하면 이전 데이터로 복구해야 한다.
setQueryData 를 통해 onMutation 에서 반환한 preData 로 캐시 된 데이터를 복구한다.
onError: (err, _, context) => {
queryClient.setQueryData(["board", boardId], context?.prevData);
}
4. onSettled: 데이터 동기화
API 요청이 완료되면 invalidateQueries 를 사용하여 최신 데이터로 업데이트하기 위해 쿼리 초기화를 하여 데이터를 동기화한다.
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["board", boardId] });
}
전체 코드 및 결과
전체 소스 코드
옵티미스틱 업데이트의 전체 코드는 다음과 같다.
const { mutate } = useMutation({
mutationFn: async (userAction: "UNLIKE" | "LIKE") => {
if (userAction === "LIKE") {
await likeBoard(boardId);
} else {
await unlikeBoard(boardId);
}
},
onMutate: async (userAction) => {
await queryClient.cancelQueries({ queryKey: ["board", boardId] });
const prevData = queryClient.getQueryData(["board", boardId]);
queryClient.setQueryData(["board", boardId], (prev: BoardResponse) => ({
...prev,
likeCount:
userAction === "LIKE" ? prev.likeCount + 1 : prev.likeCount - 1,
isLiked: userAction === "LIKE",
}));
return { prevData };
},
onError: (err, _, context) => {
queryClient.setQueryData(["board", boardId], context?.prevData);
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ["board", boardId],
});
},
});
결과
참고로 Lottie 와 Framer Motion 을 사용하여 좋아요/취소 애니메이션을 적용하였다.
아직 서버에서는 응답을 받지 않았는데 옵티미스틱 업데이트로 화면에는 바로 좋아요가 반영된 것을 볼 수 있다.

마치며....
이번에 TanStack Query 의 Optimistic Updates 를 활용하여 좋아요/취소 기능을 구현했다.
지금은 API 응답 속도가 빠르지만, 개발 초기에는 API 응답이 지연되어서 이를 사용자가 즉시 변화를 느낄 수 있게 구현해야겠다고 생각을 하여 옵티미스틱 업데이트를 활용하였다.
단순히 바로 UI 상으로 업데이트하는 것이 아니라 롤백도 가능하여 에러 처리도 안정적으로 할 수 있다는 점에서 안정성과 성능을 개선할 수 있었다.
이를 통해 사용자 경험성을 향상시키고 최적화할 수 있어서 좋았다.

'💜 프로젝트 구현' 카테고리의 다른 글
[AWS Amplify Gen2] Cognito, AWS SDK를 활용한 유저 기능 구현 (5) | 2024.11.05 |
---|---|
[Jotai/Tailwind CSS] Toast 컴포넌트 직접 구현 (feat. Next.js App Router) (0) | 2024.09.19 |
[Next.js] Lighthouse 웹 사이트 성능 개선 (100으로 만들기) (0) | 2024.09.10 |
[Next.js / TanStack query] useInfiniteQuery 사용하여 무한 스크롤 구현 (feat. cursor 방식) (0) | 2024.09.07 |
[Next.js] 세션 스토리지, 이전 페이지 저장해서 뒤로 가기 구현 (feat. app router) (0) | 2024.09.02 |
FE 개발자가 되고 싶은 짱잼이
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!