[supabase] Supabase 이메일 로그인/회원가입 with Next.js

진욱·2024년 6월 13일
6

Supabase

목록 보기
1/1
post-thumbnail

나는 프론트엔드 개발자. 계획에 없던 데이터베이스를 공부하여 사용해보고 있지만, 어 이거 생각보다 재밌는 것 같기도 하다.

오늘은 Supabase에서 사용자의 이메일과 비밀번호로 로그인/회원가입 기능을 구현해 보았다. Supabase 쉽다, 사용하기 편리하다는 후기를 많이 보았고, 공식문서, 유튜브와 여러 벨로그 등을 하나하나 다 찾아서 읽어보았지만 생각보다 "아 이거다!" 싶은 정보가 부족했다. 회원 정보가 생기기는 하는데 내 데이터베이스 테이블에는 추가가 되지 않는 등... 그래서 내가 직접 구현 과정을 한 번 정리해보려 한다. 답답해하실 많은 분들께 도움이 될 수 있기를!


1. Supabase란 무엇인가?

들어가기 앞서, Supabase란 무엇인가? 이번에는 용감하게 백엔드 없이 Next.js + TypeScript를 각자 독학한 후 프로젝트를 진행하기로 했고, api 개발부터 데이터베이스 설계까지 우리가 직접 해 보고 있는 중이다. 단기간에 완료해야하는 만큼 가장 중요한 건 학습 시간 대비 사용할 때 어렵지 않은 낮은 데이터베이스를 선택하는 것이었다.

그렇게 후보에 오른 Firebase와 Supabase. 그 중에 우리는 Supabase를 선택했는데, 둘의 차이가 무엇일까?

Supabase vs. Firebase

가장 큰 차이는 Supabase는 오픈소스이며, 관계형 데이터베이스(RDBMS)인 PostgreSQL 기반이라는 것이다.

SupabaseFirebase
소스코드오픈소스비공개
DBPostgresSQL, 500MB 용량NoSQL, 1GiB 용량
사용자 인증MAU 5만명, 소셜 로그인 업체 카카오 포함 19개MAU 5만명, 소셜 로그인 업체 5개
저장소1GB 용량, 월 2GB 대역폭5GB 용량, 일 1GB 대역폭

Supabase의 장점

  • 서버가 필요 없다.
    서버 코드를 작성할 필요 없이 클라이언트에서 데이터베이스에 직접 접근할 수 있다. 보안은 PostgreSQL에서 제공하는 RLS(Row Level Security)를 이용할 수 있지만 우리는 일단 개발 중에는 사용하지 않고 있다.

  • 개발 속도가 빠르다.
    계정 관리/ SNS 로그인 / 이메일 인증 등 복잡한 사용자 인증 작업을 이미 Supabase에서 제공한다.

앞서 말한 오픈 소스 + 관계형 데이터베이스라는 점과 함께 위의 장점들을 보고 우리는 Supabase를 이용해 개발을 진행해 보기로 하였다.


2. Supabase 회원가입 구현

그럼 이제 진짜 회원가입 과정을 소개하려고 한다. 사실 이 글을 읽기에 앞서 Supabase의 공식 문서를 먼저 읽어보는 것을 추천한다.

먼저, Client에서 Supabase Authentication 자체의 meta_data를 직접 활용할 수도 있지만, 이 방법은 보안 상의 문제로 Supabase 공식 문서에서는 추천하지 않는 방법이다. 대신 public 스키마에 user 테이블을 만들고, auth 스키마의 users 테이블을 참조하는 방법을 권장하고 있다. 그 방법에 대해 알아보자.

1) public 스키마에 테이블 생성 (Table Editor 이용)

프로젝트의 Table Editor 탭으로 이동하여 새로운 테이블을 하나 생성한다. 이름은 user라고 하자. 기본적으로 id, created_at column이 존재하는 것을 확인할 수 있는데, 여기서 id column의 데이터 형식을 uuid 로 설정한다. auth 스키마의 users 테이블에 있는 id column과 데이터 형식이 일치해야 이를 참조할 수 있기 때문이다.

그 다음, id 옆의 🔗 버튼을 누르고 테이블이 auth.users을 참조할 수 있게 해 준다.

이메일로 가입할 것이므로 email, 추가적으로 name까지 추가하면 테이블 생성은 끝이다.


2) Auth Setting

Supabase의 Project SettingsAuthentication 탭으로 이동하면 회원가입 시 기본 세팅, 비밀번호 조건 등을 설정할 수 있다. (추가적인 부분은 Pro 업그레이드 필요)


3) Auth Provider 설정

이제 Auth를 제공할 Provider를 설정해야 한다. 어떤 방식으로 로그인할 것인지 결정하는 것이라고 이해하면 된다. AuthenticationProviders 탭으로 이동한다.

기본적으로 이메일 방식이 enabled 된 상태이고, 더 아래쪽을 보면 애플, 구글, 카카오 등의 소셜 로그인도 제공하는 것을 확인할 수 있다! 우리는 우선 이메일을 이용한 로그인/회원가입을 구현할 것이므로 이메일을 선택하면 된다. 아마 자동으로 활성화된 것이 몇 가지 있을텐데, 여기서 한 가지 선택사항이 존재한다.

회원 가입 시 이메일 인증을 필수로 하려면 두 번째 Confirm email 부분을 체크 상태로, 그것이 아니라면 체크 해제해준다는 점이다! 이 항목을 활성화시키면 사용자가 메일에 접속해 verify 하기 전까지 회원가입이 완료되지 않는 것을 의미한다. 처음 만들 때 우리는 기본 설정 그대로 진행했다가, 왜 아무리 가입을 해도 사용자가 생성이 안 되지? 하면서 어리둥절 했다는... 굳이 이메일 인증이 필요하지 않은 경우는 꼭 체크 해제하기!


4) auth.userspublic.user 연동하기

이 부분이 이 글의 🌟핵심🌟.

1) ~ 3)까지 설정하면 Auth 탭에서는 사용자가 잘 추가된 것을 확인할 수 있다.

그런데 다시 public 스키마로 돌아가 내 user 테이블을 확인하면? 아무 데이터도 없다. 그 이유는 현재 회원가입이 완료된 데이터가 auth.users에는 잘 저장되지만, 이것이 public.user에 저장되지 않기 때문이다. 아니 아까 연결했잖아?

이를 해결하기 위해서는 auth.users에 데이터가 추가되었을 때 어떤 동작을 하는 trigger를 추가하고, public.user에서 새로운 데이터를 처리할 수 있는 function을 추가해야 한다. Supabase의 공식 문서에서 소개하고 있는 방법을 따라가보자.

auth.users를 건드리는 것은 Table editor로는 불가능하고, SQL 에디터를 이용해야 한다. 공식문서에 있는 SQL 코드를 우리의 user 테이블에 맞게 살짝 변경하여 다음과 같이 작성하였다.

-- public.user에 function 추가
create function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
begin
  insert into public.user (id, email, name)
  values (new.id, new.email, new.raw_user_meta_data ->> 'name');
  return new;
end;
$$;

-- auth.users에 trigger 추가
create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();

우리는 이 부분만 수정해주면 되는데, 이 부분의 의미를 알아보자.

insert into public.user (id, email, name)
  values (new.id, new.email, new.raw_user_meta_data ->> 'name');
  return new;

public.useruser부분이 데이터를 추가할 테이블이며, 괄호 안에 있는 id, email, name은 아까 테이블에 추가한 column들인 것을 알 수 있을 것이다. new는 우리가 회원가입 시 입력하는 데이터들로, 이메일과 비밀번호 외에 추가적으로 입력하는 데이터들을 new.raw_user_meta_data에 저장하는 것이다. 이를 public.user 테이블의 name column에 저장하게 된다.

"Run" 버튼을 눌러 위 SQL을 실행하면 새로운 사용자를 추가했을 때 자동으로 public.user 테이블에도 데이터가 추가되는 것을 확인할 수 있다!!


5) Next.js 애플리케이션에 추가

이제 다 끝났다. Supabase 데이터베이스와 프로젝트를 연결하는 것은 너무 쉽기 때문이다! 이것이 Supabase의 장점이 아닐까? 데이터베이스에 접근하는 api들을 이미 Supabase에서 제공하기 때문에 그대로 따라만 가면 된다.

(Supabase client 연결은 다음을 참고)

회원가입 파일에서 회원가입 코드를, 로그인 파일에서 로그인 코드를 실행하면 완전 끝이다~

[회원가입]


const { data, error } = await supabase.auth.signUp({
  email: 'example@email.com',
  password: 'example-password',
})

⇒ response

{
  "data": {
    "user": {
      "id": "11111111-1111-1111-1111-111111111111",
      "aud": "authenticated",
      "role": "authenticated",
      "email": "example@email.com",
      "email_confirmed_at": "2024-01-01T00:00:00Z",
      "phone": "",
      "last_sign_in_at": "2024-01-01T00:00:00Z",
      "app_metadata": {
        "provider": "email",
        "providers": [
          "email"
        ]
      },
      "user_metadata": {},
      "identities": [
        {
          "identity_id": "22222222-2222-2222-2222-222222222222",
          "id": "11111111-1111-1111-1111-111111111111",
          "user_id": "11111111-1111-1111-1111-111111111111",
          "identity_data": {
            "email": "example@email.com",
            "email_verified": false,
            "phone_verified": false,
            "sub": "11111111-1111-1111-1111-111111111111"
          },
          "provider": "email",
          "last_sign_in_at": "2024-01-01T00:00:00Z",
          "created_at": "2024-01-01T00:00:00Z",
          "updated_at": "2024-01-01T00:00:00Z",
          "email": "example@email.com"
        }
      ],
      "created_at": "2024-01-01T00:00:00Z",
      "updated_at": "2024-01-01T00:00:00Z"
    },
    "session": {
      "access_token": "<ACCESS_TOKEN>",
      "token_type": "bearer",
      "expires_in": 3600,
      "expires_at": 1700000000,
      "refresh_token": "<REFRESH_TOKEN>",
      "user": {
        "id": "11111111-1111-1111-1111-111111111111",
        "aud": "authenticated",
        "role": "authenticated",
        "email": "example@email.com",
        "email_confirmed_at": "2024-01-01T00:00:00Z",
        "phone": "",
        "last_sign_in_at": "2024-01-01T00:00:00Z",
        "app_metadata": {
          "provider": "email",
          "providers": [
            "email"
          ]
        },
        "user_metadata": {},
        "identities": [
          {
            "identity_id": "22222222-2222-2222-2222-222222222222",
            "id": "11111111-1111-1111-1111-111111111111",
            "user_id": "11111111-1111-1111-1111-111111111111",
            "identity_data": {
              "email": "example@email.com",
              "email_verified": false,
              "phone_verified": false,
              "sub": "11111111-1111-1111-1111-111111111111"
            },
            "provider": "email",
            "last_sign_in_at": "2024-01-01T00:00:00Z",
            "created_at": "2024-01-01T00:00:00Z",
            "updated_at": "2024-01-01T00:00:00Z",
            "email": "example@email.com"
          }
        ],
        "created_at": "2024-01-01T00:00:00Z",
        "updated_at": "2024-01-01T00:00:00Z"
      }
    }
  },
  "error": null
}

[로그인]

const { data, error } = await supabase.auth.signInWithPassword({
  email: 'example@email.com',
  password: 'example-password',
})

⇒ response

{
  "data": {
    "user": {
      "id": "11111111-1111-1111-1111-111111111111",
      "aud": "authenticated",
      "role": "authenticated",
      "email": "example@email.com",
      "email_confirmed_at": "2024-01-01T00:00:00Z",
      "phone": "",
      "last_sign_in_at": "2024-01-01T00:00:00Z",
      "app_metadata": {
        "provider": "email",
        "providers": [
          "email"
        ]
      },
      "user_metadata": {},
      "identities": [
        {
          "identity_id": "22222222-2222-2222-2222-222222222222",
          "id": "11111111-1111-1111-1111-111111111111",
          "user_id": "11111111-1111-1111-1111-111111111111",
          "identity_data": {
            "email": "example@email.com",
            "email_verified": false,
            "phone_verified": false,
            "sub": "11111111-1111-1111-1111-111111111111"
          },
          "provider": "email",
          "last_sign_in_at": "2024-01-01T00:00:00Z",
          "created_at": "2024-01-01T00:00:00Z",
          "updated_at": "2024-01-01T00:00:00Z",
          "email": "example@email.com"
        }
      ],
      "created_at": "2024-01-01T00:00:00Z",
      "updated_at": "2024-01-01T00:00:00Z"
    },
    "session": {
      "access_token": "<ACCESS_TOKEN>",
      "token_type": "bearer",
      "expires_in": 3600,
      "expires_at": 1700000000,
      "refresh_token": "<REFRESH_TOKEN>",
      "user": {
        "id": "11111111-1111-1111-1111-111111111111",
        "aud": "authenticated",
        "role": "authenticated",
        "email": "example@email.com",
        "email_confirmed_at": "2024-01-01T00:00:00Z",
        "phone": "",
        "last_sign_in_at": "2024-01-01T00:00:00Z",
        "app_metadata": {
          "provider": "email",
          "providers": [
            "email"
          ]
        },
        "user_metadata": {},
        "identities": [
          {
            "identity_id": "22222222-2222-2222-2222-222222222222",
            "id": "11111111-1111-1111-1111-111111111111",
            "user_id": "11111111-1111-1111-1111-111111111111",
            "identity_data": {
              "email": "example@email.com",
              "email_verified": false,
              "phone_verified": false,
              "sub": "11111111-1111-1111-1111-111111111111"
            },
            "provider": "email",
            "last_sign_in_at": "2024-01-01T00:00:00Z",
            "created_at": "2024-01-01T00:00:00Z",
            "updated_at": "2024-01-01T00:00:00Z",
            "email": "example@email.com"
          }
        ],
        "created_at": "2024-01-01T00:00:00Z",
        "updated_at": "2024-01-01T00:00:00Z"
      }
    }
  },
  "error": null
}

로그인이 완료되면 위 데이터들은 Local Storage에 저장되어, 정보를 사용할 수 있게 된다!


백엔드없이 api까지 내가 만들 수 있을까 정말 걱정을 많이 했고 지금도 하고는 있지만, 그래도 Supabase의 편리한 기능과 공식 문서들 덕분에 어찌저찌 프로젝트가 잘 진행되고 있는 것 같다. 아까도 말했지만 모르는 내용을 찾아내고 직접 적용해보는 것이 아주 흥미롭다 😄

다음에는 Supabase에서 데이터 fetching 하는 내용을 한 번 다뤄볼까 한다. 그럼 다들 즐거운 개발 되시길!

0개의 댓글

관련 채용 정보