
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", 혹은 그 이상의 버전이 설치가 되었는지 확인한다

그 다음 openssl을 사용하여 secret key를 생성해줄 것이다.
openssl rand -base64 32
openssl 설치 방법은 인터넷에 많이 올라와 있다. 생성된 secret key는 외부에 공유하지 않도록 유의하자! 아래 이미지에서 공유하는 키는 참고용이며 실제로 쓰이진 않았다

마지막으로 .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내의 모든 폴더는 경로가된다. 예를 들어 위의 폴더 구조를 가진 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앱을 실행한다.
두 화면이 정상적으로 나타난다면 다음 단계로 넘어가자
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.tsx과 src/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으로 호출되어 에러가 발생한다.
다음은 로그인 된 유저 정보를 조회할 수 있는 세션을 호출하는 방법과 유저정보를 수정하면서 세션도 함께 수정하는 방법에 대해 다루도록 하겠다!