팀원들과 이야기를 나누던 중 설계적인 부분에서 초기에 후순위로 좋아요를 만들어 둔 후 바쁘게 달려오다보니 기획적인 이야기를 많이 하지 못하고 일을 진행했다. 그렇게 되면서 처음에는 supabase Table을 reviews 테이블과 likeReview테이블을 만들어 후기와 후기좋아요를 각각 관리했었는데
백엔드 적으로 복잡한 문제도 있었고 (sql문 사용, uuid주기)
우리가 사용하게 될 좋아요의 기능이 정렬, 사용자가 좋아요 누른 목록 보여주기 등 간단하게만 사용하게 될 것 같아
reviews 테이블 (후기)에 좋아요(likes)를 column을 추가하여 함께 관리하기로 하였다.
★ SUPABASE 업로드 (토글형식으로 로직작성)
export const toggleReviewLike = async (postId: string, userId: string) => {
const { data: reviewData, error: fetchError } = await supabase
.from('reviews')
.select('likes')
.eq('id', postId)
.single();
if (fetchError) throw fetchError;
const existingLikes = reviewData?.likes || [];
const isLiked = existingLikes.includes(userId);
let updatedLikes;
let likeType;
if (isLiked) {
updatedLikes = existingLikes.filter((id: string) => id !== userId);
likeType = false;
} else {
updatedLikes = [...existingLikes, userId];
likeType = true;
}
const { data, error } = await supabase.from('reviews').update({ likes: updatedLikes }).eq('id', postId);
if (error) throw error;
return { data, likeType };
};
♧ onMutate: 좋아요 추가 또는 삭제 요청이 발생하기 직전에 기존 데이터를 백업하고, UI를 미리 업데이트
♧ onError: 서버 요청이 실패하면 백업된 데이터를 사용하여 UI를 복구
♧ onSettled: 서버 요청이 성공하거나 실패한 후, invalidateQueries를 사용하여 데이터를 다시 가져옴
export const useAddLikeMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ postId, userId }: { postId: string; userId: string }) => addLike({ postId, userId }),
onMutate: async ({ postId, userId }) => {
// 기존 데이터를 백업합니다.
await queryClient.cancelQueries({ queryKey: QUERY_KEYS.reviews() });
const previousReviews = queryClient.getQueryData<any[]>(QUERY_KEYS.reviews());
// 낙관적 업데이트로 UI를 미리 변경합니다.
queryClient.setQueryData(QUERY_KEYS.reviews(), (oldReviews: any[] | undefined) => {
if (!oldReviews) return [];
return oldReviews.map(review =>
review.id === postId
? { ...review, likes: [...review.likes, userId] }
: review
);
});
// 기존 데이터를 롤백을 위해 반환합니다.
return { previousReviews };
},
onError: (_error, _variables, context) => {
// 서버 요청 실패 시 기존 데이터로 롤백합니다.
if (context?.previousReviews) {
queryClient.setQueryData(QUERY_KEYS.reviews(), context.previousReviews);
}
Notify.failure('도움돼요 추가에 실패했습니다. 다시 시도해주세요.');
},
onSuccess: () => {
Notify.success('도움돼요가 추가되었습니다.');
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.reviews() });
},
});
};
export const useRemoveLikeMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ postId, userId }: { postId: string; userId: string }) => removeLike({ postId, userId }),
onMutate: async ({ postId, userId }) => {
// 기존 데이터를 백업합니다.
await queryClient.cancelQueries({ queryKey: QUERY_KEYS.reviews() });
const previousReviews = queryClient.getQueryData<any[]>(QUERY_KEYS.reviews());
// 낙관적 업데이트로 UI를 미리 변경합니다.
queryClient.setQueryData(QUERY_KEYS.reviews(), (oldReviews: any[] | undefined) => {
if (!oldReviews) return [];
return oldReviews.map(review =>
review.id === postId
? { ...review, likes: review.likes.filter((likeId: string) => likeId !== userId) }
: review
);
});
// 기존 데이터를 롤백을 위해 반환합니다.
return { previousReviews };
},
onError: (_error, _variables, context) => {
// 서버 요청 실패 시 기존 데이터로 롤백합니다.
if (context?.previousReviews) {
queryClient.setQueryData(QUERY_KEYS.reviews(), context.previousReviews);
}
Notify.failure('도움돼요 삭제에 실패했습니다. 다시 시도해주세요.');
},
onSuccess: () => {
Notify.success('도움돼요가 취소되었습니다.');
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.reviews() });
},
});
};
💥 문제점
: 낙관적 업데이트를 하지 않았을 때에는 도움돼요 추가, 삭제가 잘 반영이 되었는데 낙관적 업데이트를 위한 로직들을 추가하였을 때는 계속해서오류가 발생하였다. 그로인해 낙관적 업데이트를 좀 더 찾아보면서 새로운 사실을 알게되었다.
나의 상황은 props로 이미 최신화 데이터를 불러오는 상위 컴포넌트에서 reviews데이터를 받고있었고 독립적인 데이터를 받고있기에 일반적이지 않은 상황이었던 것이다.
낙관적 업데이트는 보통 독립적으로 데이터를 다룰 때 많이 사용되며, props로 데이터를 내려받는 경우에는 다소 복잡하거나 필요하지 않은 경우가 많습니다.
props로 데이터를 내려받는 경우:
반면에 props로 데이터를 내려받는 경우에는, 이미 상위 컴포넌트에서 상태를 관리하고 있기 때문에 낙관적 업데이트가 불필요할 수 있다. 데이터가 상위 컴포넌트에서 관리되고 props로 하위 컴포넌트로 내려오면, 해당 데이터는 이미 서버와 동기화되었거나 서버에서 최신 상태를 받아오는 방식이기 때문에 상위 컴포넌트가 상태를 관리하고, 하위 컴포넌트에서는 그 상태를 props로 받아 사용하기만 하면 됩니다.
이 경우에는 클라이언트 측에서 임의로 UI 상태를 변경할 필요가 없고, 서버 응답을 기다리고 그에 따라 UI를 업데이트하는 방식으로 진행하는 것이 일반적이다.
대신, 상위 컴포넌트에서 상태를 변경하고 그 변경 사항을 하위 컴포넌트에 props로 전달하는 방식으로 충분히 처리할 수 있.