ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 9. Message Delete
    사이드프로젝트 2024. 7. 23. 03:34

    https://www.youtube.com/watch?v=-xXASlyU0Ck&t=2007s

    를 보며 실습하며 정리하는 글

    -----

     

    저번시간에 Message Menu를 만든거에 이어서 Delete를 만들거다.

    먼저 ShadCN에 가서 Alert Dialog를 검색하고 인스톨해준다.

    https://ui.shadcn.com/docs/components/alert-dialog

     

    Alert Dialog

    A modal dialog that interrupts the user with important content and expects a response.

    ui.shadcn.com

    npx shadcn-ui@latest add alert-dialog

     

    그리고 새로운 컴포넌트를 만들어줄거다.

    components/MessageActions.tsx 파일을 만들어준다.

    import React from "react";
    import {
      AlertDialog,
      AlertDialogAction,
      AlertDialogCancel,
      AlertDialogContent,
      AlertDialogDescription,
      AlertDialogFooter,
      AlertDialogHeader,
      AlertDialogTitle,
      AlertDialogTrigger,
    } from "@/components/ui/alert-dialog";
    import { Button } from "@/components/ui/button";
    
    export function DeleteAlert() {
      return (
        <AlertDialog>
          <AlertDialogTrigger asChild>
            <button id="trigger-delete"></button>	// button에 id만 들어가있고 안에 내용이 없기 때문에 안보이는 버튼이다.
          </AlertDialogTrigger>
          <AlertDialogContent>
            <AlertDialogHeader>
              <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
              <AlertDialogDescription>
                This action cannot be undone. This will permanently delete your
                account and remove your data from our servers.
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Cancel</AlertDialogCancel>
              <AlertDialogAction>Continue</AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>
      );
    }
    
    const MessageActions = () => {
      return;
    };
    
    export default MessageActions;

     

    위에서 주의해서 봐야할 점은 button에 id만 넣어놓고 안에 내용이 없기 때문에 버튼이 보이지 않는다는 것이다.

    그리고 위에서 만들어준 DeleteAlert를 components/ListMessages.tsx에 넣어준다.

    "use client";
    
    import { useMessage } from "@/lib/store/messages";
    import React from "react";
    import Message from "./Message";
    import { DeleteAlert } from "./MessageActions";
    
    const ListMessages = () => {
      const messages = useMessage((state) => state.messages); // zustand에 전역으로 저장된 state를 불러옴.
    
      return (
        <div className="flex-1  flex flex-col p-5 h-full overflow-y-auto">
          <div className="flex-1"></div>
          <div className="space-y-7">
            {messages.map((value, idx) => {
              return <Message key={idx} message={value} />;
            })}
          </div>
          <DeleteAlert />	// <= 요기
        </div>
      );
    };
    
    export default ListMessages;

     

    그리고

    components/message.tsx로 이동해 MessageMenu에서 삭제 버튼에 해당하는 곳에 이렇게 넣어준다.

    const MessageMenu = () => {
      return (
        <DropdownMenu>
          <DropdownMenuTrigger>
            <Ellipsis />
          </DropdownMenuTrigger>
          <DropdownMenuContent>
            <DropdownMenuLabel>Action</DropdownMenuLabel>
            <DropdownMenuSeparator />
            <DropdownMenuItem>Edit</DropdownMenuItem>
            <DropdownMenuItem
              // 요기. Delete button의 아이디를 찾아내서 클릭해주는 함수
              onClick={() => {
                document.getElementById("trigger-delete")?.click();
              }}
            >
              Delete
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      );
    };

     

    이렇게 해주는 이유는 모든 대화마다 Dropdown menu에 Delete와 Edit 버튼을 추가해서 렌더링 해주는 것보다, 버튼 하나 생성해서 재사용가능하게 해주는 편이 더 좋기 때문이다. 

    여기까지 하면 버튼을 클릭했을때 경고문구가 잘 나올 것이다.

    그런데 여기서 문제가, 현재 어떤 대화를 선택했는지를 알려주고 있지 않다. 그래서 어떤 대화를 삭제요청을 보내야할지가 없다.

    이제 그 작업을 해주자.

    먼저 lib/store/messages.ts로 이동해 

    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;
    }
    
    export const useMessage = create<MessageState>()((set) => ({
      messages: [],
      addMessage: (message) =>
        set((state) => ({ messages: [...state.messages, message] })), // 기존 state에 담겨있던 message들 + 이번에 작성한 메세지
      actionMessage: undefined,
      setActionMessage: (message) => set(() => ({ actionMessage: message })),
    }));

    actionMessage와 setActionMessage라는 state를 추가해준다. 

     

    그리고 다시 components/Message.tsx로 이동해서 

    import { IMessage, useMessage } from "@/lib/store/messages";
    import Image from "next/image";
    import React from "react";
    import {
      DropdownMenu,
      DropdownMenuContent,
      DropdownMenuItem,
      DropdownMenuLabel,
      DropdownMenuSeparator,
      DropdownMenuTrigger,
    } from "@/components/ui/dropdown-menu";
    import { Ellipsis } from "lucide-react";
    import { useUser } from "@/lib/store/user";
    
    const MessageMenu = ({ message }: { message: IMessage }) => {
      // 1. props로 메세지를 받는다.
      const setActionMessage = useMessage((state) => state.setActionMessage); // 2. setActionMessage를 state에서 받아온다.
    
      return (
        <DropdownMenu>
          <DropdownMenuTrigger>
            <Ellipsis />
          </DropdownMenuTrigger>
          <DropdownMenuContent>
            <DropdownMenuLabel>Action</DropdownMenuLabel>
            <DropdownMenuSeparator />
            <DropdownMenuItem>Edit</DropdownMenuItem>
            <DropdownMenuItem
              // 요기
              onClick={() => {
                document.getElementById("trigger-delete")?.click();
                setActionMessage(message); // 3. setActionMessage로 actionMessage에 props로 받아온 메세지를 설정해준다.
              }}
            >
              Delete
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      );
    };
    
    const Message = ({ message }: { message: IMessage }) => {
      const user = useUser((state) => state.user); // 유저 정보 가져오기
    
      return (
        <div className="flex gap-2">
          <div>
            <Image
              src={message.users?.avatar_url!}
              alt={message.users?.display_name!}
              width={40}
              height={40}
              className="rounded-full right-2"
            />
          </div>
    
          <div className="flex-1">
            <div className="flex items-center  justify-between">
              <div className="flex items-center gap-1">
                <h1 className="font-bold">{message.users?.display_name}</h1>
                <h1 className="text-sm text-gray-400">
                  {new Date(message.created_at).toDateString()}
                </h1>
              </div>
              {/* props로 메세지를 넘겨준다. */}
              {message.users?.id === user?.id && <MessageMenu message={message} />}
            </div>
            <p className="text-gray-300">{message.text}</p>
          </div>
        </div>
      );
    };
    
    export default Message;

    1. MessageMen에서 props로 메세지를 받도록 해주고,

    2.   `const setActionMessage = useMessage((state) => state.setActionMessage);  `// 2. setActionMessage를 state에서 받아온다.

    3. <DropdownMenuItem
              // 요기
              onClick={() => {
                document.getElementById("trigger-delete")?.click();
                setActionMessage(message); // 3. setActionMessage로 actionMessage에 props로 받아온 메세지를 설정해준다.
              }}
            >

     

    그리고 아래 Messages에서 MessageMenu에 Props를 넘겨주는 걸 볼 수 있다.

    이렇게되면 어떤 메세지에 액션(삭제 또는 수정)을 해야할 지 알 수 있다.

     

    이제 Message Actions로 이동해, 위에서 setActionMessage를 이용해 상태를 set한 actionMessage 를 불러올 수 있다.

    // components/MessageActions.tsx

    import React from "react";
    import {
      AlertDialog,
      AlertDialogAction,
      AlertDialogCancel,
      AlertDialogContent,
      AlertDialogDescription,
      AlertDialogFooter,
      AlertDialogHeader,
      AlertDialogTitle,
      AlertDialogTrigger,
    } from "@/components/ui/alert-dialog";
    import { Button } from "@/components/ui/button";
    import { useMessage } from "@/lib/store/messages";
    
    export function DeleteAlert() {
      const actionMessage = useMessage((state) => state.actionMessage); // 앞에서 action Message 설정해준 걸 불러온다.
    
      return (
        <AlertDialog>
          <AlertDialogTrigger asChild>
            <button id="trigger-delete"></button>
          </AlertDialogTrigger>
          <AlertDialogContent>
            <AlertDialogHeader>
              <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
              <AlertDialogDescription>
                This action cannot be undone. This will permanently delete your
                account and remove your data from our servers.
                {/* 테스트를 위해 */}
                {actionMessage?.text}
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Cancel</AlertDialogCancel>
              <AlertDialogAction>Continue</AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>
      );
    }
    
    const MessageActions = () => {
      return;
    };
    
    export default MessageActions;

    테스트를 위해 텍스트를 그대로 불러오게 해줬는데, 잘 나온다.

     

    이제 메세지의 아이디를 받아서 Delete 리퀘스트를 보내주면 된다.

    components/MessageActions.tsx에서 작업해주자.

    import React from "react";
    import {
      AlertDialog,
      AlertDialogAction,
      AlertDialogCancel,
      AlertDialogContent,
      AlertDialogDescription,
      AlertDialogFooter,
      AlertDialogHeader,
      AlertDialogTitle,
      AlertDialogTrigger,
    } from "@/components/ui/alert-dialog";
    import { Button } from "@/components/ui/button";
    import { useMessage } from "@/lib/store/messages";
    import { createClient } from "@/utils/supabase/client";
    import { toast } from "sonner";
    
    export function DeleteAlert() {
      const actionMessage = useMessage((state) => state.actionMessage); // 앞에서 action Message 설정해준 걸 불러온다.
    
      const handleDeleteMessage = async () => {
        const supabase = createClient();
    	// 요기 추가됨.
        const { data, error } = await supabase
          .from("messages")
          .delete()
          .eq("id", actionMessage?.id!); // eq는 쿼리에서 WHERE 과 같은 뜻이다. 아마도 equal을 표현한게 아닐까 추측해본다.
    
        if (error) {
          toast.error(error.message);
        } else {
          toast.success("Successfully Delete a Message");
        }
      };
    
      return (
        <AlertDialog>
          <AlertDialogTrigger asChild>
            <button id="trigger-delete"></button>
          </AlertDialogTrigger>
          <AlertDialogContent>
            <AlertDialogHeader>
              <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
              <AlertDialogDescription>
                This action cannot be undone. This will permanently delete your
                account and remove your data from our servers.
                {/* 테스트를 위해 */}
                {actionMessage?.text}
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Cancel</AlertDialogCancel>
              <AlertDialogAction onClick={handleDeleteMessage}>
                Continue
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>
      );
    }
    
    const MessageActions = () => {
      return;
    };
    
    export default MessageActions;

     

    supabase 클라이언트 객체를 가져와 delete요청을 보내주고 있다. ORM문법과 거의 유사하다. 그냥 ORM이라고 봐도 될듯.

    .eq는 SQL의 WHERE과 같은 역할을 한다. 그러니깐 

    await supabase
          .from("messages")
          .delete()
          .eq("id", actionMessage?.id!);

    는 DELETE FROM messages WHERE id = actionMessageID 이 쿼리랑 같은 거다.

     

    그리고 밑에 에러처리도 해줬다.

     

    이제 삭제를 해보면 삭제가 잘 된다. 새로고침을 해줘야 메세지가 사라지지만.

     

    이것도 전시간에 INSERT 할때 optimistic update를 해줬던 것처럼 optimistic update로 지워줘보자.

     

    lib/store/messages.ts로 이동해서 optimisticDeleteMessages 메서드를 만들어줬다.

    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;
    }
    
    export const useMessage = create<MessageState>()((set) => ({
      messages: [],
      addMessage: (message) =>
        set((state) => ({ messages: [...state.messages, message] })), // 기존 state에 담겨있던 message들 + 이번에 작성한 메세지
      actionMessage: undefined,
      setActionMessage: (message) => set(() => ({ actionMessage: message })),
      optimisticDeleteMessage: (messageId) =>
        set((state) => {
          return {
            messages: state.messages.filter((message) => message.id != messageId),
          };
        }),
    }));

     

    이제 components/MessageActions.tsx로 이동해 이걸 사용해준다.

    import React from "react";
    import {
      AlertDialog,
      AlertDialogAction,
      AlertDialogCancel,
      AlertDialogContent,
      AlertDialogDescription,
      AlertDialogFooter,
      AlertDialogHeader,
      AlertDialogTitle,
      AlertDialogTrigger,
    } from "@/components/ui/alert-dialog";
    import { Button } from "@/components/ui/button";
    import { useMessage } from "@/lib/store/messages";
    import { createClient } from "@/utils/supabase/client";
    import { toast } from "sonner";
    
    export function DeleteAlert() {
      const actionMessage = useMessage((state) => state.actionMessage); // 앞에서 action Message 설정해준 걸 불러온다.
      const optimisticDeleteMessage = useMessage(	// optimisticDelete 불러오기
        (state) => state.optimisticDeleteMessage
      );
    
      const handleDeleteMessage = async () => {
        const supabase = createClient();
        optimisticDeleteMessage(actionMessage?.id!);	// optimisticDelete 사용
    
        const { data, error } = await supabase
          .from("messages")
          .delete()
          .eq("id", actionMessage?.id!); // eq는 쿼리에서 WHERE 과 같은 뜻이다. 아마도 equal을 표현한게 아닐까 추측해본다.
    
        if (error) {
          toast.error(error.message);
        } else {
          toast.success("Successfully Delete a Message");
        }
      };
    
      return (
        <AlertDialog>
          <AlertDialogTrigger asChild>
            <button id="trigger-delete"></button>
          </AlertDialogTrigger>
          <AlertDialogContent>
            <AlertDialogHeader>
              <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
              <AlertDialogDescription>
                This action cannot be undone. This will permanently delete your
                account and remove your data from our servers.
                {/* 테스트를 위해 */}
                {actionMessage?.text}
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Cancel</AlertDialogCancel>
              <AlertDialogAction onClick={handleDeleteMessage}>
                Continue
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>
      );
    }
    
    const MessageActions = () => {
      return;
    };
    
    export default MessageActions;

     

    이제 삭제를 눌러보면 대화가 삭제되는 것을 볼 수 있다.

     

    그런데 아직은 문제가, 생성(optimistic update) -> 삭제 (optimistic update) -> 새로고침 하면 삭제가 제대로 안된 걸 볼 수 있는데, 이는 생성을 할 때 optimistic update로 supabase DB에 있는 아이디로 메세지 컴포넌트를 만든 게 아니라, uuid라이브러리를 통해 임의로 만들어진 아이디로 메세지를 생성해줬기 때문에, 생성된 아이디와 삭제하는 아이디가 실제 DB와 맞지 않아서다. 이문제는 나중에 고쳐보기로 하겠다.

     

    github : https://github.com/Wunhyeon/Next-Supabase-Chat/tree/9.DeleteMessage

     

    GitHub - Wunhyeon/Next-Supabase-Chat

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

    github.com

     

    댓글

Designed by Tistory.