시작하며...
이번 프로젝트에서 게시물의 좋아요/취소 기능을 구현하는데, 백엔드 API 가 느려 응답을 기다리기까지 과정이 사용자 관점에서 좋지 않다는 단점을 발견하였다.
그러나 실제 SNS 에서는 네트워크 속도가 느려도 좋아요/취소 기능이 바로바로 반영이 되는 것을 볼 수 있다. 그래서 이를 보고 좋아요 버튼을 누르면 클라이언트 측에 좋아요가 바로 반영되는 기능을 구현하려고 한다.
이러한 기능을 구현하기 위해 TanStack Query 의 Optimistic Updates 를 사용하여 좋아요 기능을 구현하였다.
Optimistic Updates
Optimistic Updates 란?
단어 뜻 그대로 낙관적인 업데이트 라는 뜻이다.
서버 요청 시 클라이언트 측에서 UI 를 업데이트를 될 거라고 낙관적으로 가정하여, 미리 UI 를 업데이트하여 서버 요청 실패 시 롤백이 되는 방식이다.
그래서 좋아요를 눌렀을 때, 서버에서 응답이 올 때까지 기다리지 않고 UI 상으로 바로 업데이트 해주고 서버의 응답을 기다리고 만약 요청이 실패했을 경우 다시 원래 상태로 롤백을 해준다.
특히 인터넷이 좋지 않아 응답이 느릴 때 효율적이며, 빠른 인터랙션을 요구하는 기능이라 유저에서 좋은 사용자 경험성을 제공해 줄 수 있다.
자세한 내용은 TanStack Query 공식문서에서 확인할 수 있다.
[TanStack Query 공식문서 - Optimistic Updates]
구현하기
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 상으로 업데이트하는 것이 아니라 롤백도 가능하여 에러 처리도 안정적으로 할 수 있다는 점에서 안정성과 성능을 개선할 수 있었다.
이를 통해 사용자 경험성을 향상시키고 최적화할 수 있어서 좋았다.
'💜 프로젝트 구현' 카테고리의 다른 글
[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 |
[Next.js] 무한 캐러셀 컴포넌트 직접 구현하기 (feat. Tailwind CSS) (2) | 2024.09.02 |
FE 개발자가 되고 싶은 짱잼이
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!