ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • NextJS + Supabase 채팅 앱 만들기 - 3. 로그아웃, 유저 상태관리 (Zustand), OAuth User생성 Trigger를 통해 public Schema에도 유저 생성해주기
    사이드프로젝트 2024. 7. 22. 02:23

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

     

    이분의 유튜브를 보며 실습하는 글

     

    --------

     

    저번시간에 이어서, 이제 로그인이 되었으면 로그인버튼을 로그인 버튼이 아니라 로그아웃 버튼으로 바꿔주는 것부터 하겠다.

    app/page.tsx로 이동해서 

    import ChatHeader from "@/components/ChatHeader";
    import { Button } from "@/components/ui/button";
    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);
    
      return (
        <div className="max-w-3xl mx-auto md:py-10 h-screen">
          <div className="h-full border rounded-md">
            <ChatHeader />
          </div>
        </div>
      );
    };
    
    export default page;

     

    supabase 객체를 통해 session을 불러와본다. 위에서 보면 console.log를 찍어봤는데, 이렇게 나온다.

    {
      data: {
        session: {
          access_token: 'eyJhbGciOiJIUzI1asdfxz15927JPRTBFVzFrUmhoZlhJQ2siLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2JrcmJuZXRvbmx5enFkY292b2h3LnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiJiOTc1YzI1MC00OWU0LTQxNzMtYTg1Zi00YjVhZDIzZGU3YmUiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzIxNTc3MjYzLCJpYXQiOjE3MjE1NzM2NjMsImVtYWlsIjoieGh3b2d1c3hoQGdtYWlsLmNvbSIsInBob25lIjoiIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZ2l0aHViIiwicHJvdmlkZXJzIjpbImdpdGh1YiJdfSwidXNlcl9tZXRhZGF0YSI6eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzQyMzY3MzE3P3Y9NCIsImVtYWlsIjoieGh3b2d1c3hoQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmdWxsX25hbWUiOiLsnoTsnqztmIQiLCJpc3MiOiJodHRwczovL2FwaS5naXRodWIuY29tIiwibmFtZSI6IuyehOyerO2YhCIsInBob25lX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiV3VuaHllb24iLCJwcm92aWRlcl9pZCI6IjQyMzY3MzE3Iiwic3ViIjoiNDIzNjczMTciLCJ1c2VyX25hbWUiOiJXdW5oeWVvbiJ9LCJyb2xlIjoiYXV0aGVudGljYXRlZCIsImFhbCI6ImFhbDEiLCJhbXIiOlt7Im1ldGhvZCI6Im9hdXRoIiwidGltZXN0YW1wIjoxNzIxNTczNjYzfV0sInNlc3Npb25faWQiOiIzMzBkM2Q3My1iNDJmLTRmYzQtYTM4Ni1hYTQ5NTU0NzkwZjYiLCJpc19hbm9ueW1vdXMiOmZhbHNlfQ.lYfzPYIiqUwbMcfe9ajxFXUPZXOMGmnp6QX9QQvJ7Tk',
          token_type: 'bearer',
          expires_in: 3600,
          expires_at: 1721577263,
          refresh_token: 'MsiEqW7123zCLK96oof1Kbw',
          user: [Object],
          provider_token: 'gho_t9hwQdCDnyNvESUOqCvpBAhUksO2ybZezxcv'
        }
      },
      error: null
    }

    세션 객체에 액세스토큰, 리프레시토큰, 만료시간, 유저정보등이 담겨있는 걸 볼 수 있다.

    유저정보는 제대로 안나와있으니 유저만 다시 찍어보자.

      console.log(session.data.session?.user);
    
    Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and many not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.
    {
      id: 'b975c250-49e4-4173-a85f-4pp3de7be',
      aud: 'authenticated',
      role: 'authenticated',
      email: 'xhwogusxh@gmail.com',
      email_confirmed_at: '2024-07-21T14:29:18.62845Z',
      phone: '',
      confirmed_at: '2024-07-21T14:29:18.62845Z',
      last_sign_in_at: '2024-07-21T14:54:23.552570689Z',
      app_metadata: { provider: 'github', providers: [ 'github' ] },
      user_metadata: {
        avatar_url: 'https://avatars.githubusercontent.com/u/42367317?v=4',
        email: 'xhwogusxh@gmail.com',
        email_verified: true,
        full_name: '임재현',
        iss: 'https://api.github.com',
        name: '임재현',
        phone_verified: false,
        preferred_username: 'Wunhyeon',
        provider_id: '42367317',
        sub: '42367317',
        user_name: 'Wunhyeon'
      },
      identities: [
        {
          identity_id: '632a0744-6d68-531a-a645-15f1a9120eb8',
          id: '41125317',
          user_id: 'bzxc85z5c250-4ze4-4za73-abc5f-4b5abcda23de7be',
          identity_data: [Object],
          provider: 'github',
          last_sign_in_at: '2024-07-21T14:29:18.620018Z',
          created_at: '2024-07-21T14:29:18.620075Z',
          updated_at: '2024-07-21T14:54:23.24098Z',
          email: 'xhwogusxh@gmail.com'
        }
      ],
      created_at: '2024-07-21T14:29:18.607085Z',
      updated_at: '2024-07-21T14:54:23.554878Z',
      is_anonymous: false
    }

     

     

    그럼 이렇게 구체적인 유저의 정보가 나온다. 그런데 제일 위의 문장을 보면

    Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and many not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.

    이렇게 써져있는데, 위의 세션에서 바로 가져오는 것보다 supabase.auth.getUser()로 가져오라고 말하고 있다.

     

    그런데 동영상에서는 노빠꾸로 그냥 진행하길래, 나는 getUser를 통해 가져오기로 했다. 

    getUser를 통해 가져온 정보들도 크게 차이는 없다. 쪼끔 다르긴한데, 쓸 데이터들의 차이는 없는 것 같다. 그래서 이렇게 해줬다.

    import ChatHeader from "@/components/ChatHeader";
    import { Button } from "@/components/ui/button";
    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">
            <ChatHeader user = {user}/>	// ChatHeader에서 아직 props를 받고있지 않지만 미리 넣어줬다.
          </div>
        </div>
      );
    };
    
    export default page;

     

    이제 ChatHeader.tsx를 고쳐준다.

    "use client";
    
    import React from "react";
    import { Button } from "./ui/button";
    import { createClient } from "@/utils/supabase/client";
    import { User, UserResponse } from "@supabase/supabase-js";
    
    const ChatHeader = ({ user }: { user: User | null }) => {
      // props로 UserResponse를 받는다.
      const handleLoginWithGithub = () => {
        const supabase = createClient();
        supabase.auth.signInWithOAuth({
          provider: "github",
          options: {
            redirectTo: location.origin + "/auth/callback",
          },
        });
      };
    
      return (
        <div className="h-20">
          <div className="p-5 border-b flex items-center justify-between">
            <div>
              <h1 className="text-xl font-bold">Daily Chat</h1>
              <div className="flex items-center gap-1">
                <div className="h-4 w-4 bg-green-500 rounded-full animate-pulse"></div>
                <h1 className="text-sm text-gray-400">2 Online</h1>
              </div>
            </div>
            {user ? ( // user가 있으면 (로그인했으면) 로그아웃 버튼 표시, 없으면 (로그인 안했으면) 로그인 버튼 표시
              <Button onClick={handleLoginWithGithub}>Logout</Button>
            ) : (
              <Button onClick={handleLoginWithGithub}>Login</Button>
            )}
          </div>
        </div>
      );
    };
    
    export default ChatHeader;

     

    이제 사이트에 들어가보면 로그인을 했다면 로그인 버튼이 아니라 로그아웃 버튼이 표시될 것이다.

    로그아웃 버튼을 만들었으니 로그아웃 기능을 만들어보겠다.

    "use client";
    
    import React from "react";
    import { Button } from "./ui/button";
    import { createClient } from "@/utils/supabase/client";
    import { User } from "@supabase/supabase-js";
    import { useRouter } from "next/navigation"; // 주의할 점. next/navigation에서 임포트한다.
    
    const ChatHeader = ({ user }: { user: User | null }) => {
      const router = useRouter();
    
      // props로 UserResponse를 받는다.
      const handleLoginWithGithub = () => {
        const supabase = createClient();
        supabase.auth.signInWithOAuth({
          provider: "github",
          options: {
            redirectTo: location.origin + "/auth/callback",
          },
        });
      };
    
    // 로그아웃
      const handleLogout = async () => {
        const supabase = createClient();
        await supabase.auth.signOut();
        // const { error } = await supabase.auth.signOut();
        router.refresh();
      };
    
      return (
        <div className="h-20">
          <div className="p-5 border-b flex items-center justify-between">
            <div>
              <h1 className="text-xl font-bold">Daily Chat</h1>
              <div className="flex items-center gap-1">
                <div className="h-4 w-4 bg-green-500 rounded-full animate-pulse"></div>
                <h1 className="text-sm text-gray-400">2 Online</h1>
              </div>
            </div>
            {user ? ( // user가 있으면 (로그인했으면) 로그아웃 버튼 표시, 없으면 (로그인 안했으면) 로그인 버튼 표시
              <Button onClick={handleLogout}>Logout</Button>
            ) : (
              <Button onClick={handleLoginWithGithub}>Login</Button>
            )}
          </div>
        </div>
      );
    };
    
    export default ChatHeader;

    로그아웃은 간단하다. 로그아웃을 하고, 리프레시 해준다. 위 코드에서 주석처리 한 부분을 잘 읽어보면 된다.

     

    이제 로그인 하고 로그아웃해보면 로그인버튼으로 바뀌는 걸 볼 수 있다.

     

    ---

    여기까지 한거는 app/page.tsx에서 유저 객체를 받아와서 프롭스로 다른 컴포넌트들에 유저 객체 정보를 뿌려주는 걸 볼 수 있다.

    (app/page.tsx는 서버 컴포넌트이고, ChatHeader.tsx는 클라이언트 컴포넌트이기 때문에, 서버 컴포넌트에서 데이터 패치를 해와서 클라이언트 컴포넌트에 주고 있다.)

    그런데 유저 객체는 상당히 자주 쓰이기 때문에, 이렇게 props로 넘겨주게만 만들면 상당히 번거로울 수 있다. 그래서 props없이 어디서든 유저 객체를 사용할 수 있도록 해줄거다. 

     

    이렇게 해주기 위해 State Management Library인 Zustand를 사용할 거다. 내가 Redux는 사용해보지 않았지만 Redux비슷한 느낌이라고 보면 좋을 것 같다. 

    https://docs.pmnd.rs/zustand/getting-started/introduction

     

    Zustand Documentation

    Zustand is a small, fast and scalable bearbones state-management solution, it has a comfy api based on hooks

    docs.pmnd.rs

     

     

    npm install zustand

     

    설치해준다.

     

    https://docs.pmnd.rs/zustand/guides/typescript

     

    Zustand Documentation

    Zustand is a small, fast and scalable bearbones state-management solution, it has a comfy api based on hooks

    docs.pmnd.rs

    가이드에 기본 사용법이 나와있다.

     

    먼저 lib/store/user.ts 파일을 만들어준다.

    그리고 아래내용을 채워준다.

    import { User } from "@supabase/supabase-js";
    import { create } from "zustand";
    
    interface UserState {
      user: User | null;
    }
    
    export const useUser = create<UserState>()((set) => ({
      user: null, // default state
    }));

     

    또 이어서 lib/store/InitUser.tsx라는 파일을 만들어준다.

    "use client"; // react hook을 사용할 거기 때문에, 클라이언트 컴포넌트로
    import React, { useEffect, useRef } from "react";
    import { useUser } from "./user"; // 위에서 zustand로 만든 상태관리 객체
    import { User } from "@supabase/supabase-js";
    
    const InitUser = ({ user }: { user: User | null }) => {
      // props로 유저 객체를 받아옴.
      const initState = useRef(false); // 초기값
    
      useEffect(() => {
        if (!initState.current) {
          // 초기값이 false라면, state management를 할 수 있도록 user객체를 넣어준다.
          useUser.setState({ user });
        }
    
        initState.current = true;
      }, []);
    
      return <></>;
    };
    
    export default InitUser;

     

    그리고 처음에 아무것도 없는 상태에서는 상태관리를 시작할 수 없기때문에 app/page.tsx에서 user data를 fetch해 온것을 InitUser에 전달해줘서 전역 상태관리가 될 수 있도록 해준다.

    app/page.tsx

    import ChatHeader from "@/components/ChatHeader";
    import { Button } from "@/components/ui/button";
    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">
              <ChatHeader user={user} />
              {/* 이제 zustand를 통해 user의 state management를 하기 때문에 ChatHeader에 user props를 안넘겨줘도 되지만, 굳이 지울 필요도 없으므로 남겨준다. */}
            </div>
          </div>
          <InitUser user={user} />
          {/* user state 관리를 시작할 수 있도록. */}
        </>
      );
    };
    
    export default page;

     

    다음으로 유저 테이블을 만들고, 정보를 자동으로 넣어주게끔 (유저가 가입을 하면 trigger로 넣어주게끔) 하자.

    이전글에서 보았듯이, user가 가입을 하면 (supabase project내의 ) Authentication User에 정보가 들어간다. 

    이를, user table을 만들고, 거기에 자동으로 복사하게끔 할꺼다.

     

    Table Editor로 가서, User 테이블을 만들어준다.

     

    users라는 이름으로 테이블 이름을 정해준다. 또, id를 int에서 uuid로 바꿔준다. 그리고 빨간색 동그라미로 표시한 부분을 눌러 foreign key연결을 해줄거다. 위에서 만들어졌던 auth(Authentication) 스키마의 user 테이블과 연결해줄거다.

     

    이렇게 아이디끼리 연결해주고, 업데이트가 되었을 때는 cascade로 public 스키마의 user도 업데이트가 되도록 했지만, 삭제가 되었을때는 아무것도 안하게 했다. (동영상에서는 둘다 cascade로 했지만, 무작정 삭제해버리는 것 보다는 데이터를 남겨두는 게 좋지않을까 생각했다. 또, 나는 user가 Delete되더라도 물리적으로 삭제하는 게 아니라, is_delete 테이블을 트루로 바꿔서 soft delete 할거기 때문에 )

    그리고 이렇게 칼럼들을 만들어준다. display_name과 avatar_url은 옆의 톱니바퀴 모양을 눌러 is_nullable을 false로 해서, null이 못들어가게 해줬다.

     

    이제 다음으로 auth 스키마에 유저가 생성되면 정보를 받아와 public 스키마의 유저에도 유저가 생성되도록 trigger를 만들어주겠다.

     

    Database -> Functions -> Create New Functions

    위와 같이 입력해준다.

    trigger code는 다음과같다.

    BEGIN
        INSERT INTO public.users (id,display_name,avatar_url)
        VALUES (
          NEW.id,
          new.raw_user_meta_data ->>'user_name',
          new.raw_user_meta_data ->>'avatar_url'
        );
        RETURN NEW;
    END;

    혹시 다른 속성들을 넣었다면 자신에게 맞게 요소를 변경해주면 된다. 

    그리고 밑에 show advanced settings 토글을 켜주고, 생성할때만 이게 실행되도록

    security definer 를 선택해주고 컨펌해준다.

     

    이제 sql_editor로 가서

    create trigger create_user_on_signup after insert on auth.users for each row execute function create_user_on_signup();

     

    이 쿼리를 실행해준다. 혹시 트리거를 생성할때 이름을 다르게했으면 당연히 그 이름에 맞게 해줘야하고.

    그리고  database -> trigger -> auth 를 선택해서 보면, 트리거가 생성된 것을 볼 수 있다.

     

    이제 다시 Auth -> User 로 가서 기존에 생성됬던 유저를 지우고, 다시 가입시켜보쟈.

     

    유저를 삭제하고, localhost:3000 에가서 로그아웃하고 다시 로그인해보자.

    Auth -> User에는 당연히 다시 새롭게 가입이 된걸 볼 수 있고,

     

    그리고 table edito -> users를 선택해보면

     

    짠! 이렇게 유저가 자동으로 public schema에 생성된 것을 볼 수 있다.

     

     

    github : https://github.com/Wunhyeon/Next-Supabase-Chat/tree/3.login2

     

    GitHub - Wunhyeon/Next-Supabase-Chat

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

    github.com

     

    괜찮으시면 깃헙 스타 눌러주세여. 그리고 구직중이니 관심있으시면 xhwogusxh@gmail.com으로 연락주세요~

    댓글

Designed by Tistory.