코딩/React

Supabase로 로그인 구현하기 with NextJS 3편

드리프트 2022. 1. 6. 22:16
728x170

안녕하세요?

 

지난 시간에 이어 Supabase 서비스를 이용한 NextJS 로그인 구현을 이어 나가도록 하겠습니다.

 

1, 2편 링크는 아래를 참조해 주시기 바랍니다.

 

https://cpro95.tistory.com/617

 

Supabase로 로그인 구현하기 with NextJS 1편

안녕하세요? 최근 NextJS로 여러가지 로그인 구현 웹앱을 만들려고 노력하고 있는데요. 최근에는 NextAuth와 Prisma를 이용해서 카카오 로그인, 네이버 로그인, 구글 로그인 등 다방면으로 구현해 봤

cpro95.tistory.com

 

 

https://cpro95.tistory.com/618

 

Supabase로 로그인 구현하기 with NextJS 2편

안녕하세요? 이번 시간에는 지난 회부터 시작한 Supabase를 이용한 로그인 구현 2편을 이어나가도록 하겠습니다. 1편은 아래 링크 참조바랍니다. https://cpro95.tistory.com/617 Supabase로 로그인 구현하기 w

cpro95.tistory.com

 

3편에서는 2편에서 끝낸 Sign Up에 이어 Log In 부분을 작성토록 하겠습니다.

 

사실 3편의 Log In 부분은 Sign Up 부분의 Copy & Paste 수준입니다.

 

그래도 코드를 보여드려야 하니까 일단 진행하겠습니다.

 

/pages/login.tsx

import React, { useState } from "react";
import { FaLockOpen } from "react-icons/fa";
import { supabase } from "../lib/supabase";
import classNames from "classnames";
import { useFormFields, MessageProps, useMessage } from "../lib/utils";

type SignInFieldProps = {
  email: string;
  password: string;
};

type SupabaseSigninPayload = SignInFieldProps; // type alias

const FORM_VALUES: SignInFieldProps = {
  email: "",
  password: "",
};

const MESSAGE_VALUES: MessageProps = {
  type: "default",
  payload: "",
};

const Login: React.FC = (props) => {
....
....
....
....
};

export default Login;

Login 컴포넌트 전에 기존의 타입스크립트 타입 정의를 새로운 이름으로 다시 했습니다.

 

const Login: React.FC = (props) => {
  const [loading, setLoading] = useState(false);

  const [values, handleChange, resetFormFields] =
    useFormFields<SignInFieldProps>(FORM_VALUES);

  const [message, handleMessage] = useMessage<MessageProps>(MESSAGE_VALUES);

  // sign-in a user with provided details
  const signIn = async (payload: SupabaseSigninPayload) => {
    try {
      setLoading(true);
      const { error } = await supabase.auth.signIn(payload);
      if (error) {
        console.log(error);
        handleMessage({ payload: error.message, type: "error" });
      } else {
        handleMessage({
          payload: "Log in successful. I'll redirect you once I'm done",
          type: "success",
        });
      }
    } catch (error) {
      console.log(error);
      handleMessage({
        payload: error.error_description || error,
        type: "error",
      });
    } finally {
      setLoading(false);
    }
  };

  // Form submit handler to call the above function
  const handleSumbit = (event: React.FormEvent) => {
    event.preventDefault();
    signIn(values);
    resetFormFields();
  };

  return (
  ...
  ...
  ...
    );
};

 

Supabase에서 Log in 함수는 Sign Up 함수와 마찬가지 형태를 띱니다.

 

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

 

Log In 함수는 영어로는 signIn이라서 함수 이름도 supabase.auth.signIn을 사용합니다.

 

우리는 한국 사람이라서 그런지 Sign In 보다는 Log In 용어가 더 익숙해서 Log In 을 쓰는 게 좋을 듯싶습니다.

 

signIn 함수도 signUp 함수와 마찬가지로 email과 password를 인수로 보내면 user, session, error 객체를 리턴합니다.

 

만약에 signIn 함수가 성공하면 user와 session 객체가 리턴되는데 supabase에서는 이 user와 session 객체를 어떻게 정의했는지 참고 문서를 한번 보겠습니다.

 

interface User {
    id: string;
    app_metadata: /* retracted */;
    user_metadata: {
        /* non-frequent user related values */
    };
    aud: string;
    confirmation_sent_at?: string;
    email?: string;
    created_at: string;
    confirmed_at?: string;
    last_sign_in_at?: string;
    role?: string;
    updated_at?: string;
}
interface Session {
    provider_token?: string | null;
    access_token: string;
    expires_in: number;
    refresh_token: string;
    token_type: string;
    user: User;
}

 

타입스크립트의 인터페이스로 정의되어 있습니다.

 

그래서 나중에 우리는 User와 Session 객체를 이용해서 로그인한 유저의 정보에 접근할 수 있습니다.

 

이 부분은 다음 시간에 더 알아보겠습니다.

 

원론으로 돌아와서 우리의 signIn 함수도 2편의 signUp 함수와 99% 동일합니다.

 

그럼 UI 부분으로 들어가 볼까요?

 

return (
    <div className="container px-5 py-10 mx-auto w-2/3">
      <div className="w-full text-center mb-4 flex  flex-col place-items-center">
        <FaLockOpen className="w-6 h-6" />
        <h1 className="text-2xl md:text-4xl text-gray-700 font-semibold">
          Log In
        </h1>
      </div>
      {message.payload && (
        <div
          className={classNames(
            "shadow-md rounded px-3 py-2 text-shadow transition-all mt-2 text-center",
            message.type === "error"
              ? "bg-red-500 text-white"
              : message.type === "success"
              ? "bg-green-300 text-gray-800"
              : "bg-gray-100 text-gray-800"
          )}
        >
          {message?.payload}
        </div>
      )}
      <form
        onSubmit={handleSumbit}
        className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
      >
        <div className="mb-4">
          <label
            className="block text-gray-700 text-sm font-bold mb-2"
            htmlFor="email"
          >
            Email
          </label>
          <input
            className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
            id="email"
            name="email"
            type="email"
            placeholder="Your Email"
            required
            value={values.email}
            onChange={handleChange}
          />
        </div>
        <div className="mb-6">
          <label
            className="block text-gray-700 text-sm font-bold mb-2"
            htmlFor="password"
          >
            Password
          </label>
          <input
            className="shadow appearance-none border border-red-500 rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
            id="password"
            name="password"
            type="password"
            placeholder="Your password"
            required
            value={values.password}
            onChange={handleChange}
          />
        </div>
        <div className="flex items-center justify-between">
          <button
            className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
            type="submit"
          >
            Log In
          </button>
        </div>
      </form>
      {loading && (
        <div className="shadow-md rounded px-3 py-2 text-shadow transition-all mt-2 text-center">
          Loading...
        </div>
      )}
    </div>
  );

 

UI 부분은 정말 아이콘 모양만 바꿨습니다.

 

FaLock에서 FaLockOpen으로요.

 

이렇게 코드 재활용이 가능한 이유는 우리가 2편에서 만들었던 만능 리액트 훅 때문인데요.

 

바로 useFormFields 훅과 useMessage 훅입니다.

 

잘 만들어 논 리액트 훅이 노가다를 줄여주네요.

 

실행 결과를 볼까요?

 

 

실행해 볼까요?

 

일부러 잘못된 정보를 이용해서 로그인 시도를 하면 아래와 같이 에러 메시지가 리턴됩니다.

이번에는 제대로 된 정보로 로그인해볼까요?

 

Log In 이 성공했다고 나오네요.

 

3편 Log In 부분은 정말 간단하게 끝났는데요.

 

바로 2편에서 만든 리액트 훅 때문입니다.

 

UI 부분이 문구만 틀리고 같다면 아마 Sign Up 컴포넌트와 Log In 컴포넌트를 합쳐서 한 개로도 가능할 거 같습니다.

 

파일 이름은 /pages/auth.tsx로 하겠습니다.

 

/pages/auth.tsx

import React, { useState, useRef } from "react";
import { FaLock, FaLockOpen } from "react-icons/fa";
import { supabase } from "../lib/supabase";
import classNames from "classnames";
import { useFormFields, MessageProps, useMessage } from "../lib/utils";

type FormFieldProps = {
  email: string;
  password: string;
};

type SupabaseAuthPayload = FormFieldProps; // type alias

const FORM_VALUES: FormFieldProps = {
  email: "",
  password: "",
};

const MESSAGE_VALUES: MessageProps = {
  type: "default",
  payload: "",
};

const Auth: React.FC = (props) => {
...
...
...
};

export default Auth;

타입스크립트의 타입 이름도 FormFieldProps, 그리고 SupabaseAuthPayload로 바꿨습니다.

 

const Auth: React.FC = (props) => {
  const [isSignIn, setIsSignIn] = useState(true);

그리고 Auth 컴포넌트의 첫 번째 리액트 스테이트로 isSignIn을 설정했는데요.

 

isSignIn 이 true이면 로그인하려는 상황인 거고, false이면 가입하려는 상황이라는 뜻이죠.

 

좀 더 코드를 이어 나가 볼까요?

 

const Auth: React.FC = (props) => {
  const [isSignIn, setIsSignIn] = useState(true);
  const [loading, setLoading] = useState(false);

  const [values, handleChange, resetFormFields] =
    useFormFields<FormFieldProps>(FORM_VALUES);

  const [message, handleMessage] = useMessage<MessageProps>(MESSAGE_VALUES);

  // sign-up a user with provided details
  const signUp = async (payload: SupabaseAuthPayload) => {
    try {
      setLoading(true);
      const { error } = await supabase.auth.signUp(payload);
      if (error) {
        console.log(error);
        handleMessage({ payload: error.message, type: "error" });
      } else {
        handleMessage({
          payload:
            "Signup successful. Please check your inbox for a confirmation email!",
          type: "success",
        });
      }
    } catch (error) {
      console.log(error);
      handleMessage({
        payload: error.error_description || error,
        type: "error",
      });
    } finally {
      setLoading(false);
    }
  };

  // sign-in a user with provided details
  const signIn = async (payload: SupabaseAuthPayload) => {
    try {
      setLoading(true);
      const { error } = await supabase.auth.signIn(payload);
      if (error) {
        console.log(error);
        handleMessage({ payload: error.message, type: "error" });
      } else {
        handleMessage({
          payload: "Log in successful. I'll redirect you once I'm done",
          type: "success",
        });
      }
    } catch (error) {
      console.log(error);
      handleMessage({
        payload: error.error_description || error,
        type: "error",
      });
    } finally {
      setLoading(false);
    }
  };

  // Form submit handler to call the above function
  const handleSumbit = (event: React.FormEvent) => {
    event.preventDefault();
    isSignIn ? signIn(values) : signUp(values);
    resetFormFields();
  };

  return (

 

signIn 함수와 signUp 함수는 연달아 작성해 놓고요.

 

handleSubmit 함수에서 isSignIn 리액트 스테이트에 따라서 함수 호출을 달리합니다.

 

마지막으로 UI 부분을 볼까요?

 

return (
    <div className="container px-5 py-10 mx-auto w-2/3">
      <div className="w-full text-center mb-4 flex  flex-col place-items-center">
        {isSignIn ? (
          <FaLockOpen className="w-6 h-6" />
        ) : (
          <FaLock className="w-6 h-6" />
        )}
        <h1 className="text-2xl md:text-4xl text-gray-700 font-semibold">
          {isSignIn ? "Log In" : "Sign Up"}
        </h1>
      </div>
      {message.payload && (
        <div
          className={classNames(
            "shadow-md rounded px-3 py-2 text-shadow transition-all mt-2 text-center",
            message.type === "error"
              ? "bg-red-500 text-white"
              : message.type === "success"
              ? "bg-green-300 text-gray-800"
              : "bg-gray-100 text-gray-800"
          )}
        >
          {message?.payload}
        </div>
      )}
      <form
        onSubmit={handleSumbit}
        className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
      >
        <div className="mb-4">
          <label
            className="block text-gray-700 text-sm font-bold mb-2"
            htmlFor="email"
          >
            Email
          </label>
          <input
            className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
            id="email"
            name="email"
            type="email"
            placeholder="Your Email"
            required
            value={values.email}
            onChange={handleChange}
          />
        </div>
        <div className="mb-6">
          <label
            className="block text-gray-700 text-sm font-bold mb-2"
            htmlFor="password"
          >
            Password
          </label>
          <input
            className="shadow appearance-none border border-red-500 rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
            id="password"
            name="password"
            type="password"
            placeholder="Your password"
            required
            value={values.password}
            onChange={handleChange}
          />
        </div>
        <div className="flex gap-2">
          <button
            className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
            type="submit"
          >
            {isSignIn ? "Log In" : "Sign Up"}
          </button>
          <div className="flex-1 text-right">
            <small className="block text-gray-600">
              {isSignIn ? "Not a member yet?" : "Already a member?"}{" "}
            </small>
            <a
              className="block font-semibold"
              href=""
              onClick={(e) => {
                e.preventDefault();
                setIsSignIn(!isSignIn);
              }}
            >
              {isSignIn ? "Sign Up" : "Log In"}
            </a>
          </div>
        </div>
      </form>
      {loading && (
        <div className="shadow-md rounded px-3 py-2 text-shadow transition-all mt-2 text-center">
          Loading...
        </div>
      )}
    </div>
  );

UI의 핵심 부분은 오른쪽 아래에 보시면 "Already a member? Log In"입니다.

 

이걸 누르면 isSignIn 리액트 스테이트가 변해서 로그인이 되느냐 가입하기가 되느냐로 바뀝니다.

 

UI 부분은 쉽게 이해하실 수 있을 거 같아 설명 없이 지나가겠습니다.

 

그리고 아래는 auth.tsx의 전체 코드입니다.

 

/pages/auth.tsx

import React, { useState, useRef } from "react";
import { FaLock, FaLockOpen } from "react-icons/fa";
import { supabase } from "../lib/supabase";
import classNames from "classnames";
import { useFormFields, MessageProps, useMessage } from "../lib/utils";

type FormFieldProps = {
  email: string;
  password: string;
};

type SupabaseAuthPayload = FormFieldProps; // type alias

const FORM_VALUES: FormFieldProps = {
  email: "",
  password: "",
};

const MESSAGE_VALUES: MessageProps = {
  type: "default",
  payload: "",
};

const Auth: React.FC = (props) => {
  const [isSignIn, setIsSignIn] = useState(true);
  const [loading, setLoading] = useState(false);

  const [values, handleChange, resetFormFields] =
    useFormFields<FormFieldProps>(FORM_VALUES);

  const [message, handleMessage] = useMessage<MessageProps>(MESSAGE_VALUES);

  // sign-up a user with provided details
  const signUp = async (payload: SupabaseAuthPayload) => {
    try {
      setLoading(true);
      const { error } = await supabase.auth.signUp(payload);
      if (error) {
        console.log(error);
        handleMessage({ payload: error.message, type: "error" });
      } else {
        handleMessage({
          payload:
            "Signup successful. Please check your inbox for a confirmation email!",
          type: "success",
        });
      }
    } catch (error) {
      console.log(error);
      handleMessage({
        payload: error.error_description || error,
        type: "error",
      });
    } finally {
      setLoading(false);
    }
  };

  // sign-in a user with provided details
  const signIn = async (payload: SupabaseAuthPayload) => {
    try {
      setLoading(true);
      const { error } = await supabase.auth.signIn(payload);
      if (error) {
        console.log(error);
        handleMessage({ payload: error.message, type: "error" });
      } else {
        handleMessage({
          payload: "Log in successful. I'll redirect you once I'm done",
          type: "success",
        });
      }
    } catch (error) {
      console.log(error);
      handleMessage({
        payload: error.error_description || error,
        type: "error",
      });
    } finally {
      setLoading(false);
    }
  };

  // Form submit handler to call the above function
  const handleSumbit = (event: React.FormEvent) => {
    event.preventDefault();
    isSignIn ? signIn(values) : signUp(values);
    resetFormFields();
  };

  return (
    <div className="container px-5 py-10 mx-auto w-2/3">
      <div className="w-full text-center mb-4 flex  flex-col place-items-center">
        {isSignIn ? (
          <FaLockOpen className="w-6 h-6" />
        ) : (
          <FaLock className="w-6 h-6" />
        )}
        <h1 className="text-2xl md:text-4xl text-gray-700 font-semibold">
          {isSignIn ? "Log In" : "Sign Up"}
        </h1>
      </div>
      {message.payload && (
        <div
          className={classNames(
            "shadow-md rounded px-3 py-2 text-shadow transition-all mt-2 text-center",
            message.type === "error"
              ? "bg-red-500 text-white"
              : message.type === "success"
              ? "bg-green-300 text-gray-800"
              : "bg-gray-100 text-gray-800"
          )}
        >
          {message?.payload}
        </div>
      )}
      <form
        onSubmit={handleSumbit}
        className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
      >
        <div className="mb-4">
          <label
            className="block text-gray-700 text-sm font-bold mb-2"
            htmlFor="email"
          >
            Email
          </label>
          <input
            className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
            id="email"
            name="email"
            type="email"
            placeholder="Your Email"
            required
            value={values.email}
            onChange={handleChange}
          />
        </div>
        <div className="mb-6">
          <label
            className="block text-gray-700 text-sm font-bold mb-2"
            htmlFor="password"
          >
            Password
          </label>
          <input
            className="shadow appearance-none border border-red-500 rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
            id="password"
            name="password"
            type="password"
            placeholder="Your password"
            required
            value={values.password}
            onChange={handleChange}
          />
        </div>
        <div className="flex gap-2">
          <button
            className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
            type="submit"
          >
            {isSignIn ? "Log In" : "Sign Up"}
          </button>
          <div className="flex-1 text-right">
            <small className="block text-gray-600">
              {isSignIn ? "Not a member yet?" : "Already a member?"}{" "}
            </small>
            <a
              className="block font-semibold"
              href=""
              onClick={(e) => {
                e.preventDefault();
                setIsSignIn(!isSignIn);
              }}
            >
              {isSignIn ? "Sign Up" : "Log In"}
            </a>
          </div>
        </div>
      </form>
      {loading && (
        <div className="shadow-md rounded px-3 py-2 text-shadow transition-all mt-2 text-center">
          Loading...
        </div>
      )}
    </div>
  );
};

export default Auth;

 

3편은 여기까지 하겠습니다.

 

 

그리드형