ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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

     

    GitHub - Wunhyeon/Next-Supabase-Chat

    Contribute to Wunhyeon/Next-Supabase-Chat development by creating an account on GitHub.

    github.com

     

    앞으로 할것.(바로 다음은 아니고 나중에)

    https://supabase.com/docs/guides/realtime/broadcast

     

    Broadcast | Supabase Docs

    Send and receive messages using Realtime Broadcast

    supabase.com

     

    '사이드프로젝트' 카테고리의 다른 글

    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

    댓글

Designed by Tistory.