[Next.js14] NextAuth v5로 인증 구현하기 (1) - 로그인/로그아웃

김영준 ·2024년 1월 11일

Nextjs14 & NextAuthV5

목록 보기
1/3
post-thumbnail

Next.js 14과 NextAuth v5을 사용하면서 삽질했던 기록을 남긴다. Next14는 출시 된지 얼마 안됐고 NextAuth v5는 정식버전도 아닌 베타 버전이라 관련 글이 거의 없었으며 공식 문서도 굉장히 불친절...

Next.js 14 공식 튜토리얼 + 개발 하면서 배운 것을 토대로 작성했다.

준비

👉 패키지 설치

npm을 활용하여 NextAuth v5를 설치해준다

npm install next-auth@beta

package.json에 "next-auth": "^5.0.0-beta.4", 혹은 그 이상의 버전이 설치가 되었는지 확인한다


👉 secret key 생성

그 다음 openssl을 사용하여 secret key를 생성해줄 것이다.

openssl rand -base64 32

openssl 설치 방법은 인터넷에 많이 올라와 있다. 생성된 secret key는 외부에 공유하지 않도록 유의하자! 아래 이미지에서 공유하는 키는 참고용이며 실제로 쓰이진 않았다


👉.env 변수 추가

마지막으로 .env 파일에 AUTH_SECRET 변수를 설정해주면 준비 끝 (.gitignore에 .env가 추가되어있는지 확인하자)

/.env
AUTH_SECRET=2rhmxBZsnncgb392RxtB...


코드

👉 폴더 구조

프로젝트의 주요 폴더 구조는 다음과 같다

npx create-next-app@latest 를 사용하여 Next.js 14 프로젝트를 생성하였고 typescript, tailwind css, src/ directory, app router를 모두 사용하였다.

src/app/login 폴더,
src/lib 폴더,
auth.config.ts,
auth.ts,
middleware.ts

를 설명을 위해 추가하였다.

👉 App router

App router를 간략하게 짚고 넘어가자면 app내의 모든 폴더는 경로가된다. 예를 들어 위의 폴더 구조를 가진 Next앱을 실행하고http://localhost:3000/login 에 접속시 src/app/login/page.tsx 에 작성한 컴포넌트 화면을 볼 수 있다.

page.tsx와 같이 Next에서 특별한 기능을 하는 파일명이 몇가지 존재하는데 자세한 사항은 공식문서에서 확인 가능하다

👉 로그인 페이지

// src/app/login/page.tsx
"use client"

// import { authenticate } from "@/lib/actions"
// import { useFormState } from "react-dom"

export default function Page() {
  	// 추후에 추가될 로그인 메소드
    // const [errorMsg, dispatch] = useFormState(authenticate, undefined)
    return (
      <div>
        <h1>로그인 페이지</h1>
        <form className="flex flex-col"> {/*action={dispatch}*/}  
            <input className="bg-blue-300 text-black" name="id"></input>
            <input className="bg-yellow-300 text-black" name="password" type="password"></input>
            <button>
                로그인
            </button>
 			{/* 추후에 추가될 에러 메세지
            <p>{errorMsg}</p> */}
        </form>
      </div>
    )
  }
  

👉 홈 페이지

// src/app/page.tsx
// import { signOut } from "../auth"

export default async function Home() {
  return (
    <div>
      <h1>홈 페이지</h1>
      <h2>인증 없이 못보는 화면</h2>
      <form
          // action={async () => {
    	  // // 추후에 추가될 로그아웃 메소드
          // 'use server';
          //  await signOut();
          }}
        >
          <button>
            로그아웃
          </button>
        </form>
    </div>
  )
}

코드를 작성하였다면 cmd에 npx next dev 또는 npm run dev를 실행하여 Next앱을 실행한다.

http://localhost:3000/login

http://localhost:3000/

두 화면이 정상적으로 나타난다면 다음 단계로 넘어가자

👉 인증과 Protected Route 구현하기

src 폴더 내에 auth.config.ts, auth.ts, middleware.ts, 3개의 파일을 추가한다.
그리고 src/lib 폴더 내에 definitions.ts, actions.ts, 2개의 파일을 추가한다.

Next.js에서는 'middleware'의 이름을 가진 파일은 어떤 요청이 처리 되기 전에 먼제 실행되는 코드다. 예를들어 홈페이지인 http://localhost:3000/ 에 접속하겠다는 요청을 받으면 그 전에 middleware.ts 내의 코드가 먼저 실행된다.

바로 이 middleware에서 유저인증이 이루어졌나? 예->홈페이지, 아니오->로그인 페이지라는 로직을 코딩해주면된다.

간혹가다 middleware가 정상적으로 실행이 안되는 경우가 있는데 이럴때 3파일을 src 폴더 바깥, 즉 root 디렉토리에 옮겨보고 다시 실행해보면 된다.

먼저 유저 타입을 선언한 코드

// src/lib/definitions.ts
export type User = {
    id: string
    email: string
    name : string
};

인증 로직을 구현한 코드

// src/auth.config.ts
import type { NextAuthConfig } from 'next-auth';
 
export const authConfig = {
  pages: {
    signIn: '/login',
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      // 유저 인증 확인
      const isLoggedIn = !!auth?.user;
      // 보호하고 싶은 경로 설정
      // 여기서는 /login 경로를 제외한 모든 경로가 보호 되었다
      const isOnProtected = !(nextUrl.pathname.startsWith('/login'));
      
      if (isOnProtected) {
        if (isLoggedIn) return true;
        return false; // '/login' 경로로 강제이동
      } else if (isLoggedIn) {
        // 홈페이지로 이동
        return Response.redirect(new URL('/', nextUrl));
      }
      return true;
    },
  },
} satisfies NextAuthConfig;

NextAuth의 설정, 로그인/로그아웃 설정을 초기화한다

// src/auth.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import Credentials from 'next-auth/providers/credentials';
import { User } from '@/lib/definitions';

export const { signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    Credentials({
      async authorize(credentials) {
        if (credentials.id && credentials.password) {
          // 백엔드에서 로그인 처리
          // let loginRes = await backendLogin(credentials.id, credentials.password)
          let loginRes = {
            success : true,
            data : {
              user: {
                ID: "user1",
                NAME: "홍길동",
                EMAIL: "email@email.email",
              },
            }
          }
          // 로그인 실패 처리
          if (!loginRes.success) return null;
          // 로그인 성공 처리
          const user = {
            id: loginRes.data.user.ID ?? '',
            name: loginRes.data.user.NAME ?? '',
            email: loginRes.data.user.EMAIL ?? '',
          } as User;
          return user;
        }
        return null;
      },
    })
  ],
  callbacks: {
    async session({ session, token, user }) {
      session.user = token.user as User
      return session;
    },
    async jwt({ token, user, trigger, session }) {
      if (user) {
        token.user = user;
      }
      return token;
    },
  },
});

마지막으로 middleware.ts에서 NextAuth를 호출해준다!

// src/middleware.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
 
export default NextAuth(authConfig).auth;

export const config = {
  // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
};

그리고 클라이언트에서 호출할 로그인 함수

// src/lib/actions.ts
"use server"

import { signIn } from "../auth";
import { AuthError } from "next-auth";

export async function authenticate(
    prevState: string | undefined,
    formData: FormData,
  ) {
    try {
      await signIn('credentials', formData);
    } catch (error) {
      if (error instanceof AuthError) {
        return '로그인 실패'
      }
      throw error;
    }
  }

🔴이제 src/app/login/page.tsxsrc/app/page.tsx 에 주석 처리 되어 있는 코드를 해제한다.🔴

로그인 페이지

// src/app/login/page.tsx
"use client"

import { authenticate } from "@/lib/actions"
import { useFormState } from "react-dom"

export default function Page() {
    const [errorMsg, dispatch] = useFormState(authenticate, undefined)
    return (
      <div>
        <h1>로그인 페이지</h1>
        <form className="flex flex-col action={dispatch}"> 
            <input className="bg-blue-300 text-black" name="id"></input>
            <input className="bg-yellow-300 text-black" name="password" type="password"></input>
            <button>
                로그인
            </button>
            <p>{errorMsg}</p>
        </form>
      </div>
    )
  }
  

홈페이지

// src/app/page.tsx
import { signOut } from "../auth"

export default async function Home() {
  return (
    <div>
      <h1>홈 페이지</h1>
      <h2>인증 없이 못보는 화면</h2>
      <form
          action={async () => {
          	'use server';
            await signOut();
          }}
        >
          <button>
            로그아웃
          </button>
        </form>
    </div>
  )
}

✅결과

http://localhost:3000/ 을 로그인 없이 접속하면

url에 callbackurl이 호출되며 로그인 페이지로 강제 이동되는 것을 확인할 수 있다!

로그인 페이지에 아무거나 치고

로그인 버튼을 누르면...

인증을 성공적으로 구현했다.

지금은 로그인 데이터에 아무거나 쳐도 src/auth.ts파일에서

user: 
	{
    	ID: "user1",
        NAME: "홍길동",
        EMAIL: "email@email.email",
	},

가 하드코딩 된채로 loginRes에 반환된다. 벡엔드에 따라 loginRes 부분을 수정해주면 실제 로그인 구현이 가능하다!

🔴배포 시 필독사항🔴

Next 앱을 서버에 배포할때 .env 파일 안에 변수를 추가해줘야한다.

NEXTAUTH_URL= ${배포 도메인 url}
NEXT_PUBLIC_API_URL= ${배포 도메인 url}

위의 설정을 해주지 않으면 callback이 항상 localhost:3000으로 호출되어 에러가 발생한다.

다음

다음은 로그인 된 유저 정보를 조회할 수 있는 세션을 호출하는 방법과 유저정보를 수정하면서 세션도 함께 수정하는 방법에 대해 다루도록 하겠다!

profile
창업과 개발

0개의 댓글