코딩/React

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

드리프트 2022. 1. 7. 16:32
728x170

 

안녕하세요?

 

Supabae 로그인 구현 4편입니다.

 

전편 링크는 아래와 같습니다.

 

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

 

https://cpro95.tistory.com/619

 

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

안녕하세요? 지난 시간에 이어 Supabase 서비스를 이용한 NextJS 로그인 구현을 이어 나가도록 하겠습니다. 1, 2편 링크는 아래를 참조해 주시기 바랍니다. https://cpro95.tistory.com/617 Supabase로 로그인 구.

cpro95.tistory.com

 

3편까지는 Supabase에서 제공하는 supabase.auth.signIn, supabaseauth.signUp 함수를 이용해 유저 가입과 로그인을 구현해 봤습니다.

 

이번 편에서는 프로젝트 확장을 위해 코드를 체계화시켜볼 예정입니다.

 

먼저, Auth 부분과 Message 부분을 리액트의 createContext를 이용해서 어디서든 접근 가능하게 글로벌 State 형식으로 만들 예정인데요.

 

먼저, 기존에 만들었던 useMessage 함수를 리액트 Context를 이용해서 새로 만들겠습니다.

 

리액트의 Context를 사용하는 방법은 다음과 같이 사용하시면 됩니다.

import { createContext } from 'react';

const SomeContext = createContext(null);

export default SomeContext;

리액트에서 제공하는 createContext 함수를 이용해서 어떤 Context를 만들고 그걸 export 하면 됩니다.

 

그러면 필요로하는 곳에서 import 하는 방식으로 사용하면 됩니다.

 

MessageContext를 만들어 보겠습니다.

 

/lib/message 폴더를 만들어 주시고요.

 

그 밑에 MessageContext.tsx 파일을 만듭시다.

 

/lib/message/MessageContext.tsx

import { createContext, FunctionComponent, useState } from "react";
import { MessageProps } from "./message.types";

export type MessageContextProps = {
  ...
};

export const MessageContext = createContext<Partial<MessageContextProps>>({});

export const MessageProvider: FunctionComponent = ({ children }) => {

  return (
    <MessageContext.Provider
      value={{
        ...
        ...
      }}
    >
      {children}
    </MessageContext.Provider>
  );
};

 

리액트의 Context를 만들면 그걸 Provider로 제공해 줘야 하는데요.

 

우리가 만들 MessageProvider는 MessageContext.Provider를 이용해서 value에 지정된 객체를 전달합니다.

 

일단 MessageProps를 기존에 썻던걸 재활용합시다.

 

/lib/message/message.types.ts 파일에 다음과 같이 MessageProps를 정의합시다.

export type MessageType = "default" | "success" | "error";

export type MessageProps = {
    type: MessageType;
    message: string;
};

MessageProps의 항목중에 기존에 우리가 썼던 payload 대신 좀 더 명확한 이름인 message로 바꿨습니다.

 

이제 MessageContext가 전달할 Props에 대한 타입을 정의해 볼까요?

 

Message는 메시지 자체와 그걸 핸들링하는 함수로 지정하면 될 거 같습니다.

 

그래서 MessageContextProps 는 다음과 같이 타입 선언을 합시다.

export type MessageContextProps = {
  messages: MessageProps[];
  handleMessage: (MessageProps) => void;
};

messages는 복수기 때문에 MessageProps의 [] 배열로 선언했고, handleMessage는 MessageProps 객체를 받으면 뭔가 일을 수행하는 함수로 선언했습니다.

 

그다음 createContext로 Context를 만들어야 하는데 타입을 위에서 만든 MessageContextProps를 넣어주면 됩니다.

 

근데 앞에 왜 Partial이란 타입 스크립트의 헬퍼 유틸리티를 이용했냐면요.

 

Partial를 앞에 써주면 그 뒤에 빈 객체를 {} 넣어도 초기화가 되기 때문입니다.

 

그럼 그 다음으로 MessageProvider에서 MessageContext가 제공한 messages와 handleMessage 함수를 만들어 봅시다.

 

export const MessageProvider: FunctionComponent = ({ children }) => {
  const [messages, setMessages] = useState<MessageProps[]>([]);

  const handleMessage = (message: MessageProps) => {
    setMessages((prevMessages) => prevMessages.concat([message]));
    setTimeout(() => {
      setMessages((prevMessages) => prevMessages.slice(1));
    }, 5000);
  };

  return (
    <MessageContext.Provider
      value={{
        messages,
        handleMessage,
      }}
    >
      {children}
    </MessageContext.Provider>
  );
};

messages는 단순히 useState를 이용해서 MessageProps의 배열[]로 선언했습니다.

 

그리고 handleMessage 함수에서는 배열의 concat과 함께 5초마다 그전의 메시지를 삭제하게끔 만들었습니다.

 

그리고 마지막으로 MessageContext.Provider 를 이용해서 value 값에 우리가 만든 messages와 handleMessage 함수를 전달해 줬습니다.

 

이제 MessageContext를 완성했네요.

 

이 MessageContext를 사용하는 방법을 알아볼까요?

import { useContext } from 'react';

import { MessageContext } from '~/lib/message/MessageContext.tsx';

const { messages, handleMessage } = useContext(MessageContext);

위와 같이 리액트의 useContext를 이용해서 해당 Context의 messages와 handleMessage를 디스트럭쳐 링 방식으로 불러오면 됩니다.

 

근데 매번 useContext를 이용하기가 번거롭죠?

 

그래서 useMessage란 리액트 훅을 만들어 보겠습니다.

 

/lib/message/useMessage.tsx

import { useContext } from "react";
import { MessageContext } from "./MessageContext";

export const useMessage = () => {
  const context = useContext(MessageContext);

  if (context === undefined) {
    throw new Error("useMessage must be used within a MessageContext.Provider");
  }

  return context;
};

 

이렇게 하면 이제 다음과 같이 좀더 쉽게 코드를 짜면 됩니다.

import { useMessage } from "../lib/message";

const { messages, handleMessage } = useMessage();

뭔가 좀 간단해진거 같지 않나요?

 

마지막으로 /lib/message/index.ts 파일을 만들어 줍시다.

 

/lib/message/index.ts

export * from './message.types';
export * from './MessageContext';
export * from './useMessage';

 

이제 MessageContextProvider를 우리의 전체 프로젝트에 입혀야 됩니다.

 

_app.tsx파일을 수정해 봅시다.

 

import React from "react";
import Head from "next/head";
import "../styles/globals.css";

import Layout from "../components/Layout";
import { MessageProvider } from "../lib/message";

function MyApp({ Component, pageProps }) {
  return (
    <React.Fragment>
      <Head>
        <meta content="width=device-width, initial-scale=1" name="viewport" />
      </Head>
      
      <MessageProvider>
        
          <Layout>
            <Component {...pageProps} />
          </Layout>
        
      </MessageProvider>
      
    </React.Fragment>
  );
}

export default MyApp;

 

Layout 컴포넌트위에 MessageProvider를 입혔습니다.

 

이렇게 하면 언제든지 useMessage 훅을 이용해서 message 관리가 가능합니다.

 

이제 UI 부분에서 적용해 볼까요?

 

우리가 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 } from "../lib/utils";
import { useMessage } from "../lib/message";

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

type SupabaseAuthPayload = FormFieldProps; // type alias

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

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

  const { messages, handleMessage } = useMessage();
  const [values, handleChange, resetFormFields] =
    useFormFields<FormFieldProps>(FORM_VALUES);

  // sign-up a user with provided details
  const signUp = async (payload: SupabaseAuthPayload) => {
  ...
  ...
  ...
  }
  
  // sign-in a user with provided details
  const signIn = async (payload: SupabaseAuthPayload) => {
  ...
  ...
  ...
  }
}

 

핵심 부분은 아래와 같습니다.

const { messages, handleMessage } = useMessage();

이제 messages가 전역 변수같은 리액트 Context가 되었습니다.

 

그리고 한가지 고칠 게 있는데요. handleMessage를 사용할 때 기존에 payload라고 했던걸 message라고 변경하시면 됩니다.

 

변경 전)
handleMessage({ payload: error.message, type: "error" });

변경 후)
handleMessage({ message: error.message, type: "error" });

 

모든 handleMessage 함수의 payload를 message라고 바꾸시면 됩니다.

 

왜냐하면,  MessageProps에서 message라고 이름지었기 때문입니다.

export type MessageProps = {
    type: MessageType;
    message: string;
};

그리고 UI 부분에서 수정해야할 부분이 있는데요.

 

3편까지는 message가 그냥 텍스트였습니다.

 

근데 이제 messages는 배열입니다. 그래서 messages.map을 이용해서 UI 부분 코드를 바꿔줘야 합니다.

 

message를 보여주는 코드를 아래와 같이 바꿔 주시면 됩니다.

 

{messages &&
        messages.map((message, index) => (
          <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.message}
          </div>
        ))}

messages가 있으면 messages.map을 이용해서 배열을 한 바퀴 돌려줍니다.

 

실행하시면 위와 같이 에러코드같은 messages가 연달아 나옵니다.

 

그리고 5초마다 하나씩 사라지게 됩니다.

 

그리고 마지막으로 /lib/utils.ts 코드에서 Message 관련 부분을 삭제하시면 됩니다.

 

/lib/utils.ts

import { useState } from 'react'

export function useFormFields<T>(
    initialValues: T
): [T, (event: React.ChangeEvent<HTMLInputElement>) => void, () => void] {
    const [values, setValues] = useState<T>(initialValues);
    const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        event.persist();
        const { target } = event;
        const { name, value } = target;
        setValues({ ...values, [name]: value });
    }
    const resetFormFields = () => setValues(initialValues);
    return [values, handleChange, resetFormFields];
}

 

MessageContext 부분은 완성되었습니다.

 

이다음으로 생각해 볼게 Context를 이용한 Auth 관련 부분인데요.

 

signIn, signUp 관련 함수는 언제 어디서든 접근할 수 있게 해야 하기 때문에 전역 변수 같은 리액트 Context로 만들고 관리하는 게 좋을 거 같습니다.

 

먼저, /lib 폴더 밑에 auth 폴더를 만들고 그다음에 /lib/auth/AuthContext.tsx 파일을 만듭시다.

 

/lib/auth/AuthContext.tsx

import { createContext, FunctionComponent, useState } from "react";
import { supabase } from "../supabase";
import { useMessage, MessageProps } from "../message";
import { SupabaseAuthPayload } from "./auth.types";

export type AuthContextProps = {
  ...
};

export const AuthContext = createContext<Partial<AuthContextProps>>({});

export const AuthProvider: FunctionComponent = ({ children }) => {
  

  return (
    <AuthContext.Provider
      value={{
       
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

 

아까 위에서 만들었던 MessageContext와 구조는 같습니다.

 

먼저, 타입스크립트 타입 관련 auth.types.ts 파일을 만듭시다.

 

/lib/auth/auth.types.ts

export type SupabaseAuthPayload = {
    email: string;
    password: string;
};

Auth 부분으로 우리가 사용하는 email과 password부분입니다.

 

이제 우리가 AuthContext로 제공할게 뭔지 생각해 봐야 합니다.

 

프로젝트 어디에서든지 signIn, signUp 을 이용하려는 게 목적이기 때문에 기존에 /pages/auth.tsx 에서 만들었던 signIn, signUp 함수를 여기 AuthContext에 넣기로 합시다.

 

import { createContext, FunctionComponent, useState } from "react";
import { supabase } from "../supabase";
import { useMessage, MessageProps } from "../message";
import { SupabaseAuthPayload } from "./auth.types";

export type AuthContextProps = {
  signUp: (payload: SupabaseAuthPayload) => void;
  signIn: (payload: SupabaseAuthPayload) => void;
  loading: boolean;
};

export const AuthContext = createContext<Partial<AuthContextProps>>({});

export const AuthProvider: FunctionComponent = ({ children }) => {
  

  return (
    <AuthContext.Provider
      value={{
        signUp,
        signIn,
        loading,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

 

위 코드를 보시면 AuthContextProps 설정에서 보시면 signUp, signIn, loading을 설정한 게 보입니다.

 

그리고 AuthContext.Provider에서 그걸 value로 제공해 주고 있습니다.

 

그럼 실제 signUp, signIn 함수를 만들어 봅시다.

 

export const AuthProvider: FunctionComponent = ({ children }) => {
  const [loading, setLoading] = useState(false);
  const { handleMessage } = useMessage();

  // 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({ message: error.message, type: "error" });
      } else {
        handleMessage({
          message:
            "Signup successful. Please check your inbox for a confirmation email!",
          type: "success",
        });
      }
    } catch (error) {
      console.log(error);
      handleMessage({
        message: 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({ message: error.message, type: "error" });
      } else {
        handleMessage({
          message: "Log in successful. I'll redirect you once I'm done",
          type: "success",
        });
      }
    } catch (error) {
      console.log(error);
      handleMessage({
        message: error.error_description || error,
        type: "error",
      });
    } finally {
      setLoading(false);
    }
  };

 

별거 없습니다.

 

우리가 앞에서 /pages/auth.tsx파일에서 썻던 signUp, signIn 함수를 그대로 불러왔습니다.

 

코드를 옮겼다고 보는게 맞을 듯싶습니다.

 

이제 완성되었습니다.

 

만든 김에 useAuth 훅도 만들어 볼까요?

 

/lib/auth/useAuth.tsx

import { useContext } from "react";
import { AuthContext } from "./AuthContext";

export const useAuth = () => {
  const context = useContext(AuthContext);

  if (context === undefined) {
    throw new Error("useAuth must be used within an AuthContext.Provider");
  }

  return context;
};

 

MessageContext의 useMessage와 같습니다.

 

index.ts 파일도 만들어 볼까요?

export * from './auth.types';
export * from './AuthContext';
export * from './useAuth';

 

이제 AuthProvider를 _app.tsx에 입혀야 합니다.

 

/pages/_app.tsx

import React from "react";
import Head from "next/head";
import "../styles/globals.css";

import Layout from "../components/Layout";
import { AuthProvider } from "../lib/auth";
import { MessageProvider } from "../lib/message";

function MyApp({ Component, pageProps }) {
  return (
    <React.Fragment>
      <Head>
        <meta content="width=device-width, initial-scale=1" name="viewport" />
      </Head>
      <MessageProvider>
        <AuthProvider>
          <Layout>
            <Component {...pageProps} />
          </Layout>
        </AuthProvider>
      </MessageProvider>
    </React.Fragment>
  );
}

export default MyApp;

 

이제 auth.tsx 파일을 수정해 볼까요?

 

/pages/auth.tsx

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

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

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

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

  const { loading, signIn, signUp } = useAuth();

  const { messages } = useMessage();

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

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

 

원래 있던 signIn, signUp 함수가 없어지고 loading 이란 State도 없어졌습니다.

 

그리고 그 대신에 아래 코드만 넣었습니다.

 

const { loading, signIn, signUp } = useAuth();

 

오늘은 Context를 이용해서 Message와 Auth 부분을 전역적으로 불러올 수 있는 리액트 훅까지 만들었고 그걸 코드에 직접 넣어봤습니다.

 

좀 더 코드가 체계화되는 거 같아서 기분이 좋네요.

 

 

그리드형