안녕하세요?
오늘은 지난 시간부터 알아본 NextAuth 강좌의 연장인데요.
우리는 지금까지 카카오 로그인, 네이버 로그인, 구글 로그인에 대해 알아보았습니다.
1편: 카카오 로그인
https://cpro95.tistory.com/516
2편: 네이버 로그인
https://cpro95.tistory.com/517
3편: 구글 로그인
https://cpro95.tistory.com/518
그런데 한 가지 불만이 있었습니다.
위 그림과 같이 로그인 화면의 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 커스텀 로그인 화면 만들기 강좌를 마치겠습니다.
읽어 주셔서 감사합니다.
'코딩 > React' 카테고리의 다른 글
NextJS Prisma DB로 블로그 시스템 만들기 2편 (0) | 2021.10.08 |
---|---|
NextJS Prisma DB로 블로그 시스템 만들기 (1) | 2021.10.06 |
구글 로그인 구현 React(리액트) Nextjs NextAuth google login (0) | 2021.10.04 |
네이버 로그인 구현 React(리액트) Nextjs NextAuth naver login (0) | 2021.10.04 |
카카오 로그인 구현 React(리액트) Nextjs NextAuth kakao login (1) | 2021.10.04 |