코딩/React

NextAuth.js로 NextJS앱에 로그인 로직 만들기

드리프트 2021. 12. 26. 17:15
728x170

 

 

안녕하세요?

 

지난 블로그를 보시면 NextAuth를 이용해서 카카오, 네이버, 구글 로그인 구현에 대한 글이 있는데요.

 

오늘은 카카오 로그인, 네이버 로그인, 구글 로그인 없이 그냥 바닥에서부터 유저 이메일과 패스워드로만 로그인하는 방식을 구현해 보려고 합니다.

 

먼저, 지난 시간에 만들었던 NextJS + Typescript + TailwindCSS 템플릿을 이용할 건데요.

 

https://cpro95.tistory.com/610

 

NextJS + Typescript + TailwindCSS 적용 2편

안녕하세요? 지난 블로그에서 NextJS와 Typescript 그리고 Tailwind CSS까지 적용된 템플릿을 만들었는데요. 이번 시간에는 거기에 추가해서 Tailwind CSS를 이용한 모바일 적용되는 반응형 템플릿을 만들

cpro95.tistory.com

 

 

1. NextAuth 설치

 

오늘 우리가 사용할 로그인 구현 라이브러리는 NextAuth입니다.

 

NextJS에서 아주 쉽게 로그인을 구현할 수 있게 도와주는 라이브러리인데요.

 

일단 다음과 같이 설치하시면 됩니다.

 

npm i next-auth

or

yarn add next-auth

 

package.json 파일을 보시면 next-auth 버전이 4.0을 넘어섰습니다.

 

최근에 V4로 NextAuth 자체가 큰 폭의 업데이트가 있었습니다.

 

지난 카카오 로그인 블로그를 보시면 그때의 next-auth는 버전이 3이었거든요.

 

최근에 버전 4를 이용해서 카카오 로그인을 해보니까 에러가 발행하더라고요.

(원인 파악 중에 있고 성공하면 블로그 올리겠습니다.)

 

그래서 일단은 유저 이메일과 패스워드로 로그인 구현을 해볼 건데요.

 

NextAuth에서는 이 방식을 Credentials이라고 합니다.

 

 

username / password 방식인데요.

 

간단한 사이트 제작과 익명성을 원할 때 아주 유용한 방식입니다.

 

 

2. [... nextauth]. ts 파일 설정하기

 

본격적으로 NextAuth의 Credentials 설정을 해야 하는데요.

 

/pages/api/auth 폴더에 [... nextauth].ts 파일을 다음과 같이 만듭시다.

 

/pages/api/auth/[... nextauth].ts

import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";


export default NextAuth({
    providers: [
        CredentialsProvider({
            // The name to display on the sign in form (e.g. "Sign in with...")
            name: "Credentials",
            // The credentials is used to generate a suitable form on the sign in page.
            // You can specify whatever fields you are expecting to be submitted.
            // e.g. domain, username, password, 2FA token, etc.
            // You can pass any HTML attribute to the <input> tag through the object.
            credentials: {
                username: { label: "Username", type: "text", placeholder: "jsmith" },
                password: { label: "Password", type: "password" }
            },
            async authorize(credentials, req) {
                // Add logic here to look up the user from the credentials supplied
                const user = { id: 1, name: "J Smith", email: "jsmith@example.com" }

                if (user) {
                    // Any object returned will be saved in `user` property of the JWT
                    return user
                } else {
                    // If you return null or false then the credentials will be rejected
                    return null
                    // You can also Reject this callback with an Error or with a URL:
                    // throw new Error("error message") // Redirect to error page
                    // throw "/path/to/redirect"        // Redirect to a URL
                }
            }
        })
    ]
})

 

NextAuth 홈페이지에 나와 있는 예제 파일인데요.

 

일단 이걸 보면서 설명해 드리겠습니다.

 

NextAuth는 Provider를 제공하는데요.

 

카카오 로그인, 네이버 로그인, 구글 로그인 자체가 각각 Provider입니다.

 

우리는 유저네임과 패스워드를 이용하려고 하는데요.

 

이 방식이 바로 Credentials이라고 NextAuth에서는 이름 지었고 그게 바로 CredentialsProvider입니다.

 

[... nextauth]. ts 파일은 NextAuth 설정 파일인데요.

 

여기서 우리가 제공하는 Provider가 여러 개가 있을 수 있는데요.

 

우리는 그냥 CredentialsProvider만 입력했습니다.

 

일단 위와 같이 NextAuth 홈페이지에서 제공하는 기본적인 코드만 넣고 실행해 볼까요?

 

npm run dev

or

yarn dev

실행해 보면 우리의 NextJS + Typescript + TailwindCSS 템플릿 외에는 아무런 추가 동작도 보이지 않는데요.

 

왜냐하면 NextAuth는 NextJS의 서버사이드 API를 제공하기 때문입니다.

 

일단 다음 주소로 강제로 이동해 볼까요?

 

http://localhost:3000/api/auth/signin

 

위 주소로 강제로 이동하니까 바로 위 그림처럼 NextAuth가 기본적으로 제공하는 로그인 화면이 나왔습니다.

 

참고로 위 방식의 로그인 화면이 마음에 들지 않는다면 커스텀 로그인 페이지를 만들 수 있는데요.

 

제 지난 블로그를 읽어 보시면 됩니다.

 

참고로 yarn dev를 실행했던 터미널 창에는 next-auth 가 경고를 하고 있는데요.

 

바로 NO_SECRET 경고입니다.

 

 

지금은 중요하지 않을 수 있지만 경고가 보이면 눈에 거슬리니까 다음과 같이. env 파일을 만들어서 다음 항목을 추가해 볼까요?

 

SECRET=mysecretofnextjsnextauth

NEXTAUTH_URL=http://localhost:3000

SECRET는 아무 문자열을 써넣으면 됩니다.

 

그리고 NEXTAUTH_URL은 우리가 개발 서버로 돌리는 주소를 넣어주면 됩니다.

 

그리고 [... nextauth].ts 파일에 다음과 같이 추가하시면 됩니다.

 

[...nextauth].ts

import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";


export default NextAuth({
    providers: [
        CredentialsProvider({
            .....
            .....
            .....
            .....
            .....
            
    ],
    secret: process.env.SECRET,
})

이제 다시 yarn dev 해볼까요?

 

이제 nextauth warn no_secret 문구가 안 보일 겁니다.

 

 

3. 로그인해보기

 

우리가 NextAuth CredentialsProvider로 지정한 내용을 보면 다음과 같이 부분이 있습니다.

 

            name: "Credentials",
            
            credentials: {
                username: { label: "Username", type: "text", placeholder: "jsmith" },
                password: { label: "Password", type: "password" }
            },

 

여기서 보시면 name 부분은 로그인 UI의 이름인데요.

 

이름을 바꿔 보시면 아래 그림처럼 바뀝니다.

 

 

그리고 두 번째 credentials 부분을 볼까요?

 

이 부분은 그냥 HTML의 form 같은 부분입니다.

 

여기도 한글로 바꿔 볼까요?

 

credentials: {
                username: { label: "유저네임", type: "text", placeholder: "jsmith" },
                password: { label: "패스워드", type: "password" }
            },

 

위 그림처럼 뭐 어려운 거 없이 잘 작동하고 있습니다.

 

이제 중요한 부분이 바로 아래 부분인데요.

 

 

실제 로그인 로직을 담당하는 부분입니다.

 

async authorize(credentials, req) {
                // Add logic here to look up the user from the credentials supplied
                const user = { id: 1, name: "J Smith", email: "jsmith@example.com" }

                if (user) {
                    // Any object returned will be saved in `user` property of the JWT
                    return user
                } else {
                    // If you return null or false then the credentials will be rejected
                    return null
                    // You can also Reject this callback with an Error or with a URL:
                    // throw new Error("error message") // Redirect to error page
                    // throw "/path/to/redirect"        // Redirect to a URL
                }
            }

authorize 함수를 async 방식 즉 비동기 방식으로 구현해야지만 NextAuth가 작동을 합니다.

 

authorize 함수는 credentials 변수와 req 변수를 받는데요.

async authorize(credentials, req) {

credentials 변수에 우리가 아까 HTML 로그인 폼에 입력한 username과 password 값이 저장되어 넘어오는 겁니다.

 

그래서 여기서 credentials 변수 안에 저장되어 넘어온 username / password를 비교해서 user를 넘겨주면 되는데요.

 

이 부분은 조금 있다가 구현해보고, 일단 예제 코드에서는 다음과 같이 user 부분을 강제로 지정했습니다.

 

const user = { id: 1, name: "J Smith", email: "jsmith@example.com" }

user 객체인데요.

 

NextAuth는 로그인이 되면 이렇게 user 객체를 리턴해 줍니다.

 

user 객체에는 id, name, email 이 필수로 들어가야 되고요.

 

그다음 코드를 볼까요?

 

if (user) {
                    // Any object returned will be saved in `user` property of the JWT
                    return user
                } else {
                    // If you return null or false then the credentials will be rejected
                    return null
                    // You can also Reject this callback with an Error or with a URL:
                    // throw new Error("error message") // Redirect to error page
                    // throw "/path/to/redirect"        // Redirect to a URL
                }

 

user 객체가 있다면 그 user 객체를 리턴하면서 authorize 함수가 끝납니다.

 

user 객체가 없다면 null을 리턴하면서 authorize 함수가 끝나고요.

 

그럼 우리가 강제로 지정한 user 객체의 email부분을 넣어서 로그인을 시도해 볼까요?

 

사실 authorize 함수를 잘 보시면 로그인 UI에서 넘어온 credentials 값을 체크하는 부분이 없어서 그냥 아래 그림처럼 Sign in 버튼을 누르면 됩니다.

 

 

한번 눌러볼까요?

 

위의 Sign in 버튼을 누르면 그냥 홈페이지의 루트 부분으로 이동합니다.

 

로그인이 된 건지 안된 건지 확인할 일이 없는데요.

 

Navbar 컴포넌트를 손봐서 로그인이 됐는지를 반영해 보겠습니다.

 

 

/components/Navbar.tsx

import React, { useState } from "react";
import { MenuIcon, XIcon } from "@heroicons/react/outline";

import { signOut, useSession } from "next-auth/react";

const Navbar = () => {
  const [menuToggle, setMenuToggle] = useState(false);
  const { data: session, status } = useSession();

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

export default Navbar;

 

여기서 일단 설명을 하고 나머지 코드를 작성하겠습니다.

 

위 코드를 보시면 "next-auth/react" 모듈에서 signOut과 useSession 부분을 불러왔습니다.

 

signOut 은 로그아웃 함수이고요.

 

useSession 훅이 아주 중요한데요.

 

NextAuth에서 user가 로그인되어있는지를 알려주는 아주 유용한 Hook입니다.

 

useSession 훅은 NextAuth 홈페이지에서 찾아보시면 다음과 같이 나오는데요.

 

설명을 보시면 useSession Hook은 클라이언트 사이드에서 작동하는 Hook입니다.

 

클라이언트 사이드라면 React를 말하는 겁니다.

 

status를 보시면 loading, authenticated, unauthenticated 세 가지 상태가 있네요.

 

그리고 data를 보시면 Session / undefined / null 이 있고요.

 

그래서 data: session이라고 하면 아까 [... nextauth].ts 부분에서 return user를 한 user 객체가 바로 session.user 에 저장되어 넘어오게 됩니다.

 

실행해 보면 아래와 같이 에러가 뜨는데요.

 

NextAuth를 실행하려면 SessionProvider를 추가해야 합니다.

 

 

 

_app.tsx 파일을 다음과 같이 수정하시면 됩니다.

 

import { SessionProvider } from "next-auth/react";
import "../styles/globals.css";

import Layout from "../components/Layout";

function MyApp({ Component, pageProps }) {
  return (
    <SessionProvider>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </SessionProvider>
  );
}

export default MyApp;

 

기존의 우리의 Layout위에 SessionProvider를 감쌌는데요.

 

SessionProvider는 next-auth/react 에서 불러왔습니다.

 

그리고 Navbar 부분의 HTML 코드 부분도 수정해 보겠습니다.

 

아래 부분은 HTML 코드가 있는 return 부분입니다.

return (
    //   navbar goes here
    <nav className="bg-gray-100">
      <div className="max-w-6xl mx-auto px-4">
        <div className="flex justify-between">
          <div className="flex space-x-4">
            {/* logo */}
            <div>
              <a href="/" className="flex items-center py-5 px-2 text-gray-700">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  className="h-5 w-5 mr-2 text-blue-400"
                  viewBox="0 0 20 20"
                  fill="currentColor"
                >
                  <path
                    fillRule="evenodd"
                    d="M9.504 1.132a1 1 0 01.992 0l1.75 1a1 1 0 11-.992 1.736L10 3.152l-1.254.716a1 1 0 11-.992-1.736l1.75-1zM5.618 4.504a1 1 0 01-.372 1.364L5.016 6l.23.132a1 1 0 11-.992 1.736L4 7.723V8a1 1 0 01-2 0V6a.996.996 0 01.52-.878l1.734-.99a1 1 0 011.364.372zm8.764 0a1 1 0 011.364-.372l1.733.99A1.002 1.002 0 0118 6v2a1 1 0 11-2 0v-.277l-.254.145a1 1 0 11-.992-1.736l.23-.132-.23-.132a1 1 0 01-.372-1.364zm-7 4a1 1 0 011.364-.372L10 8.848l1.254-.716a1 1 0 11.992 1.736L11 10.58V12a1 1 0 11-2 0v-1.42l-1.246-.712a1 1 0 01-.372-1.364zM3 11a1 1 0 011 1v1.42l1.246.712a1 1 0 11-.992 1.736l-1.75-1A1 1 0 012 14v-2a1 1 0 011-1zm14 0a1 1 0 011 1v2a1 1 0 01-.504.868l-1.75 1a1 1 0 11-.992-1.736L16 13.42V12a1 1 0 011-1zm-9.618 5.504a1 1 0 011.364-.372l.254.145V16a1 1 0 112 0v.277l.254-.145a1 1 0 11.992 1.736l-1.735.992a.995.995 0 01-1.022 0l-1.735-.992a1 1 0 01-.372-1.364z"
                    clipRule="evenodd"
                  />
                </svg>
                <span className="font-bold">Home</span>
              </a>
            </div>

            {/* primary nav */}
            <div className="hidden md:flex items-center space-x-1">
              <a
                href="/features"
                className="py-5 px-3 text-gray-700 hover:text-gray-900"
              >
                Features
              </a>
              <a
                href="#"
                className="py-5 px-3 text-gray-700 hover:text-gray-900"
              >
                Pricing
              </a>
            </div>
          </div>
          {/* secondary nav */}
          {status === "authenticated" ? (
            <div className="hidden md:flex items-center space-x-1">
              <button className="py-5 px-3" onClick={() => signOut()}>Log out</button>
            </div>
          ) : (
            <div className="hidden md:flex items-center space-x-1">
              <a href="/api/auth/signin" className="py-5 px-3">
                Login
              </a>
              <a
                href="#"
                className="py-2 px-3 bg-yellow-400 hover:bg-yellow-300 text-yellow-900 hover:text-yellow-800 rounded transition duration-300"
              >
                Signup
              </a>
            </div>
          )}
          {/* mobile menu */}
          <div className="md:hidden flex items-center">
            <button onClick={() => setMenuToggle(!menuToggle)}>
              {menuToggle ? (
                <XIcon className="w-6 h-6" />
              ) : (
                <MenuIcon className="w-6 h-6" />
              )}
            </button>
          </div>
        </div>
      </div>

      {/* mobile menu items */}
      <div className={`${!menuToggle ? "hidden" : ""} md:hidden`}>
        <a
          href="/features"
          className="block py-2 px-4 text-sm hover:bg-gray-200"
        >
          Features
        </a>
        <a href="#" className="block py-2 px-4 text-sm hover:bg-gray-200">
          Pricing
        </a>

        {status === "authenticated" ? (
          <button className="block py-2 px-4 text-sm hover:bg-gray-200" onClick={() => signOut()}>Log out</button>
        ) : (
          <div>
            <a
              href="/api/auth/signin"
              className="block py-2 px-4 text-sm hover:bg-gray-200"
            >
              Login
            </a>
            <a href="#" className="block py-2 px-4 text-sm hover:bg-gray-200">
              Signup
            </a>
          </div>
        )}
      </div>
    </nav>
  );

 

 

여기서 우리가 어떻게 로그인했고 로그인 안 됐는지를 구분하냐면

 

 {status === "authenticated" ? (
            <div className="hidden md:flex items-center space-x-1">
              <button className="py-5 px-3" onClick={() => signOut()}>Log out</button>
            </div>
          ) : (
            <div className="hidden md:flex items-center space-x-1">
              <a href="/api/auth/signin" className="py-5 px-3">
                Login
              </a>
              <a
                href="#"
                className="py-2 px-3 bg-yellow-400 hover:bg-yellow-300 text-yellow-900 hover:text-yellow-800 rounded transition duration-300"
              >
                Signup
              </a>
            </div>
          )}

 

위와 같이 status 가 "authenticated"와 같은지만 체크하면 됩니다.

 

이제 실행해 볼까요?

 

 

상단 Navbar 오른쪽이 Log out으로 바뀌었네요.

 

그리고 Log out 버튼을 누르면  아래 그림처럼 Login이 보입니다.

이제 Login 버튼을 누르면 로그인 화면(/api/auth/signin)으로 넘어갑니다.

 

뭔가 NextAuth를 이용한 로그인 로그아웃이 잘 구현된 거 같은데요.

 

 

4. session 정보 보기

 

위에서 로그인 후에 넘어온 user 정보를 보려면 useSession 훅을 이용해서 session.user.email 이런 식으로 정보를 얻는다고 했는데요.

 

한번 볼까요?

 

Navbar.tsx 부분에 다음 코드를 추가해 보겠습니다.

 

const Navbar = () => {
  const [menuToggle, setMenuToggle] = useState(false);
  const { data: session, status } = useSession();

  if (status === "authenticated") console.log("session", session);

  return (
  ....
  ....
  ....
  ....

 

핵심코드는 아래와 같습니다.

if (status === "authenticated") console.log("session", session);​

즉, status 가 인증(authenticated) 되었을때 session 을 console.log하라는 명령어입니다.
 
결과를 한번 볼까요?
 

 

브라우저의 콘솔 창에 session 부분이 나타났습니다.

 

user 객체에는 우리가 [... nextauth].ts 파일에서 강제로 지정했던 user 객체가 보입니다.

 

const user = { id: 1, name: "J Smith", email: "jsmith@example.com" }

위 코드처럼요.

 

이제 전체적인 로그인 구현을 맛보기로 봤는데요.

 

[... nextauth].ts 파일의 authorize 함수만 손보면 우리가 원하는 username / password 인증 로직이 구현될 수 있을 거 같습니다.

 

다음 시간에는 그 구현에 대해 자세히 알아보겠습니다.

 

 

그리드형