⭐ 개발환경
* React (CRA)
* TypeScript
* TanStack Query v5
* Firebase / FireStore
* Devtools
⭐ Optimistic Update + useInfiniteQuery 사용 이유
* 사용자 경험을 향상시키기 위함
* 비동기 작업과 관련된 복잡성을 쉽게 해결 가능
* 데이터 패칭/ 데이터 캐싱/ 오류시 상태관리 가능
=> 요청된 데이터 캐시하고, 해당 데이터 변경시 자동으로 업데이트 해줌.
=> 프리패칭으로 미리 가져와서 처리하기 가능 (리팩토링시 적용 예정)
1) Optimistic Update
: 사용자가 좋아요 버튼을 눌렀을때, 즉시 UI 업데이트를 하기 가능, 후에 데이터처리
2) useInfiniteQuery
: 한번에 모든 데이터가 아닌 일부 데이터만 불러오기 가능함
(대량의 데이터를 처리에 유용해보임)
: 더보기, 페이지네이션, 무한 스크롤 등 패칭을 구현하는데 쉬움
⭐ 참고자료
(1) 하트 클릭하는 버튼
<St.HeartClickButton
onClick={(e) => handleClickLikeButton(e, post.id, post)}
$isLiked={!!post.isLiked}
>
{post.isLiked ? <GoHeartFill /> : <GoHeart />}
</St.HeartClickButton>
▶ <St.HeartClickButton> : styled-components 사용
▶ 클릭시 handleClickLikeButton : event, 아이디, 게시물 데이터 전달
▶ $isLiked : css props (★: props로 전달시, 꼭 camelCase로 작성해야함)
▶ !!post.isLiked : !! 사용해서 boolean으로 반환하기 (!!post.isLiked = true /false)
▶ 아이콘 : react-icons/go import해서 사용
(2) handleClickLikeButton 함수
const handleClickLikeButton = async (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
id: string,
post: PostType
) => {
e.preventDefault();
e.stopPropagation();
if (!currentUserId) {
//모달창: 로그인 또는 좋아요 누르기 취소
await toggleLike({ postId: id, postData: post });
};
▶ e.preventDefault() / e.stopPropagation() : 이벤트 기본 동작 취소
(새로고침 + 부모요소로 이벤트 전달 등)
▶ if (!currentUserId) { ... } : 로그인한 경우에만 좋아요 기능 구현 가능
▶ toggleLike({ postId: id, postData: post }) : 좋아요 토글
: 뮤테이션은 toggleLike 함수를 호출
(postId와 postData => 좋아요 하고 싶은 게시물의 id와 게시물 데이터)
(3) toggleLike : 좋아요 토글 + 좋아요 수 반영
(3-1) mutationFn
interface PostCardProps {
postId: string;
postData: PostType;
}
const { mutateAsync: toggleLike } = useMutation({
mutationFn: async (params: PostCardProps) => {
const { postId, postData } = params;
const postRef = doc(db, 'posts', postId);
if (postData.isLiked !== undefined && currentUserId !== undefined) {
let newLikeCount;
if (postData.isLiked) {
//이미 좋아요한 경우
newLikeCount = postData.likeCount ? postData.likeCount - 1 : 0;
} else {
//좋아요 안 한 경우
newLikeCount = postData.likeCount != undefined ? postData.likeCount + 1 : 1;
}
await updateDoc(postRef, {
likedUsers: postData.isLiked ? arrayRemove(currentUserId) : arrayUnion(currentUserId),
likeCount: newLikeCount
});
} else {
return;
}
},
onMutate:()=>{},
onError:()=>{},
onSetteld:()=>{},
});
▶ mutationFn: async (params: PostCardProps) : 비동기함수의 뮤테이션 함수
: params라는 객체를 매개변수로 받음
( 해당 개체는 PostId, postData: 게시물 데이터 (PostType형식: 내가 지정함)
▶ const postRef = doc(db, 'posts', postId);
: Firestore에서 'posts'라는 컬렉션에서,
특정 게시물(postId)을 Firestore 문서에 대한 참조 생성
▶ if (postData.isLiked !== undefined && currentUserId !== undefined) { ... }
: 좋아요 기능은 로그인한 사용자만 가능
▶ newLikeCount : 업데이트할 좋아요 수
(이렇게 구분하지 않으면 계속 -1 이 되었음)
1) 좋아요를 이미 누른 경우: 좋아요 취소 + 좋아요 수 -1
2) 좋아요를 안 누른 경우 : 좋아요 + 좋아요 수 +1
▶ await updateDoc(postRef, { ... })
: updateDoc은 fireStore에서 문서를 업데이트 할때 사용
: likeCount 에는 새롭게 업데이트 한 좋아요 수 넣기
: arrayUnion과 arrayRemove를 사용해서 좋아요 & 좋아요 취소한 사용자 id 저장
(3-2) onMutate
onMutate: async (params: PostCardProps) => {
const { postId: selectedPostId } = params;
await queryClient.cancelQueries({ queryKey: [QUERY_KEYS.POSTS, category] });
//이전 데이터 저장
const previousPosts = queryClient.getQueriesData<InfiniteData<PostTypeFirebase[]> | undefined>({
queryKey: [QUERY_KEYS.POSTS, category]
});
queryClient.setQueryData<InfiniteData<PostTypeFirebase[]>>([QUERY_KEYS.POSTS, category], (oldData) => {
if (!oldData) return oldData;
const updateData = oldData?.pages.map((pagesPost) => {
return pagesPost.map((post) => {
const newLikeCount = post.isLiked ? post.likeCount - 1 : post.likeCount + 1;
if (post.id === selectedPostId) {
return {
...post,
isLiked: !post.isLiked,
likeCount: newLikeCount
};
} else {
return post;
}
});
});
return {
...oldData,
pages: updateData
};
});
return { previousPosts };
},
▶ 뮤테이션이 시작되기 전기 전에 호출되는 함수 = 제일 먼저 실행됨!
(로컬 상태나 캐시를 업데이트 하는 용도로 사용)
▶queryClient.cancelQuries({쿼리키})
: 아직 처리하지 못한 데이터 또는 overwirte 하지 않기 위해 작성
▶queryClient.getQueriesData 함수 : 이전 데이터 가져옴 => 백업용
▶ param에서 필요한 postId 추출 = selectedPostId (좋아요 토글 작업할 게시물 ID)
▶ queryClient.setQueriesData 함수 : 쿼리 업데이트
(호출 방법)
1) queryKey : 쿼리키 설정 (카테고리 별로, 데이터를 가져오는 구조임)
2) (prevPosts) => {...} : 이전 쿼리 데이터를 받아서 새로운 데이터로 변환하는 로직
* 이전 쿼리 데이터에 (최초 쿼리 수행시)
빈 데이터 객체 반환 (★ infiniteQuery의 구조인 pages와 pageParams 중요)
3) updatePages : 이전 데이터에서 pages 배열 가져오기
( ★ infiniteQuery의 구조인 pages )
여기서 selectedPostId와 일치하는 게시물 찾아서 'isLiked' 속성 바꾸기
4) return 반환값 : 업데이트된 페이지 데이터를 포함한 새로운 객체
React Query 내부 메커니즘 중 하나인 캐시에 저장
캐시된 데이터: useMutation을 통해 다른 곳에서 사용=> 업데이트 된 데이터 반환!
▶ context에 이전데이터(백업용) 에 넣어줌
=> onMutate는 UI 변경부터 해주기에 error 발생시 다시 원래 UI로 돌려야함
(3-3) onError & onSettled
onError: (Error, _, context) => {
if (context?.previousPosts) {
queryClient.setQueryData([QUERY_KEYS.POSTS], context.previousPosts);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: [category] });
}
▶ onMutate는 UI 변경부터 해주기에 error 발생시 다시 원래 UI로 돌려야함
▶ onSettled 핸들러는 성공 &실패 모든 경우에 항상 호출
▶ queryClient.invalidateQueries : [category] 쿼리를 재호출해서 데이터를 업데이트
= 캐시데이터에 반영하기 위한 코드
[ (3) toggleLike : 전체 코드]
const { mutateAsync: toggleLike } = useMutation({
mutationFn: async (params: PostCardProps) => {
const { postId, postData } = params;
const postRef = doc(db, 'posts', postId);
if (postData.isLiked !== undefined && currentUserId !== undefined) {
let newLikeCount;
if (postData.isLiked) {
//이미 좋아요한 경우
newLikeCount = postData.likeCount ? postData.likeCount - 1 : 0;
} else {
//좋아요 안 한 경우
newLikeCount = postData.likeCount != undefined ? postData.likeCount + 1 : 1;
}
await updateDoc(postRef, {
likedUsers: postData.isLiked ? arrayRemove(currentUserId) : arrayUnion(currentUserId),
likeCount: newLikeCount
});
} else {
return;
}
},
onMutate: async (params: PostCardProps) => {
const { postId: selectedPostId } = params;
await queryClient.cancelQueries({ queryKey: [QUERY_KEYS.POSTS, category] });
//이전 데이터 저장
const previousPosts = queryClient.getQueriesData<InfiniteData<PostTypeFirebase[]> | undefined>({
queryKey: [QUERY_KEYS.POSTS, category]
});
queryClient.setQueryData<InfiniteData<PostTypeFirebase[]>>([QUERY_KEYS.POSTS, category], (oldData) => {
if (!oldData) return oldData;
const updateData = oldData?.pages.map((pagesPost) => {
return pagesPost.map((post) => {
const newLikeCount = post.isLiked ? post.likeCount - 1 : post.likeCount + 1;
if (post.id === selectedPostId) {
return {
...post,
isLiked: !post.isLiked,
likeCount: newLikeCount
};
} else {
return post;
}
});
});
return {
...oldData,
pages: updateData
};
});
return { previousPosts };
},
onError: (Error, _, context) => {
if (context?.previousPosts) {
queryClient.setQueryData([QUERY_KEYS.POSTS], context.previousPosts);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: [category] });
}
});
끝.
'TIL :: Today I Learned' 카테고리의 다른 글
query-string : URL 쿼리 파라미터로 카테고리와 정렬 처리 (0) | 2024.01.23 |
---|---|
파이어베이스 오류 : The query requires an index (0) | 2024.01.22 |
타입스크립트 + useInfiniteQuery : 더보기 기능 마지막 데이터 확인 (0) | 2024.01.18 |
타입스크립트 + Context : 모달 팝업 구현 (0) | 2024.01.17 |
react-spinners : 로딩스피너 구현 (0) | 2024.01.16 |