마이페이지 페이지네이션과 캐싱을 활용한 성능 최적화
개선된 점
// 경험치에 따라 레벨을 반복적으로 업데이트하는 함수
export const updateUserLevel = async (userId: string) => {
try {
const { data: userData, error: userError } = await supabase
.from("USER_TABLE")
.select("user_exp, user_rank")
.eq("user_id", userId)
.single();
if (userError || !userData) {
console.error("사용자 데이터 불러오기 실패", userError?.message);
return;
}
const { user_exp } = userData;
let { user_rank } = userData;
const { data: rankData, error: rankError } = await supabase
.from("RANK_TABLE")
.select("rank_base, exp")
.lte("exp", user_exp) // user_exp 이하의 exp를 가져오기
.order("exp", { ascending: false }) // exp 기준 내림차순으로 정렬
.limit(1); // 가장 높은 exp 기준 한 개만 가져오기
if (rankError || !rankData) {
console.log("레벨 기준 데이터 불러오기 실패", rankError?.message);
return; // 조건에 맞는 레벨 기준이 없으면 종료
}
const { rank_base } = rankData[0];
//현재 레벨과 DB 레벨 비교
if (user_rank !== rank_base) {
user_rank = rank_base;
const { error: updateError } = await supabase.from("USER_TABLE").update({ user_rank }).eq("user_id", userId);
if (updateError) {
console.log("USER_TABLE 업데이트 오류", updateError.message);
} else {
console.log("USER_TABLE 업데이트 성공", { user_exp, user_rank });
}
}
} catch (error) {
console.error("레벨 업데이트 중 오류 발생:", error);
}
};
const UserComment = ({ userId }: { userId: string }) => {
const [comments, setComments] = useState<UserComment[] | null>(null);
useEffect(() => {
const loadCommentsWithRecipes = async () => {
const commentsData = await fetchUserComments(userId);
if (commentsData?.comments?.length) {
// 댓글의 post_id를 이용해 레시피 정보 추가
const commentsWithRecipes = await Promise.all(
commentsData.comments.map(async (comment) => {
const recipeData = await fetchRecipeByPostId(comment.post_id);
return { ...comment, recipe: recipeData };
})
);
setComments(commentsWithRecipes);
}
};
loadCommentsWithRecipes();
}, [userId]);
if (!comments)
return (
<div className="flex w-full flex-col items-center justify-center gap-2 pt-6">
<Image src={AlertIcon} alt="느낌표 아이콘" width={30} height={30} />
아직 작성한 댓글이 없어요!
</div>
);
// 하단 로직 생략
export const useUserLevel = (userId: string) => {
const queryClient = useQueryClient();
// 사용자 데이터 가져오기
const { data: userData, isLoading: isUserLoading } = useQuery({
queryKey: ["userData", userId],
queryFn: () => fetchUserData(userId),
enabled: !!userId, // userId가 있을 때만 쿼리가 활성화 (조건부 실행)
staleTime: 1000 * 60 * 5, // 불필요한 재요청을 방지
gcTime: 1000 * 60 * 10, // 캐시에 저장된 데이터는 마지막으로 접근된 후 10분 동안 유지
refetchOnWindowFocus: false, // 사용자가 브라우저 창으로 돌아와도 데이터를 안 가져옴
refetchInterval: false // 주기적으로 데이터를 재요청하지 않음
});
// 랭킹 데이터 가져오기
const { data: rankData, isLoading: isRankLoading } = useQuery({
queryKey: ["rankData", userData?.user_exp],
queryFn: () => fetchRankData(userData!.user_exp),
enabled: !!userData?.user_exp,
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 10,
refetchOnWindowFocus: false,
refetchInterval: false
});
// 사용자 레벨 업데이트
const { mutate: updateRank } = useMutation({
mutationFn: updateUserRank,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["userData", userId] });
}
});
// 사용자 랭킹 비교 및 업데이트
const checkAndUpdateRank = () => {
if (userData && rankData && userData.user_rank !== rankData.rank_base) {
updateRank({ userId, userRank: rankData.rank_base });
}
};
return {
userData,
rankData,
isLoading: isUserLoading || isRankLoading,
checkAndUpdateRank
};
};
import LeftArrow from "@images/LeftArrow";
import RightArrow from "@images/RightArrow";
interface PaginationProps {
currentPage: number;
pageSize: number;
totalItems: number;
className?: string;
buttonClassName?: string;
onPageChange: (page: number) => void;
}
const Pagination: React.FC<PaginationProps> = ({
currentPage,
pageSize,
totalItems,
className,
buttonClassName,
onPageChange
}) => {
const totalPages = Math.ceil(totalItems / pageSize);
const maxVisiblePages = 5;
// 표시할 페이지 범위를 계산
const halfVisiblePages = Math.floor(maxVisiblePages / 2);
let startPage = Math.max(1, currentPage - halfVisiblePages);
let endPage = Math.min(totalPages, currentPage + halfVisiblePages);
// 페이지 범위가 5개 미만일 때 양쪽으로 채워서 항상 5개가 되도록 조정
if (endPage - startPage + 1 < maxVisiblePages) {
if (startPage === 1) {
endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
} else if (endPage === totalPages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
}
const handlePageClick = (page: number) => {
if (page > 0 && page <= totalPages) {
onPageChange(page);
}
};
return (
<div className={`text-body-14 ${className}`}>
{/* 이전 버튼 */}
<div className="flex items-center justify-between">
<button onClick={() => handlePageClick(currentPage - 1)} className={`${buttonClassName}`}>
<LeftArrow className="cursor-pointer stroke-[#C4C3BE] hover:stroke-Primary-300" />
</button>
{/* 페이지 번호 */}
{Array.from({ length: maxVisiblePages }, (_, index) => {
const page = startPage + index;
return (
<button
key={index}
onClick={() => handlePageClick(page)}
disabled={page > totalPages}
className={`min-h-[1.875rem;] min-w-[1.875rem;] gap-6 rounded sm:gap-3 ${
currentPage === page ? "rounded-full bg-Primary-300 text-white" : "text-Primary-300"
} ${page > totalPages ? "cursor-not-allowed opacity-50" : ""}`}
>
{page}
</button>
);
})}
{/* 다음 버튼 */}
<button onClick={() => handlePageClick(currentPage + 1)} className={`${buttonClassName}`}>
<RightArrow className="cursor-pointer stroke-[#C4C3BE] hover:stroke-Primary-300" />
</button>
</div>
</div>
);
};
export default Pagination;
집밥도감 사이트는 이미지가 많은 편이어서 단순 스크롤로 댓글을 보여줄 시 이미지 렌더링이 오래걸린다.
게다가 이미지 뿐만 아니라 각 댓글 별 사용자 정보, 게시글 정보까지 불러오기 때문에 Pagination
으로 나눠서 가져오도록 수정하였다.
Pagination
은 집밥도감 사이트 전반적으로 사용하고 있어서 공용컴포넌트로 제작하고 마이페이지에 적용해주었다.
const UserComment = ({ userId }: { userId: string }) => {
const [comments, setComments] = useState<UserComment[] | null>(null);
const [commentCount, setCommentCount] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const pageSize = 4; // 페이지당 댓글 수
const loadCommentsWithRecipes = async (page: number) => {
setIsLoading(true);
const { comments: commentsData, commentCount } = await fetchUserComments(userId, page, pageSize);
setCommentCount(commentCount);
if (commentsData?.length) {
const commentsWithRecipes = await Promise.all(
commentsData.map(async (comment) => {
const recipeData = await fetchRecipeByPostId(comment.post_id);
return { ...comment, recipe: recipeData };
})
);
setComments(commentsWithRecipes);
} else {
setComments(null);
}
setIsLoading(false);
};
// 하단 로직 생략
{/* 페이지네이션 */}
<Pagination
currentPage={currentPage}
pageSize={pageSize}
totalItems={commentCount}
onPageChange={(page) => setCurrentPage(page)}
className="min-w-[372px] gap-6"
buttonClassName="px-10"
/>
}
const pageSize = 4
값을 지정해서 단순 스크롤로 댓글을 불러오는게 아니라 페이지네이션으로 4개로 끊어서 가져오게 했다.