Next.js + Supabase 회원가입 & 로그인 구현

이지·2024년 7월 5일

Project

목록 보기
2/9
post-thumbnail

우선 Supabase를 설정해야 한다.

Supabase 설정

user 테이블 설정

테이블 생성

user 테이블에 원하는 속성을 설정해주면 된다.
나의 경우 user_type, name, phone 속성을 추가적으로 설정해주었다.

❗️주의할 점
원래 컬럼 이름을 user_type이 아니라 type으로 설정했었다.
그런데 코드에서는 userType으로 formData를 전달받고 있어 userType으로 컬럼명을 수정했었다. 근데 이때부터 서버 에러(status: 500, code: "unexpected_failure", AuthApiError: Database error saving new user)가 나기 시작하더니 1시간이 넘도록 회원가입이 안돼서 테이블 싹 다 날리고 처음부터 다시 설정했는데도 서버 에러가 해결이 안됐다.
알고보니 내가 컬럼명을 userType으로 바꿔서 생긴 문제였다...😱
에러 메시지를 Supabase의 로그에서 더 자세하게 살펴볼 수 있었는데 column "usertype" of relation "user" does not exist라고 나타났었다. (userType이 아니라 usertype으로 나타나고 있음)

그 이유가 Supabase는 PostgreSQL을 기반으로 하는데 PostgreSQL의 경우 tables, columns, functions 등의 모든 이름을 소문자로 바꾸기 때문이라고 한다.-> 참고
대문자를 쓰고 싶으면 "" 안에 작성하면 되는데 그보단 소문자를 사용하자

콘솔에 찍힌 에러 메시지로는 정확하게 무슨 에러인지 파악하기가 힘든데 Supabase>Logs>Auth에서 더 자세하게 어떤 에러인지 파악할 수 있었다.

관계 설정

id 옆의 🔗를 클릭하거나 아래의 Foreign keys에서 add foreign key relation을 눌러서 아래 사진처럼 설정해준다.
(Authentication의 users 테이블에 관계를 설정하는 단계)

테이블을 설정하고나서 Authentication의 users에서 새로운 유저를 생성해도 앞에서 만든 테이블에 자동으로 생성되지 않는데 아직 함수와 트리거를 설정해주지 않았기 때문이다.

함수, 트리거 설정

함수

create function public.add_new_user () returns trigger as $$
begin
  insert into public.user (id, email, name, user_type, phone)
  values (
    new.id,
    new.email,
    new.raw_user_meta_data->> 'name',
    new.raw_user_meta_data->> 'user_type',
    new.raw_user_meta_data->> 'phone'
  );
  return new;
end;
$$ language plpgsql security definer;

트리거

create trigger on_auth_user_created
after insert on auth.users for each row
execute procedure public.add_new_user ();

함수와 트리거 설정이 제대로 됐는지 확인하려면 Database>Functions or Triggers에서 확인 가능하다. Triggers에서 스키마를 auth로 변경해주면 위에서 설정한 트리거를 확인할 수 있다.

Next.js

회원가입

server action

// actions.ts
"use server";

export async function joinAction(prevState: State, formData: FormData): Promise<State> {
  const userType = formData.get("userType") as UserType;
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;
  const name = formData.get("name") as string;
  const phone = formData.get("phone") as string;
  const supabase = createClient();
  const errorMsg: ErrorMsg = {};

  // form validation
  ...

  const { error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      data: {
        name,
        user_type: userType,
        phone,
      },
    },
  });

  // 서버 에러 처리
  if (error?.code === "user_already_exists") {
    errorMsg.email = ERROR_MESSAGE.emailAlreadyExists;
  }
  ...

  if (Object.keys(errorMsg).length > 0) {
    return { errorMsg };
  }
  return redirect("/login"); // 회원가입에 성공 시 로그인 페이지로 이동
}
  • 서버 에러 처리는 해당 글을 참고해서 작성했다.

page.tsx

"use client";

import { useFormState } from "react-dom";

import { joinAction } from "@/actions/actions";

export default function Join() {
  const [state, formAction] = useFormState(joinAction, {});

  return (
    <form action={formAction}>
      ...
    </form>
  );
}

로그인

server action

export async function loginAction(
  prevState: State,
  formData: FormData
): Promise<loginState> {
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;
  const userType = formData.get("userType") as string;
  const supabase = createClient();
  const errorMsg: ErrorMsg = {};

  // form validation
  ...
  
  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password,
  });
	
  if (error) {
    errorMsg.login = ERROR_MESSAGE.loginInvalid;
  }

  const { data: userData, error: userError } = await supabase
    .from("user")
    .select()
    .eq("id", data.user?.id)
    .single();

  // user DB로부터 데이터를 가져오는 것을 실패했을 때
  if (userError) {
    console.log("userError", userError);
    errorMsg.login = ERROR_MESSAGE.getUserError;
  }

  // form에서 선택된 userType과 userDB로부터 가져온 user_type을 비교
  if (userData?.user_type !== userType) {
    errorMsg.login = ERROR_MESSAGE.loginUserType;
  }

  if (Object.keys(errorMsg).length > 0) {
    return { errorMsg };
  }

  return redirect("/"); // 로그인 성공 시 메인 페이지로 이동
}
  • 로그인의 경우 user_type이 무엇이냐에 따라 로그인 성공 여부가 달라져야 했다.
  • 우선 입력된 이메일, 비밀번호를 가지고 로그인을 시도하고, 반환된 data의 id를 사용해서 user 데이터베이스에서 같은 id를 가진 데이터를 가져와서 선택된 user_type과 userData의 user_type이 다르다면 에러 메시지를 나타나도록 구현했다.
  • ❗️user 테이블에서 데이터를 가져올 때 값이 null이라면 테이블 정책 설정이 제대로 되어있는지 확인하자

page.tsx

"use client";

import { useFormState } from "react-dom";

import { loginAction } from "@/actions/actions";

export default async function Login() {
  const [state, formAction] = useFormState(loginAction, {});

  return (
    <form action={formAction}>
      ...
    </form>
  );
}

OAuth 구현

참고

1개의 댓글

comment-user-thumbnail
2024년 10월 4일

ㅠㅠ 아오 감사합니다..
AuthApiError: Database error saving new user
몇시간째 이거 외않되 스트레스 받다가 이지님 글 읽고 소문자로 테이블이름 바꾸니 되네요 ㅠㅠㅠ

답글 달기