-
13. Pagination사이드프로젝트 2024. 7. 24. 04:54
https://www.youtube.com/watch?v=-xXASlyU0Ck&t=2007s
를 보며 실습하며 정리하는 글
-----
지금은 처음 접속할 때 모든 대화를 한번에 가져오고 있는데, 이게 아니라 페이지네이션으로 20개만 가져오도록 하겠다. 데이터가 너무 많으면 한번에 가져와서 처리하는 것보다 순차적으로 가져와서 처리하는 게 부담도 안가고 좋기때문이다.
// components/ChatMessages.tsx
import React, { Suspense } from "react"; import ListMessages from "./ListMessages"; import { createClient } from "@/utils/supabase/server"; import InitMessages from "@/lib/store/InitMessages"; const ChatMessages = async () => { const supabase = createClient(); const result = await supabase .from("messages") .select("*,users(*)") .range(0, 19) // index로 결정. 0부터 19번까지. 즉 20개 .order("created_at", { ascending: false }); // 내림차순으로. 최신의 20개를 가져오도록. return ( <Suspense fallback={"loading..."}> <ListMessages /> <InitMessages messages={result.data?.reverse() || []} /> // 오름차순으로 20개를 가져오면 최근께 위로 올라가기 때문에 reverse()를 통해 최신께 가장 밑으로 내려가게끔 해준다. {/* result.data는 null 이 올수도 있는데, InitMessages에서는 props로 IMessages[]만 받고있으므로 null일경우 빈배열을 넣어주기 위해 || []를 해줌. */} </Suspense> ); }; export default ChatMessages;
데이터를 가져오는 부분에서 range와 order 를 추가해줬다. 그리고 내림차순으로 20개를 가져오면 순서가, 가장 최신께 위로 가기 때문에 reverse()로 거꾸로 표시해주도록 만들었다.
그럼 이렇게 순서대로 잘 나오는 걸 볼 수 있다.
그런데 페이지네이션을 실험하기에는 많은 수보다는 적은수로 해서 실험하고 바꿔주는게 좋다.
이렇게 해주기 위해 직접 range안의 숫자를 바꿔줘도 좋지만, 좀 더 관리를 편하게하기 위해. 혹시 다른것들도 이런게 생길수 있으니.
lib/constant/index.ts 변수를 만들어 상수들을 관리할 수 있도록 한다.
export const LIMIT_MESSAGE = 2;
이렇게 상수로 만들어주고 임포트해와서 range에 넣어준다.
그리고 메세지를 더 가져오라는 버튼을 만들어준다.
components/LoadMoreMessages.tsx
import React from "react"; import { Button } from "./ui/button"; import { createClient } from "@/utils/supabase/client"; import { LIMIT_MESSAGE } from "@/lib/constant"; const LoadMoreMessages = () => { const fetchMore = async () => { const supabase = createClient(); const { data } = await supabase .from("messages") .select("*,users(*)") .range(0, LIMIT_MESSAGE) .order("created_at", { ascending: false }); }; return ( <Button variant={"outline"} className="w-full"> Load More </Button> ); }; export default LoadMoreMessages;
fetchMore이라는 함수로 데이터를 더 패치해오도록 할것이다.
그런데 데이터를 어디에서부터 어디까지 가져와야할지 정해줘야 한다. 그걸 위한 작업을 해주겠다.
먼저 lib/utils로 가서 함수를 만들어준다.
export function getFromAndTo(page: number, itemPerPage: number) { let from = page * itemPerPage; let to = from + itemPerPage - 1; return { from, to }; }
이게 뭐냐면 한 페이지에서 몇번 인덱스부터 몇번 인덱스까지 가져올지를 정하는 거다.
예를들어 0페이지이고, 한페이지당 아이템이 10개씩 있다고 하자.
그럼 from = 0 * 10 = 0
to = 0 + 10 -1 = 9 가 되어 0페이지는 0번부터 9번 인덱스까지의 10개의 아이템을 가져온다.
그리고 1페이지는
from = 1 * 10 = 10
to = 10 + 10 -1 = 19
으로 10번 인덱스부터 19번인덱스까지의 아이템을 가져오게 된다.
2페이지는
from = 2 * 10 = 20
to = 20 + 10 -1 = 29
이런식으로 몇페이지에 아이템을 어디서부터 어디까지를 정해주는 함수다.
---
보통 page는 0페이지부터가 아니라 1페이지 부터이므로
export function getFromAndTo(page: number, itemPerPage: number) { let from = (page - 1) * itemPerPage; let to = from + itemPerPage - 1; return { from, to }; }
이렇게 해준다.
그런데 우리는 지금 LoadMore 버튼을 만들고 있고, 처음 랜딩이 되고 난 상태에서 추가로 더 불러오는 버튼이기 때문에 0페이지는 이미 불러진 상태이고, 그 다음인 1페이지부터 불러오는거다. 그래서 let from - page * itemPerPage로 1을 안빼줬다.
---
이제 page도 전역 state로 관리하도록 lib/store/messages.ts로 이동해서 page와 page설정을 해주는 걸 추가해주자.
import { create } from "zustand"; export type IMessage = { created_at: string; id: string; is_deleted: string | null; is_edit: boolean; text: string; user_id: string; users: { avatar_url: string; created_at: string; deleted_at: string | null; display_name: string; id: string; updated_at: string | null; } | null; }; interface MessageState { messages: IMessage[]; addMessage: (message: IMessage) => void; actionMessage: IMessage | undefined; setActionMessage: (message: IMessage | undefined) => void; optimisticDeleteMessage: (messageId: string) => void; optimisticUpdateMessage: (message: IMessage) => void; optimisticIds: string[]; setOptimisticIds: (id: string) => void; page: number; // page ******* setMessages: (messages: IMessage[]) => void; // 새로 패치한 메세지를 설정해주고, 페이지를 한칸 늘려주는 메서드 ******* } export const useMessage = create<MessageState>()((set) => ({ messages: [], addMessage: (newMessages) => set((state) => ({ messages: [...state.messages, newMessages], })), // 기존 state에 담겨있던 message들 + 이번에 작성한 메세지 actionMessage: undefined, setActionMessage: (message) => set(() => ({ actionMessage: message })), optimisticDeleteMessage: (messageId) => set((state) => { return { messages: state.messages.filter((message) => message.id != messageId), }; }), optimisticUpdateMessage: (updateMessage) => set((state) => { return { messages: state.messages.filter((message) => { if (message.id == updateMessage.id) { message.text = updateMessage.text; message.is_edit = updateMessage.is_edit; } return message; }), }; }), optimisticIds: [], setOptimisticIds: (id: string) => set((state) => ({ optimisticIds: [...state.optimisticIds, id] })), page: 1, // page. default가 1이다. ********** setMessages: (messages) => // 새로 불러온 메세지를 설정해주는 메서드 ****** set((state) => ({ messages: [...messages, ...state.messages], page: state.page + 1, // 데이터를 새로 로드하고 page +1을 해준다. ******** })), }));
그리고 components/LoadMoreButtons.tsx로 돌아가 이걸 활용해준다.
import React from "react"; import { Button } from "./ui/button"; import { createClient } from "@/utils/supabase/client"; import { LIMIT_MESSAGE } from "@/lib/constant"; import { getFromAndTo } from "@/lib/utils"; import { useMessage } from "@/lib/store/messages"; import { toast } from "sonner"; const LoadMoreMessages = () => { const page = useMessage((state) => state.page); const setMessages = useMessage((state) => state.setMessages); const fetchMore = async () => { const supabase = createClient(); const { from, to } = getFromAndTo(page, LIMIT_MESSAGE); const { data, error } = await supabase .from("messages") .select("*,users(*)") .range(from, to) .order("created_at", { ascending: false }); if (error) { toast.error(error.message); } else { setMessages(data.reverse()); } }; return ( <Button variant={"outline"} className="w-full" onClick={fetchMore}> Load More </Button> ); }; export default LoadMoreMessages;
이제 LoadMore Button을 누를때마다 데이터를 추가로 가져오는 걸 볼 수 있다.
이제, 더이상 추가로 가져올 데이터가 없을 때, LoadMore 버튼이 사라지도록 해보자.
lib/store/messages.ts
import { create } from "zustand"; import { LIMIT_MESSAGE } from "../constant"; export type IMessage = { created_at: string; id: string; is_deleted: string | null; is_edit: boolean; text: string; user_id: string; users: { avatar_url: string; created_at: string; deleted_at: string | null; display_name: string; id: string; updated_at: string | null; } | null; }; interface MessageState { messages: IMessage[]; addMessage: (message: IMessage) => void; actionMessage: IMessage | undefined; setActionMessage: (message: IMessage | undefined) => void; optimisticDeleteMessage: (messageId: string) => void; optimisticUpdateMessage: (message: IMessage) => void; optimisticIds: string[]; setOptimisticIds: (id: string) => void; page: number; setMessages: (messages: IMessage[]) => void; hasMore: boolean; // LoadMore버튼으로 더 가져올 데이터가 있는지. **************** } export const useMessage = create<MessageState>()((set) => ({ messages: [], addMessage: (newMessages) => set((state) => ({ messages: [...state.messages, newMessages], })), // 기존 state에 담겨있던 message들 + 이번에 작성한 메세지 actionMessage: undefined, setActionMessage: (message) => set(() => ({ actionMessage: message })), optimisticDeleteMessage: (messageId) => set((state) => { return { messages: state.messages.filter((message) => message.id != messageId), }; }), optimisticUpdateMessage: (updateMessage) => set((state) => { return { messages: state.messages.filter((message) => { if (message.id == updateMessage.id) { message.text = updateMessage.text; message.is_edit = updateMessage.is_edit; } return message; }), }; }), optimisticIds: [], setOptimisticIds: (id: string) => set((state) => ({ optimisticIds: [...state.optimisticIds, id] })), page: 1, setMessages: (messages) => set((state) => ({ messages: [...messages, ...state.messages], page: state.page + 1, hasMore: messages.length >= LIMIT_MESSAGE, // 새로 받아온 messages가 LIMIT_MESSAGE보다 많거나 같은지. 예를들어 LIMIT_MESSAGE가 10이고 이번에 새로 받아온 messages가 10이라면 더 가져올 데이터가 있다는 뜻이지만, LIMIT_MESSAGE가 10인데 이번에 새로 받아온 messages가 5라면 더 가져올 데이터가 없다는 뜻이다. })), hasMore: true, // LoadMore버튼으로 더 가져올 데이터가 있는지.*********8 }));
여기서 setMessages에서도 hasMore를 설정해주는 걸 볼 수 있는데,
새로 받아온 messages가 LIMIT_MESSAGE보다 많거나 같은지. 예를들어 LIMIT_MESSAGE가 10이고 이번에 새로 받아온 messages가 10이라면 더 가져올 데이터가 있다는 뜻이지만, LIMIT_MESSAGE가 10인데 이번에 새로 받아온 messages가 5라면 더 가져올 데이터가 없다는 뜻이다.
그리고 lib/store/InitMessages.tsx로 이동해 초기 hasMore 설정을 해준다.
"use client"; // react hook을 사용할 거기 때문에, 클라이언트 컴포넌트로 import React, { useEffect, useRef } from "react"; import { IMessage, useMessage } from "./messages"; import { LIMIT_MESSAGE } from "../constant"; const InitMessages = ({ messages }: { messages: IMessage[] }) => { // props로 유저 객체를 받아옴. const initState = useRef(false); // 초기값 const hasMore = messages.length >= LIMIT_MESSAGE; // 처음 받아온 데이터가 LIMIT_MESSAGE보다 많으면 hasMore = true, 없으면 false useEffect(() => { if (!initState.current) { // 초기값이 false라면, state management를 할 수 있도록 user객체를 넣어준다. useMessage.setState({ messages, hasMore }); // 초기값에 hasMore를 set해준다. } initState.current = true; }, []); return <></>; }; export default InitMessages;
그리고 components/LoadMoreMessages.tsx로 이동해서 메세지를 불러오다가 더 불러올 메세지가 없으면 LoadMore버튼이 안보이게끔 처리해주자.
import React from "react"; import { Button } from "./ui/button"; import { createClient } from "@/utils/supabase/client"; import { LIMIT_MESSAGE } from "@/lib/constant"; import { getFromAndTo } from "@/lib/utils"; import { useMessage } from "@/lib/store/messages"; import { toast } from "sonner"; const LoadMoreMessages = () => { const page = useMessage((state) => state.page); const setMessages = useMessage((state) => state.setMessages); const hasMore = useMessage((state) => state.hasMore); // hasMore State 불러오기 const fetchMore = async () => { const supabase = createClient(); const { from, to } = getFromAndTo(page, LIMIT_MESSAGE); const { data, error } = await supabase .from("messages") .select("*,users(*)") .range(from, to) .order("created_at", { ascending: false }); if (error) { toast.error(error.message); } else { setMessages(data.reverse()); } }; if (hasMore) { // true면 버튼 보여줌. return ( <Button variant={"outline"} className="w-full" onClick={fetchMore}> Load More </Button> ); } else { // False면 버튼 안보여줌. return <></>; } }; export default LoadMoreMessages;
그럼 이제 끝까지 Load해보면 LoadMore 버튼이 사라진 것을 볼 수 있다.
github : https://github.com/Wunhyeon/Next-Supabase-Chat/tree/13.pagination
앞으로 할것.(바로 다음은 아니고 나중에)
https://supabase.com/docs/guides/realtime/broadcast
'사이드프로젝트' 카테고리의 다른 글
15. Logout (0) 2024.07.26 14. Presence (현재 접속자) (0) 2024.07.25 한글 두번씩 입력되는 버그해결 (2) 2024.07.24 12. Arrow down & notification (4) 2024.07.24 11.실시간 통신 (3) 2024.07.23