코딩/React

카카오 네이버 구글 커스텀 로그인 화면 만들기 NextAuth React Next

드리프트 2021. 10. 5. 23:40
728x170

 

 

안녕하세요?

 

오늘은 지난 시간부터 알아본 NextAuth 강좌의 연장인데요.

 

우리는 지금까지 카카오 로그인, 네이버 로그인, 구글 로그인에 대해 알아보았습니다.

 

 

1편: 카카오 로그인

https://cpro95.tistory.com/516

 

카카오 로그인 구현 React(리액트) Nextjs NextAuth kakao login

안녕하세요? 지난 시간에는 NextJS와 MongoDB로 유저 로그인 세션 구현하기에 도전해 봤는데요. 최근에는 직접 유저 가입과 그 정보를 DB에 저장하는 거는 굉장히 위험한 일입니다. 그래서 각 대표

cpro95.tistory.com

 

2편: 네이버 로그인

https://cpro95.tistory.com/517

 

네이버 로그인 구현 React(리액트) Nextjs NextAuth naver login

안녕하세요? 지난 시간에는 카카오 로그인에 대해 구현해 봤는데요. 1편. 카카오 로그인 https://cpro95.tistory.com/516 카카오 로그인 구현 React(리액트) Nextjs NextAuth kakao login 안녕하세요? 지난 시간에..

cpro95.tistory.com

 

3편: 구글 로그인

https://cpro95.tistory.com/518

 

구글 로그인 구현 React(리액트) Nextjs NextAuth google login

안녕하세요? 지난 시간에는 네이버 로그인, 카카오 로그인에 대해 구현해 봤는데요. 1편. 카카오 로그인 https://cpro95.tistory.com/516 카카오 로그인 구현 React(리액트) Nextjs NextAuth kakao login 안녕하세..

cpro95.tistory.com

 

 

그런데 한 가지 불만이 있었습니다.

 

 

위 그림과 같이 로그인 화면의 UI가 별로 마음에 들지 않는데요.

 

그래서 NextAuth에서는 커스텀 로그인 페이지를 제작할 수 있게 옵션을 부여해 주고 있습니다.

 

오늘은 그 방법에 대해 알아보겠습니다.

 

 

[...nextauth].ts 설정 편집

 

먼저 NextAuth의 설정 화면인 /pages/api/auth/[...nextauth].ts 파일에서 추가해야 할 부분이 있습니다.

 

아래 코드를 보시면 쉽게 이해할 수 있는데요.

 

import { NextApiHandler } from 'next';
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import Adapters from 'next-auth/adapters';
import prisma from '../../../lib/prisma';

const authHandler: NextApiHandler = (req, res) => NextAuth(req, res, options);
export default authHandler;

const options = {
    providers: [
        Providers.Kakao({
            clientId: process.env.KAKAO_CLIENT_ID,
            clientSecret: process.env.KAKAO_CLIENT_SECRET
        }),
        // Providers.Naver({
        //     clientId: process.env.NAVER_CLIENT_ID,
        //     clientSecret: process.env.NAVER_CLIENT_SECRET
        // }),
        {
            id: "naver",
            name: "Naver",
            type: "oauth",
            version: "2.0",
            params: { grant_type: "authorization_code" },
            protection: ["state"],
            accessTokenUrl: "https://nid.naver.com/oauth2.0/token",
            authorizationUrl:
                "https://nid.naver.com/oauth2.0/authorize?response_type=code",
            profileUrl: "https://openapi.naver.com/v1/nid/me",
            profile(profile: any) {
                return {
                    id: profile.response?.id,
                    name: profile.response?.name,
                    email: profile.response?.email,
                    image: profile.response?.profile_image
                }
            },
            clientId: process.env.NAVER_CLIENT_ID,
            clientSecret: process.env.NAVER_CLIENT_SECRET
        },
        Providers.Google({
            clientId: process.env.GOOGLE_CLIENT_ID,
            clientSecret: process.env.GOOGLE_CLIENT_SECRET
        })
    ],
    adapter: Adapters.Prisma.Adapter({ prisma }),
    pages: {
        signIn: "/signin",
    }
};

 

마지막 부분에 pages라는 옵션을 추가했습니다.

 

즉, NextAuth의 signIn 은 페이지 "/signin"으로 리다이렉트 됩니다.

 

그래서 우리는 /pages/signin.tsx 파일만 만들면 되는 거죠.

 

이 방법이 NextAuth가 커스텀 페이지를 작성하게 해주는 방식인데요.

 

NextAuth가 제공하는 커스텀 페이지는  signIn, signOut, error, verifyRequest, newUser 페이지가 있습니다.

 

나중에 한번 signOut 페이지나 error 페이지도 직접 만들어 보세요.

 

아래와 같이 하시고 /pages 폴더 밑에 "signout.tsx" 파일과 "error.tsx" 파일만 만들면 됩니다.

pages: {
        signIn: "/signin",
        signOut: "/signout",
        error: "/error",
    }

 

오늘은 제일 중요한 signIn 커스텀 페이지만 만들어 보겠습니다.

 

 

/pages/signin.tsx

 

import React from "react";
import type { NextPage, NextPageContext } from "next";
import { getProviders, signIn, getSession } from "next-auth/client";

const SignIn: NextPage = ({ providers }) => {
  return (
    <div>커스텀 로그인</div>
};

export async function getServerSideProps(context: NextPageContext) {
  const { req, res } = context;
  const session = await getSession({ req });

  if (session && res && session.accessToken) {
    res.writeHead(302, {
      Location: "/",
    });
    res.end();
    return;
  }
  return {
    props: {
      providers: await getProviders(),
    },
  };
}

export default SignIn;

 

UI 부분을 보기 전에 서버사이드 쪽 함수를 먼저 살펴보겠습니다.

 

signin 페이지를 구현하기 위해서는 우리가 어떤 프로바이더(Provider)로 로그인 기능을 제공하는지 알아야 하는데요.

 

이 작업은 서버 사이드 쪽에서 이루어져야 합니다.

 

그래서 NextJS의 서버 사이드 함수인 getServerSideProps 함수를 이용합니다.

 

export async function getServerSideProps(context: NextPageContext) {
  const { req, res } = context;
}

 

위 코드가 getServerSideProps 함수의 정석입니다.

 

우리가 보통 다른 강좌를 보면 다음과 같이 쓰는 경우가 많은데요.

export async function getServerSideProps({ req, res }) {
  
}

차이를 아시겠죠? 자바스크립트의 최신 기능인 Destructuring 기능을 이용한 겁니다.

 

두 번째 방법으로 사용하셔도 됩니다.

 

일단 getServerSideProps의 목적은 NextAuth가 우리가 지정한 프로바이더(Provider)를 돌려주는 게 목적입니다.

 

그리고 로그인한 상태일 때는 또 로그인하지 않게 루트 페이지로 이동하는 기능도 다음가 같이 추가했습니다.

 

const session = await getSession({ req });

  if (session && res && session.accessToken) {
    res.writeHead(302, {
      Location: "/",
    });
    res.end();
    return;
  }

 

그리고 마지막으로 getServerSideProps의 props로 해서 getProviders() 함수를 호출해서 Providers를 리턴해 주는 방식입니다.

 

그러면 우리의 UI 쪽 컴포넌트인 SignIn 함수는 {providers} 변수에 Providers 객체를 저장받게 되는데요.

 

그럼 UI 쪽을 살펴보겠습니다.

 

import React, { useEffect, useState } from "react";
import type { NextPage, NextPageContext } from "next";
import { getProviders, signIn, getSession } from "next-auth/client";

import {
  Flex,
  Stack,
  Box,
  Heading,
  Button,
  Text,
  useColorModeValue,
} from "@chakra-ui/react";
import { setPriority } from "os";

const SignIn: NextPage = ({ providers }) => {
  
  return (
    <Flex
      py={12}
      align={"center"}
      justify={"center"}
      bg={useColorModeValue("gray.50", "gray.800")}
    >
      <Stack spacing={8} mx={"auto"} maxW={"lg"} py={1} px={6}>
        <Stack align={"center"}>
          <Heading fontSize={"4xl"} mb={2}>
            로그인
          </Heading>
          <Text
            fontSize={"lg"}
            color={useColorModeValue("gray.600", "gray.50")}
          >
            원하시는 포털 아이디로 로그인 하십시요 ✌️
          </Text>
        </Stack>
        <Box
          rounded={"lg"}
          bg={useColorModeValue("white", "gray.700")}
          boxShadow={"lg"}
          p={8}
        >
          <Stack spacing={10}>
            {Object.values(providers).map((provider) => (
              <div key={provider.name}>
                <Button
                  w="full"
                  colorScheme="facebook"
                  onClick={() => signIn(provider.id)}
                >
                  {provider.name}로 로그인 하기
                </Button>
              </div>
            ))}
          </Stack>
        </Box>
      </Stack>
    </Flex>
  );
};

export async function getServerSideProps(context: NextPageContext) {
  const { req, res } = context;
  const session = await getSession({ req });

  if (session && res && session.accessToken) {
    res.writeHead(302, {
      Location: "/",
    });
    res.end();
    return;
  }
  return {
    props: {
      providers: await getProviders(),
    },
  };
}

export default SignIn;

 

실행 화면을 볼까요?

 

providers 객체를 Array map으로 돌리기 위해 Object.values 함수를 썼습니다.

 

그러나 이렇게 안 해도 됩니다.

 

console.log(providers)를 해볼까요?

 

Object.values로 Array Map 할 필요 없이 우리가 알고 있는 로그인 Provider이기 때문에 다음과 같이 수작업으로 UI를 꾸밀 수 있습니다.

 

        <Stack spacing={10}>
            <Button colorScheme="yellow" onClick={() => signIn(providers.kakao.id)}>
              카카오로 로그인 하기
            </Button>
            <Button colorScheme="green" onClick={() => signIn(providers.naver.id)}>
              네이버로 로그인 하기
            </Button>
            <Button colorScheme="cyan" onClick={() => signIn(providers.google.id)}>
              구글로 로그인 하기
            </Button>
            {/* {Object.values(providers).map((provider) => (
              <div key={provider.name}>
                <Button
                  w="full"
                  colorScheme="facebook"
                  onClick={() => signIn(provider.id)}
                >
                  {provider.name}로 로그인 하기
                </Button>
              </div>
            ))} */}
          </Stack>

실행 화면은 다음과 같습니다.

 

어떤가요?

 

두 번째 방법이 훨씬 쉽고 또 멋지게 UI도 꾸밀 수 있지 않나요?

 

이제, 마무리된 거 같은데요.

 

한 가지 더 공부할 게 있습니다.

 

 

 

Client Side에서 getProviders() 함수 실행

 

NextAuth 문서를 보면 getProviders 함수는 Client Side나 Server Side 두 군데 모두 작동된다고 합니다.

 

그래서 아래와 같이 Client Side에서 useEffect, useState Hook으로 똑같은 동작을 하는 코드를 만들어 보았습니다.

 

import React, { useEffect, useState } from "react";
import type { NextPage, NextPageContext } from "next";
import { useSession, getProviders, signIn, getSession } from "next-auth/client";
import { useRouter } from "next/router";
import {
  Flex,
  Stack,
  Box,
  Heading,
  Button,
  Text,
  useColorModeValue,
} from "@chakra-ui/react";
import { setPriority } from "os";

// const SignIn: NextPage = ({ providers }) => {
const SignIn: NextPage = () => {
  const [providers, setProviders] = useState({});
  const [session, loading] = useSession();
  const router = useRouter();

  useEffect(() => {
    getProviders().then((res) => setProviders(res));
  }, []);

  useEffect(() => {
    if (session) {
      router.push("/");
    }
  }, [session]);

  return (
    <Flex
      py={12}
      align={"center"}
      justify={"center"}
      bg={useColorModeValue("gray.50", "gray.800")}
    >
      <Stack spacing={8} mx={"auto"} maxW={"lg"} py={1} px={6}>
        <Stack align={"center"}>
          <Heading fontSize={"4xl"} mb={2}>
            로그인
          </Heading>
          <Text
            fontSize={"lg"}
            color={useColorModeValue("gray.600", "gray.50")}
          >
            원하시는 포털 아이디로 로그인 하십시요 ✌️
          </Text>
        </Stack>
        <Box
          rounded={"lg"}
          bg={useColorModeValue("white", "gray.700")}
          boxShadow={"lg"}
          p={8}
        >
          <Stack spacing={10}>
            <Button
              colorScheme="yellow"
              onClick={() => signIn(providers.kakao.id)}
            >
              카카오로 로그인 하기
            </Button>
            <Button
              colorScheme="green"
              onClick={() => signIn(providers.naver.id)}
            >
              네이버로 로그인 하기
            </Button>
            <Button
              colorScheme="cyan"
              onClick={() => signIn(providers.google.id)}
            >
              구글로 로그인 하기
            </Button>
            {/* {Object.values(providers).map((provider) => (
              <div key={provider.name}>
                <Button
                  w="full"
                  colorScheme="facebook"
                  onClick={() => signIn(provider.id)}
                >
                  {provider.name}로 로그인 하기
                </Button>
              </div>
            ))} */}
          </Stack>
        </Box>
      </Stack>
    </Flex>
  );
};

// export async function getServerSideProps(context: NextPageContext) {
//   const { req, res } = context;
//   const session = await getSession({ req });

//   if (session && res && session.accessToken) {
//     res.writeHead(302, {
//       Location: "/",
//     });
//     res.end();
//     return;
//   }
//   return {
//     props: {
//       providers: await getProviders(),
//     },
//   };
// }

export default SignIn;

 

두 번째 방식의 핵심은 Client Side에서의 작업인데요.

 

useEffect를 이용해서 session 있을 때는 router.push("/")로 루트 페이지로 돌려보내고,

 

useEffect를 빈 배열을 줘서 컴포넌트가 처음 마운트 될 때 getProviders() 함수를 실행하게끔 했습니다.

 

getProviers() 함수는 Promise를 리턴하기 때문에. then을 써서 setProviders라는 useState 훅으로 providers 스테이트(state)에 저장시켰습니다.

 

<Client Side 쪽 핵심 코드>

const [providers, setProviders] = useState({});
  const [session, loading] = useSession();
  const router = useRouter();

  useEffect(() => {
    getProviders().then((res) => setProviders(res));
  }, []);

  useEffect(() => {
    if (session) {
      router.push("/");
    }
  }, [session]);

 

이렇게 되면 아래쪽 UI 부분은 본질적으로 똑같아지는 거죠.

 

어떤가요?

 

Client Side 쪽이 요즘 브라우저 성능상 훨씬 빠를 겁니다.

 

선택은 여러분의 몫이니까요?

 

그럼, 이만 NextAuth 커스텀 로그인 화면 만들기 강좌를 마치겠습니다.

 

읽어 주셔서 감사합니다.

 

그리드형