🔥딱 8일간! 인프런x토스x허먼밀러 역대급 혜택

[인프런 워밍업 클럽 3기 풀스택 ] 4주차 발자국

[인프런 워밍업 클럽 3기 풀스택 ] 4주차 발자국

목차

  • 회원가입 & 로그인

    • Supabase Auth 소개

    • 인증 구현 - Confirmation URL 방식 (이메일 코드 전송)

    • 인증 구현 - 6-digit OTP 방식

       

       

  • 채팅 기능 구현

    • Supabse Realtime 소개

       

    • 실시간 채팅 구현

    • 유저별 마지막 접속 시간 구현

  • 4주차 제출

    • 메시지 삭제 시 '이 메세지는 삭제되었습니다'로 표시


 

1. 회원가입 & 로그인

[Supabase Auth 소개]

  • 다양한 인증 방식을 지원하는 인증 시스템

  • 사용자 인증 및 권한 관리를 웹사이트에서 할 수 있음

  • 이메일 인증, Magic Link를 통한 비밀번호 없는 로그인, 전화번호를 통한 로그인, 소셜 로그인, 그리고 기업용 SSO 등

     

    다양한 방법으로 사용자 인증을 지원

  • 소셜 로그인 방식 지원

  • 전화번호 인증은 Twilio 등의 외부 Provider를 사용하여 진행 가능

  • 인증 방식은 JWT나 Session을 사용하여 설정

  • next.js를 사용하는 경우, 별도의 설정 문서를 통해 Supabase Auth 설정을 진행

[인증 구현 - Confirmation URL 방식]

  • supabase 접속 - Authentication - Emails 페이지에서 이메일로 전송 될 내용과 링크를 설정

     

    • {.confirmationURL} 템플릿을 활용해 인증 링크 설정

       

image

  • [가입하기] 버튼 클릭 시 인증 주소가 담긴 이메일을 보냄

     

  • 메일 전송 후 인증된 주소로 접속 시 가입 확인 완료를 체크함

    • supabase.auth.signup({..})로 인증 코드 주소를 통해 로그인 세션 획득

      • setConfirmationRequired로 가입하기 완료를 체크함

// 가입하기 완료 확인 체크
const [confirmaitonRequired, setConfirmationRequired] = useState(false);
const supabase = createBrowserSupabaseClient();

const signupMutation = useMutation({
    mutationFn: async () => {
      const { data, error } = await supabase.auth.signUp({
        email,
        password,
        options: {
          // 유저가 회원가입 끝나고 이메일 링크 클릭 => 서버처리 완료 => 해당 url로 다시 이동
          emailRedirectTo: "http://loacalhost:3000/signup/confirm",
        },
      }); 
      if (data) {
        setConfirmationRequired(true);
      }
      if (error) {
        alert(error.message);
      }
    },
  });
  • Web Client에서 받은 인증 코드 값을 활용해 로그인 세션을 획득

    • route.ts 에서 code 값이 있으면 supabase.auth.exchangeCodeForSession(code)를 통해 세션 획득

export async function GET(request: Request) {
  const requestUrl = new URL(request.url);
  const code = requestUrl.searchParams.get("code");

  if (code) {
    const supabase = await createServerSupabaseClient();
    await supabase.auth.exchangeCodeForSession(code); // 세션 획득
  }

  return NextResponse.redirect(requestUrl.origin); // 화면 메인페이지로 이동
}
  • 획득한 세션이 존재하면 자동으로 로그인 됨

 

[인증 구현 - 6-digit OTP 방식]

  • supabase 접속 - Authentication - Emails 페이지에서 이메일로 전송 될 내용과 인증 코드 설정

    • {.Token} 템플릿을 활용해 인증 코드 설정

    image

  • [가입하기] 버튼 클릭 시 인증 코드가 담긴 이메일을 보냄

  • 메일 전송 후 인증 코드를 입력하면 가입 확인 완료를 체크함

    • supabase.auth.verifyOtp({...})로 otp token값을 이용해 로그인 세션 획득

  const verityOtpMutation = useMutation({
    mutationFn: async () => {
      const { data, error } = await supabase.auth.verifyOtp({
        type: "signup",
        email,
        token: otp,
      });
      if (data) {
        setConfirmationRequired(true);
      }
      if (error) {
        alert(error.message);
      }
    },
  });
  • 획득한 세션이 존재하면 자동으로 로그인 됨

 

2. 채팅 기능 구현

 [Supabse Realtime 소개]

  • 연결된 클라이언트와 메시지를 주고 받는 실시간 서비스를 제공

  • Broadcast

    • 모든 사용자에게 동일한 데이터를 전송하는 방식

    • 채팅 시스템에서 실시간 메시지 전송, 동시 알림 보낼 때 등에 유용

  • Presence

    • 현재 연결된 사용자를 실시간으로 추적하는 방식

    • 실시간 채팅에서 현재 접속 중인 사용자 표시, 같은 세션에 있는 유저 보여주는 등에 유용

  • Postgres Changes

    • 데이터베이스에서 발생하는 변경 사항을 실시간으로 추적하는 방식

    • 사용자는 DB의 상태 변경을 실시간으로 모니터링, 필요에 따라 즉각 반응 가능 

[실시간 채팅 구현]

  • supabase 페이지 - Table Editor - message DB 클릭

     

  • 오른쪽 상단의 [Realtime off] 버튼을 클릭해 [Realtime on] 으로 변경

  • 채팅으로 전송되는 메시지 DB에 저장하기

    • chatActions.ts

    • 전송하는 메시지 내용과, 유저 id값을 받아 DB에 insert

export async function sendMessage({ message, chatUserId }) {
  const supabase = await createServerSupabaseClient(); // 현재 로그인한 유저

  const {
    data: { session },
    error,
  } = await supabase.auth.getSession();

  if (error || !session.user) {
    throw new Error("User is not authenticated");
  }

  const { data, error: sendMessageError } = await supabase
    .from("message")
    .insert({
      message,
      receiver: chatUserId,
      sender: session.user.id,
    });

  if (sendMessageError) {
    throw new Error(sendMessageError.message);
  }

  return data;
}
  • 채팅 중인 메시지 모두 가져오기

    • chatActions.ts

    • 현재 채팅중인 user ID 값을 받아서 전송되는 메시지 값을 select

export async function getAllMessages({ chatUserId }) {
  // 현재 나와 채팅중인 메시지 모두 가져오기
  const supabase = await createServerSupabaseClient();
  const {
    data: { session },
    error,
  } = await supabase.auth.getSession();

  if (error || !session.user) {
    throw new Error("User is not authenticated");
  }

  const { data, error: getMessagesError } = await supabase
    .from("message")
    .select("*")
    .or(`receiver.eq.${chatUserId},receiver.eq.${session.user.id}`) // 상대방 또는 나
    .or(`sender.eq.${chatUserId},sender.eq.${session.user.id}`) 
    .order("created_at", { ascending: true });

  if (getMessagesError) {
    return [];
  }

  return data;
}
  • 메시지를 전송하고 메시지 창에 표시하기

     

    • ChatScreen.tsx

    • sendMessageMutation: 메시지 DB로 전송 후, 성공하면

      • setMessage(""): 메시지 입력 창 초기화

      • getAllMessagesQuery.refetch(): 메시지 내용 가져와서 보여주기

    • getAllMessagesQuery: 상대 유저 id 값으로 현재 대화중인 메시지 가져오기

const sendMessageMutation = useMutation({
    mutationFn: async () => {
      return sendMessage({
        message,
        chatUserId: selectedUserId,
      });
    },
    onSuccess: () => {
      setMessage(""); // 메시지 입력 칸 비우기
      getAllMessagesQuery.refetch(); // 메시지 가져오기
    },
  });

  const getAllMessagesQuery = useQuery({
    queryKey: ["messages", selectedUserId],
    queryFn: () => getAllMessages({ chatUserId: selectedUserId }),
  });

  // 새로고침 없이 대화를 실시간으로 표시
  useEffect(() => {
    const channel = supabase
      .channel("message_postgres_changes")
      .on(
        "postgres_changes",
        {
          event: "INSERT",
          schema: "public",
          table: "message",
        },
        (payload) => {
          if (payload.eventType === "INSERT" && !payload.errors) {
            getAllMessagesQuery.refetch();
          }
        }
      )
      .subscribe();

    return () => {
      channel.unsubscribe();
    };
  }, []);

 

[유저별 마지막 접속 시간 구현]

  • channel의 config내 key 값(로그인한 유저 id)을 넣어 실제 로그인한 유저의 상태를 트래킹함

  • presence에서 sync 사용

    • channel이 subscribe 되었다면 새로운 precence status 가 들어옴

    • channel.track 안에 트래킹 하고 싶은 오브젝트(현재 날짜)를 넣어 가져오기

  const supabase = createBrowserSupabaseClient();

  useEffect(() => {
    const channel = supabase.channel("online_users", {
      config: {
        presence: {
          key: loggedInUser.id,
        },
      },
    });

    channel.on("presence", { event: "sync" }, () => {
      const newState = channel.presenceState();
      console.log(newState);
    });

    channel.subscribe(async (status) => {
       // 구독이 완료되지 않았을 때 종료
      if (status !== "SUBSCRIBED") {
        return;
      }
      // 구독 성공시 새로운 presence status가 들어옴
      // channel.track안에 트래킹하고 싶은 오브젝트 넣기
      const newPresenceStatus = await channel.track({
        onlineAt: new Date().toISOString(),
      });
    });

    return () => {
      channel.unsubscribe();
    };
  }, []);
  • 가져온 현재 활동 시간을 직접적으로 넣을 수 없어 atoms.ts에서 정의

export const presenceState = atom({
  key: "presenceState",
  default: null,
});
  • 현재 활동 시간 표시하기

    • ChatPeopleList.tsx

    • onlineAt에 presence가 있으면 유저의 onlineAt을 가져옴

  const [presence, setPresence] = useRecoilState(presenceState);
 ...
    channel.on("presence", { event: "sync" }, () => {
      const newState = channel.presenceState();
      // newState를 그냥 쓰면 값을 마음대로 바꿔버릴 수 있기 때문에
      // 다시 파싱하여 오브젝트를 깔끔하게 셋팅해줌
      const newStateObj = JSON.parse(JSON.stringify(newState));
      setPresence(newStateObj);
    });
...
 return (
    <div className="h-screen min-w-60 flex flex-col bg-gray-50">
      {getAllUsersQuery.data?.map((user, index) => (
        <Person
          onClick={() => {
            setSelectedUserId(user.id);
            setSelectedUserIndex(index);
          }}
          index={index}
          isActive={selectedUserId === user.id}
          name={user.email.split("@")[0]}
          onChatScreen={false}
          onlineAt={presence?.[user.id]?.[0]?.onlineAt}
          // onlineAt={new Date().toISOString()}
          userId={user.id}
        />
      ))}

 

 3. 4주차 제출

 [메시지 삭제]

  • 메시지 삭제 시 '이 메세지는 삭제되었습니다'로 표시

  • image

  • 메시지 삭제 쿼리

    • 메시지 변경 및 is_deleted를 true로 변경

export async function updateMessage({ messageId }: { messageId: string }) {
  const supabase = await createBrowserSupabaseClient();

  const {
    data: { session },
    error: authError,
  } = await supabase.auth.getSession();

  if (authError || !session?.user) {
    throw new Error("User is not authenticated");
  }

  const { data, error: updateError } = await supabase
    .from("message")
    .update({ message: "이 메시지는 삭제되었습니다", is_deleted: true }) // 메시지 내용 변경
    .eq("id", messageId);

  if (updateError) {
    throw new Error(updateError.message);
  }

  return data;
}
  • 메시지 삭제시 실시간 업데이트

      const updateMessageMutation = useMutation({
        mutationFn: async ({ messageId }: { messageId: string }) => {
          return updateMessage({ messageId });
        },
        onSuccess: () => {
          // 성공적으로 메시지를 업데이트한 후 메시지 목록을 다시 가져옵니다.
          getAllMessagesQuery.refetch();
        },
      });
    

 

 

 

 

댓글을 작성해보세요.

채널톡 아이콘