이전 글에서 useInfinityQuery를 사용하여 무한 스크롤 피드를 구현해보았다.
하지만 두가지 문제가 존재했다.
- 새롭게 글을 작성/수정/삭제 하는경우 피드데이터는 어떻게 최신화 되어야 하는가?
- 세부 글보기에서 좋아요/댓글을 추가/삭제하는 경우 피드데이터는 어떻게 최신화 되어야 하는가?
이번에는 이 두 가지 문제를 해결해보자.
이 문제 중 두번째 문제가 당근마켓 그룹플랫폼 인턴직무의 사전 질문이었다...!
이걸 직접 해결해 볼 수 있는 기회가 이렇게나 빨리 오게 될 줄이야....
글을 새롭게 작성하거나 수정/삭제하는 경우
우선, 글을 새롭게 작성하거나/수정/삭제하는 경우다.
사용자입장에서 글을 쓰고,수정하고, 삭제를 한다면 그 사용자의 목적은 내 글이 어떻게 되었는가, 잘 업데이트 되었는가이다.
따라서 피드 데이터를 처음부터 받아오고, 최신 피드데이터를보아야 하므로 최상단으로 이동시켜야 한다고 생각했다.
그렇다면 queryKey의 데이터를 무시하고 새롭게 피드데이터를 받아와야 한다.
이는 invalidateQueries 메서드로 가능하다.
invalidateQueries
React-query에서는 queryClient의 invalidateQueries 메서드를 통해 캐시데이터를 무효화한다.
이러면 해당 쿼리가 마운트된 상황일때 queryFn을 통해 데이터를 다시 받아오게된다.
queryClient.invalidateQueries({ queryKey: [무효화할 query Key] });
한줄 요약하면 QueryKey에 저장된 데이터를 다시 받아오게 만드는 것이다.
예시로 글을 삭제하는 경우, mutate가 성공적으로 수행되면 무한 스크롤 데이터의 캐시 데이터를 무효화시켜주었다.
import { useToast } from '@repo/toast';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import REACT_QUERY_KEYS from '@/constants/REACT_QUERY_KEYS';
import useAuthAxios from '@/hooks/useAuthAxios';
export default function useDeleteArticleMutation() {
const openToast = useToast();
const queryClient = useQueryClient();
const navigate = useNavigate();
const authAxios = useAuthAxios();
const deleteThread = (id: number) => {
return authAxios.delete(`/apiURL/${id}`);
};
return useMutation({
mutationFn: deleteThread,
onSuccess: () => {
openToast({ type: 'information', content: '삭제 완료' });
navigate(-1);
queryClient.invalidateQueries({ queryKey: [REACT_QUERY_KEYS.ENTIRE_THREADS] });
},
});
}
이러면 글 작성/수정/삭제시, 진행중이던 무한스크롤에서 최상단으로 이동하게 되며 최신 데이터를 받아올 수 있게 된다.
좋아요/댓글/답글을 추가/삭제 하는 경우
그렇다면, 단일 글에서 좋아요, 댓글, 답글을 다는 이벤트가 일어났을때는 어떻게 해야할까?
이 때는 글작성/수정/삭제와 달리 사용자의 의도가 피드 탐색에 더욱 초점이 맞춰진 상태라고 생각했다.
따라서 캐시 데이터를 무효화하면 탐색하던 페이지로 돌아오는게 아닌, 최상단으로 올라가게 될 것이므로 사용자가 불편함을 느끼게 될 것이다.
따라서 다른 방법이 필요하다.
setQueryData
React Query에서는 queryKey내부 데이터를 변경할 수 있는 메서드도 제공한다...!
아래와 같이 이전의 쿼리데이터도 가져올 수 있고, 새롭게 쿼리 데이터를 지정할 수 도 있다.
queryClient.setQueryData([querKey], (이전 쿼리 데이터) => (새롭게 지정할 쿼리 데이터));
자 그럼 새로운 데이터만 제대로 가져올 수 있다면 피드데이터를 수정해줄 수 있다.
단일 글 데이터를 활용하기
우리 프로젝트는 피드에서 단일 글을 눌러 해당 글로 이동할 때, 단일 글 요소의 id를 통해 서버에서 데이터를 받아온다.
그리고, 댓글, 좋아요 등 사용자의 actions이 있을 시 해당 글의 캐시를 무효화 하는 형태로 단일 글의 데이터를 최신상태로 유지했다.
단일글 받아오기(useThreadQuery)
export default function useThreadQuery(threadId: number) {
const authAxios = useAuthAxios();
const getThread = async () => {
const result = await authAxios.get<ArticleRawData>(`/apiURL/${threadId}`);
return result.data;
};
return useQuery({ queryKey: [REACT_QUERY_KEYS.THREAD, threadId], queryFn: getThread });
}
좋아요 클릭시 캐시 무효화 (useLikeArticleMutation)
export default function useLikeArticleMutation() {
const queryClient = useQueryClient();
const authAxios = useAuthAxios();
const likeThread = async (threadId: number) => {
await authAxios.post(`/likeApiURL/?id=${threadId}`);
return threadId;
};
return useMutation({
mutationFn: likeThread,
onSuccess: (threadId) => {
//mustate가 제대로 일어나면 해당 id 글의 캐시를 무효화한다.
queryClient.invalidateQueries({ queryKey: [REACT_QUERY_KEYS.THREAD, threadId] });
},
});
}
그럼 최신데이터를 받아왔을 때, 피드데이터를 동기화 시켜주면 된다.
어떻게 동기화 할 것인가?
단순하게 모든 페이지를 순회하면서 해당 글 id를 통해 요소를 찾을 수도 있을 것이다.
하지만 그것보다는 요소 클릭시 해당 요소의 무한 스크롤 페이지 index를 기록하고, 해당 페이지 내에서 탐색하는 것이 효과적이라고 판단했다.
export default function Threads() {
const { data, isFetchingNextPage, fetchNextPage } = useThreadsQuery();
// 특정 글 클릭시 전역상태 변경하는 함수
const setPageIndex = usePageIndex((state) => state.setPageIndex);
...
return (
...
{data.pages.map((page, pageIndex) =>
... // 클릭시 전역상태 변경
<FeedArticle data={feedData} onLinkMove={() => setPageIndex(pageIndex)} />
...
)),
)}
...
);
}
그럼 이제 단일글을 불러올때, 전역 pageIndex를 기반으로 해당 글을 탐색한 후, 최신 값으로 변경시켜주면 된다.
export default function useThreadQuery(threadId: number) {
const authAxios = useAuthAxios();
const queryClient = useQueryClient();
const pageIndex = usePageIndex((state) => state.pageIndex);
//최신화한 피드데이터 얻기
const changeFeedArticleData = (feedData: InfiniteData<FeedData>, newThreadData: ArticleRawData) => {
const newPage = [...feedData.pages];
const articleData = newPage[pageIndex].posts.find((post) => post.id === threadId); //해당 페이지에서 현재 글 찾기
if (articleData) { // 데이터 갱신
articleData.liked = newThreadData.liked; //좋아요 여부
articleData.likeCount = newThreadData.likeCount; //좋아요 수 갱신
articleData.commentCount = newThreadData.commentCount; //댓글 개수 갱신
...
}
return newPage;
};
const getThread = async () => {
const result = await authAxios.get<ArticleRawData>(`/apiURL/${threadId}`);
if (queryClient.getQueryState([REACT_QUERY_KEYS.THREADS]))
queryClient.setQueryData([REACT_QUERY_KEYS.THREADS], (feedData: InfiniteData<FeedData>) => ({
pages: changeFeedArticleData(feedData, result.data), //기존 피드데이터에서 새로운 피드데이터로 갱신
pageParams: feedData.pageParams,
}));
return result.data;
};
return useQuery({ queryKey: [REACT_QUERY_KEYS.THREAD, threadId], queryFn: getThread });
}
이러면 단일 글 보기에서 좋아요,댓글 입력시 피드에 반영이 된다!