ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 5. Message 보내기. - 메세지 테이블 만들기, Policy 생성, type 생성
    사이드프로젝트 2024. 7. 22. 17:32

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

    를 보며 따라하는 실습

     

     

    ---

    저번시간에 이어서 메세지 만들기를 진행한다.

     

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

    "use client";
    
    import React from "react";
    import { Input } from "./ui/input";
    
    export const ChatInput = () => {
      // 메세지 전송 펑션
      const handleSendMessage = (text: string) => {
        alert(text);
      };
    
      return (
        <div className="p-5">
          <Input
            placeholder="send message"
            onKeyDown={(e) => {
              if (e.key === "Enter") {
                // enter 키를 누르면 메세지가 전송되도록
                handleSendMessage(e.currentTarget.value);
                e.currentTarget.value = ""; // 메세지를 전송하고 나서 칸을 비워준다.
              }
            }}
          />
        </div>
      );
    };

    안을 이렇게 채워준다. 아직 supabase와 연결하지는 않았고, 기본 틀만 잡았다.

    이제 app/page.tsx에 가서 기존에 만들어놨던 message input부분을 이 컴포넌트로 바꿔준다.

    import ChatHeader from "@/components/ChatHeader";
    import { ChatInput } from "@/components/ChatInput";
    import { Button } from "@/components/ui/button";
    import { Input } from "@/components/ui/input";
    import InitUser from "@/lib/store/InitUser";
    import { createClient } from "@/utils/supabase/server"; // supabase 객체 불러오기. // 서버 컴포넌트니깐 서버에서 임포트해온다.
    import React from "react";
    
    const page = async () => {
      const supabase = createClient(); // supabase 객체 불러오기.
    
      // const session = await supabase.auth.getSession();
      // console.log(session.data.session?.user);
    
      const user = (await supabase.auth.getUser()).data.user;
      // console.log("user: ", user);
    
      return (
        <>
          <div className="max-w-3xl mx-auto md:py-10 h-screen">
            <div className="h-full border rounded-md flex flex-col">
              <ChatHeader user={user} />
              {/* 이제 zustand를 통해 user의 state management를 하기 때문에 ChatHeader에 user props를 안넘겨줘도 되지만, 굳이 지울 필요도 없으므로 남겨준다. */}
              <div className="flex-1  flex flex-col p-5 h-full overflow-y-auto">
                <div className="flex-1"></div>
                <div className="space-y-7">
                  {Array.from({ length: 15 }, (v, i) => i).map((value) => {
                    return (
                      <div className="flex gap-2" key={value}>
                        <div className="h-10 w-10 bg-green-500 rounded-full"></div>
                        <div className="flex-1">
                          <div className="flex items-center gap-1">
                            <h1 className="font-bold">Jaehyeon</h1>
                            <h1 className="text-sm text-gray-400">
                              {new Date().toDateString()}
                            </h1>
                          </div>
                          <p className="text-gray-300">
                            Lorem ipsum dolor, sit amet consectetur adipisicing
                            elit. Quisquam minus dolorum voluptatibus perferendis
                            nesciunt ex odit rem est a maiores!
                          </p>
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>
              <ChatInput />	// 요기
            </div>
          </div>
          <InitUser user={user} />
          {/* user state 관리를 시작할 수 있도록. */}
        </>
      );
    };
    
    export default page;

     

    이제 메세지를 입력하고 엔터를 눌러보면 alert가 뜨며 정상작동 하는걸 볼 수 있따.

     

    이제 supabase DB와 연결해줘야 한다. 먼저 supabase 프로젝트에 들어가서 테이블을 생성해준다.

    messages라는 이름으로 테이블을 생성해주고 다음과 같은 칼럼들을 만들어준다.

     

    Enable Realtime 에 체크해주고, user_id는 users의 아이디와 외래키 연결을 해준다. 그리고 default value를 auth.uid()로 해준다. 옆의 버튼을 누르면 나온다. (로우를 추가하거나 업데이트 할때, 유저의 아이디를 반환해준다.)

    밑에 짤려서 안보이는데, remove는 삭제되도 no action으로 했다. 유저가 삭제되더라도 메세지는 남아있어야 한다고 생각했기 때문이다.

    그리고 위 칼럼들에서 is_deleted말고는 전부 nullable이 False이다. 톱니바퀴를 눌러 isNullable설정을 해제해준다. 

    테이블 생성.

     

    테이블이 생성되었으면 RLS Policy를 생성해준다. 누가 글을 쓸수있고, 볼수있고, 수정할수있고 등등의 권한을 설정해주는 거다.

    Add RLS Policy로 들어가서, Create Policy를 해준다. 

    먼저, 인증된 사람만 메세지를 읽을수있도록 해주기 위해 SELECT를 선택하고, Target Roles에 authenticated를 선택해준다. 

    그러면 밑에 쿼리를 만들어준다. 

    Save policy

     

    다음으로 메세지 테이블에 인서트 할수있는 권한을 생성할 거다. Insert도 역시 인증된 유저들만 가능하게 할거다. 그리고 user_id와 created_at을 자동으로 생성해주기 위해 밑의 쿼리를 살짝 바꿔준다.

     

    save policy

     

    다음으로 수정, 삭제 정책도 생성해준다.

    일단 나는 메세지를 물리적으로 삭제해주는 게 아니라 is_deleted에 날짜가 들어가 있으면 삭제된걸로 치는 Soft delete를 할거긴 한데, 이건 연습예제이니 삭제도 만들어줘보기로 한다. 삭제 정책을 만드는 건 쉽다.

     

    삭제는 이 메세지를 작성한 유저만 할 수 있어야 하는데, Enable delete for users based on user_id 라는 게 옆에 있어서 이걸 눌러주면 바로 쿼리를 생성해줘서 쉽다.

    그래도 쿼리를 다시한번 읽어줘본다. Target Roles랑 Table도 한번 살펴주고.

     

    Update 는 해줘야 할 것들이 좀 있다.

    이름도 알맞게 바꿔주고, 테이블 선택하고 Target Roles선택해주는것까지는 전에 했던 것들이랑 같다.

    여기서 밑의 쿼리문을 위 사진과 같이 만들어준다.

     

    이제 메세지에 필요한 정책(권한)들이 생성됬다.

     

    다음으로 코드로 돌아와 supabase types를 만들어 줄거다.

    이게 무슨말이냐면, 지금 타입스크립트로 만들어주고 있는데, supabase DB(postgreSQL)에서는 타입스크립트를 사용하지 않으니 타입추론을 하지 못하게되고, 타입이 맞지않고 불편할 수 있는데 이걸 맞춰주는 거다.

    말로 설명하기가 조금 이상한데, 혹시 ORM 써봤다면 Supabase ORM을 설정해주는거라 생각하면 편하다.

     

    https://supabase.com/docs/guides/api/rest/generating-types

     

    Generating TypeScript Types | Supabase Docs

    How to generate types for your API and Supabase libraries.

    supabase.com

    이쪽으로 들어가 Type-definition 정도까지 해볼꺼다.

    더 필요한게 있으면 더 진행해도 된다.

     

    진행하다 보면 뜬금없이 Deno Setting 을 해줄거냐고 나와서 No라고 해줬고,

    Generate types for your project to produce the types/supabase.ts file:

    npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > types/supabase.ts

    이렇게 프로젝트 ID를 찾아 넣어줘야 하는 부분이 있는데,

    프로젝트 -> Settings -> General -> ReferenceID를 넣어주면 된다.

     

    아, 그리고 그 전에 위에서 하라는 대로 types/supabase.ts 파일을 만들어줘야 한다. 저기다가 안만들어줄거면, 다른 파일을 만들고 명령어 뒷부분을 수정하도록 하자.

     

    여기까지 진행했으면 types/supabase.ts파일안에 supabase에서 만들었던 테이블들이 정의되어 있는 것을 볼 수 있다. 

    이제 우리의 프로젝트에서 타입추론을 쉽게해주기 위해 예전에 만들었던 utils/supabase 폴더의 client.ts, server.ts파일에서 supabase 객체를 정의하는 부분에 generic을 붙여준다.

     

    utils/supabase/client.tsx
    
    "use client";
    
    import { Database } from "@/types/supabase";
    import { createBrowserClient } from "@supabase/ssr";
    
    export function createClient() {
      return createBrowserClient<Database>( // 뒤에 generic으로 Database를 붙여줬다.
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
      );
    }

     

    // utils/supabase/server.tsx
    
    import { Database } from "@/types/supabase";
    import { createServerClient } from "@supabase/ssr";
    import { cookies } from "next/headers";
    
    export function createClient() {
      const cookieStore = cookies();
    
      return createServerClient<Database>( // 뒤에 generic으로 Database를 붙여줬다.
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
        {
          cookies: {
            getAll() {
              return cookieStore.getAll();
            },
            setAll(cookiesToSet) {
              try {
                cookiesToSet.forEach(({ name, value, options }) =>
                  cookieStore.set(name, value, options)
                );
              } catch {
                // The `setAll` method was called from a Server Component.
                // This can be ignored if you have middleware refreshing
                // user sessions.
              }
            },
          },
        }
      );
    }

     

    이제 ChatInput.tsx로 돌아가 메세지 전송 펑션 작성을 계속 할건데, 이렇게 자동완성이 되는 걸 볼 수 있다.

    messages테이블과 users테이블이 자동완성 되는 모습

     

     

    이제 메세지 전송 펑션을 이렇게 작성해놓고 한번 실행해보겠다. 어떤 결과를 반환받나 콘솔을 한번 찍어보는 거다.

     // 메세지 전송 펑션
      const handleSendMessage = async (text: string) => {
        alert(text);
    
        const result = await supabase.from("messages").insert({ text });
        // user_id(작성자 id) 의 경우 위에서 policy를 만들 때, user_id = auth.uid()구문을 통해 자동으로 들어가게끔 만들어져있다.
        console.log("result : ", result);
      };

     

    예상외로 콘솔에 아무것도 찍히지 않았다.

    그래도 supabase에 들어가보면

    이렇게 DB에는 잘 저장된 것을 볼 수 있었다.

     

    혹시나 싶어 

    const { data, error, status } = await supabase
          .from("messages")
          .insert({ text });
          
        console.log("data : ", data);
        console.log("error : ", error);
        console.log("status : ", status);

     

    이렇게 객체를 분리해서도 콘솔을 찍어보았지만 아무것도 찍히지 않았다. 그래도 에러 대비는 해줘야 하므로 

    에러가 있을경우 처리를 해주자.

     

    shadcn에서 sonner를 검색해 인스톨해주고,

    npx shadcn-ui@latest add sonner

    layout.tsx에 가서

    import type { Metadata } from "next";
    import { Space_Grotesk } from "next/font/google";
    import "./globals.css";
    import { ThemeProvider } from "@/components/theme-provider";
    import { Toaster } from "@/components/ui/sonner";
    
    const space_Grotesk = Space_Grotesk({ subsets: ["latin"] });
    
    export const metadata: Metadata = {
      title: "Create Next App",
      description: "Generated by create next app",
    };
    
    export default function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      return (
        <html lang="en">
          <body className={space_Grotesk.className}>
            <ThemeProvider
              attribute="class"
              defaultTheme="dark"
              enableSystem
              disableTransitionOnChange
            >
              {children}
              {/* 경고 Toaster Sonner */}
              <Toaster position="top-center" />
            </ThemeProvider>
          </body>
        </html>
      );
    }

    Toaster를 추가해준다. position은 자기 원하는대로

     

    그리고 다시 ChatInput.tsx로 돌아와 메세지 전송 펑션을 완성해준다.

     // 메세지 전송 펑션
      const handleSendMessage = async (text: string) => {
        const { data, error, status } = await supabase
          .from("messages")
          .insert({ text });
    
        if (error) {
          toast.error(error.message);
        }
      };

     

    Sonner에 대해서 궁금해서 더 찾아봤는데, (Shadcn홈페이지에서는 그냥 인스톨하고 쓰는 방법만 나와있어서),

    아마도? 개인이 만든 토스트 라이브러리인 것 같고, Node module에 직접 들어가 어떻게 쓸수 있는지 보거나

    https://sonner.emilkowal.ski/

     

    Sonner

    Installation npm install sonner Usage Render the toaster in the root of your app. import { Toaster, toast } from 'sonner'function App() { return ( toast('My first toast')}> Give me a toast )} Types You can customize the type of toast you want to render, a

    sonner.emilkowal.ski

    여기로 들어가면 다양한 옵션들을 볼 수 있다.

     

    github : https://github.com/Wunhyeon/Next-Supabase-Chat/tree/5.sendMessage

     

    댓글

Designed by Tistory.