[TIL]NextAuth.js에서 Auth.js로 마이그레이션 하기(v4 → v5)

Sohee Yeo·2025년 9월 9일
0
post-thumbnail

NextAuth.js v4 → Auth.js v5

Nex.js의 인증 라이브러리였던 NextAuth.js가 모든 JavaScript 프레임워크에서 사용할 수 있는 Auth.js로 확장되었다.

이번에 기존 Next.js 프로젝트를 마이그레이션하면서 인증 기능도 함께 Auth.js로 마이그레이션 해보았다.

auth.js 설치

npm uninstall next-auth
npm install next-auth@beta

Adapter 설치

데이터베이스 어댑터를 사용하고 있는 경우 새로 설치한다.

npm uninstall @next-auth/prisma-adapter
npm install @auth/prisma-adapter

환경변수 변경

.env 파일의 변수명을 바꿔준다.

// 기존
- GOOGLE_CLIENT_ID= '...'
- GOOGLE_CLIENT_SECRET= '...'
- NEXTAUTH_URL=http://localhost:3000

// 변경
+ AUTH_GOOGLE_ID= '...'
+ AUTH_GOOGLE_SECRET= '...'
+ AUTH_URL='http://localhost:3000'

구성 파일 변경

기존의 app/api/auth/[...nextauth]/route.ts의 설정을 루트 위치로 옮긴다. 기존 파일은 App Router의 라우트 핸들러로 사용된다.

이때 데이터베이스 어댑터를 사용하는 경우 한 파일에서 관리했던 설정을 두 개의 파일로 나눈다.

🤔 왜 두 개의 파일로 나눠야 할까?

💡 Auth.js는 두 가지 세션 전략을 지원한다.

1. Database 세션: 데이터베이스에 사용자 정보를 저장해서 기억하는 방식
2. JWT 세션: 사용자 정보 대신 암호화된 토큰을 주고받으면서 기억하는 방식. 데이터베이스는 거치지 않음

데이터베이스 어댑터를 사용하는 경우 Database 세션 전략이 적용된다. 하지만 데이터베이스 어댑터가 Edge 런타임과 호환되지 않으면 데이터베이스에 접근할 수 없다.
Edge 런타임은 사용자와 가까운 서버에서 코드를 실행시켜 응답 속도를 높이는 환경인데, 데이터베이스에 직접 연결할 수는 없다.

따라서 설정 파일을 나누어 Edge 런타임 같은 특정 환경에서는 JWT 세션 관련 설정만 적용되도록 만들고, 다른 환경에서는 기존 Database 세션을 사용할 수 있도록 만들어야 한다.

기존 NextAuth.js (v4)

// app/api/auth/[...nextauth]/route.js

import NextAuth from "next-auth";
import { prisma } from "@/lib/prisma";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import CredentialsProvider from "next-auth/providers/credentials";
import bcrypt from "bcrypt";
import GoogleProvider from "next-auth/providers/google";
import KakaoProvider from "next-auth/providers/kakao";
import NaverProvider from "next-auth/providers/naver";

export const authOptions = {
    adapter: PrismaAdapter(prisma),
    providers: [
        CredentialsProvider({
            id: "credentials",
            name: "Credentials",
            credentials: {
                email: { label: "Email", type: "text", placehoder: "이메일" },
                password: {
                    label: "Password",
                    type: "password",
                    placehoder: "비밀번호",
                },
            },

            async authorize(credentials, req) {
                if (!credentials.email) {
                    throw new Error("아이디를 입력해주세요.");
                } else if (!credentials.password) {
                    throw new Error("비밀번호를 입력해주세요.");
                }

                try {
                    let user = await prisma.user.findUnique({
                        where: {
                            email: credentials.email,
                        },
                    });

                    if (!user) {
                        throw new Error(
                            "이메일 또는 비밀번호가 일치하지 않습니다."
                        );
                    }

                    const pwCheck = await bcrypt.compare(
                        credentials.password,
                        user.password
                    );

                    if (!pwCheck) {
                        throw new Error(
                            "이메일 또는 비밀번호가 일치하지 않습니다."
                        );
                    }
                    return user;
                } catch (err) {
                    console.log(err);
                    // try 문에서 전달된 에러 처리
                    if (
                        err.message ==
                        "이메일 또는 비밀번호가 일치하지 않습니다."
                    ) {
                        throw err;
                    }

                    // 예상치 못한 에러 처리
                    throw Error(
                        "로그인 요청 중 문제가 발생했습니다. 잠시 후 다시 시도해주세요."
                    );
                }
            },
        }),
        KakaoProvider({
            clientId: process.env.KAKAO_CLIENT_ID,
            clientSecret: process.env.KAKAO_CLIENT_SECRET,
        }),
        NaverProvider({
            clientId: process.env.NAVER_CLIENT_ID,
            clientSecret: process.env.NAVER_CLIENT_SECRET,
        }),
        GoogleProvider({
            clientId: process.env.GOOGLE_CLIENT_ID,
            clientSecret: process.env.GOOGLE_CLIENT_SECRET,
        }),
    ],
    session: {
        strategy: "jwt",
    },
    callbacks: {
        jwt: async ({ token, user }) => {
            if (user) {
                token.sub = user.id;
            }
            return token;
        },
        session: async ({ session, token }) => ({
            ...session,
            user: {
                ...session.user,
                id: token.sub,
            },
        }),
    },
    pages: {
        signIn: "/login",
    },
    secret: process.env.SECRET,
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

Auth.js (v5)

auth.config.ts: JWT 세션 관련 설정 파일. Edge 런타임에도 안전하다.

// auth.config.ts

import type { NextAuthConfig } from "next-auth";
import Google from "next-auth/providers/google";
import Kakao from "next-auth/providers/kakao";
import Naver from "next-auth/providers/naver";

export const authConfig = {
    providers: [
        Google({
            clientId: process.env.AUTH_GOOGLE_ID!,
            clientSecret: process.env.AUTH_GOOGLE_SECRET!,
        }),
        Kakao({
            clientId: process.env.AUTH_KAKAO_ID!,
            clientSecret: process.env.AUTH_KAKAO_SECRET!,
        }),
        Naver({
            clientId: process.env.AUTH_NAVER_ID!,
            clientSecret: process.env.AUTH_NAVER_SECRET!,
        }),
    ],
    pages: {
        signIn: "/login",
    },
} satisfies NextAuthConfig;

auth.ts: Database 세션 관련 설정 파일. auth.config.ts 파일을 가져와 필요한 설정을 추가한다.

// auth.ts

import { prisma } from "@/lib/prisma";
import { PrismaAdapter } from "@auth/prisma-adapter";
import NextAuth from "next-auth";
import { authConfig } from "./auth.config";
import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";

export const { auth, handlers, signIn, signOut } = NextAuth({
    ...authConfig, // auth.config.ts에서 정의한 기본 설정 가져옴
    adapter: PrismaAdapter(prisma),
    session: {
        strategy: "jwt",
        maxAge: 60 * 60 * 24,
    },
    providers: [
        ...authConfig.providers, // auth.config.ts에 정의된 providers 가져옴
        Credentials({
            name: "Credentials",
            credentials: {
                email: { label: "Email", type: "text" },
                password: { label: "Password", type: "password" },
            },
            authorize: async (credentials, req) => {
                const { email, password } = credentials;

                try {
                    let user = await prisma.user.findUnique({
                        where: {
                            email: email as string,
                        },
                    });

                    // 유저가 없는 경우 에러 처리
                    if (!user) {
                        console.log("유저 정보 없음");
                        return null;
                    }

                    const pwCheck = await bcrypt.compare(
                        password as string,
                        user.password!
                    );

                    // 비밀번호가 일치하지 않는 경우 에러 처리
                    if (!pwCheck) {
                        console.log("비밀번호 불일치");
                        return null;
                    }

                    return {
                        id: user.id,
                        email: user.email || undefined,
                        name: user.name || null,
                    };
                } catch (err) {
                    // 예상치 못한 에러 처리
                    console.error("Authorize Error:", err);
                    return null;
                }
            },
        }),
    ],
    callbacks: {
        signIn: async () => {
            return true;
        },
        jwt: async ({ token, user }) => {
            if (user) {
                token.id = user.id;
                token.email = user.email;
            }
            return token;
        },
        session: async ({ session, token }) => {
            if (session && token) {
                session.user.id = token.id as string;
                session.user.email = token.email as string;
            }
            return session;
        },
    },
});

app/api/auth/[...nextauth]/route.ts 파일은 라우트 핸들러로 대체된다.

// app/api/auth/[...nextauth]/route.ts

import { handlers } from "@/auth";

export const { GET, POST } = handlers;

서버 컴포넌트

기존 NextAuth.js (v4)

getServerSession을 사용하여 세션을 가져왔다.

import { getServerSession } from "next-auth";
import { authOptions } from "../auth/[...nextauth]/route";

export async function POST(req) {
    const session = await getServerSession(authOptions);
  
  // 코드
}

Auth.js (v5)

Auth.js에서 제공하는 auth() 함수를 호출하여 세션을 가져온다.

// auth.ts

export const { auth, handlers, signIn, signOut } = NextAuth({
  // 코드
})
import { auth } from "@/auth";

export async function POST(req: NextRequest) {
    const session = await auth();  
  
  // 코드
}

미들웨어

기존 NextAuth.js (v4)

기존에는 next-auth/jwtgetToken을 사용해 사용자의 세션을 확인했다.

// middleware.js

import { getToken } from "next-auth/jwt";
import { NextResponse } from "next/server";

const secret = process.env.SECRET;

export async function middleware(req) {
    const token = await getToken({ req, secret });
    const { pathname } = req.nextUrl;

    if (pathname.startsWith("/login") || pathname.startsWith("/signup")) {
        if (token) {
            // ...
        }
    } 
}

Auth.js (v5)

Auth.js의 auth() 함수를 사용해 사용자의 세션을 확인한다. 미들웨어는 Edge 런타임에서 실행되기 때문에 auth.config.ts 파일의 설정을 가져와 필요한 인증만 가볍게 처리한다.

// middleware.ts

import { NextResponse } from "next/server";
import { authConfig } from "./auth.config";
import NextAuth from "next-auth";

const { auth } = NextAuth(authConfig);

export default auth(async function middleware(req) {
    const { pathname } = req.nextUrl;

    if (pathname.startsWith("/login") || pathname.startsWith("/signup")) {
        if (req.auth?.user) {
            // ...
        }
    }
});

정리

  • 환경변수 NEXTAUTH_AUTH_로 변경
  • app/api/auth/[...nextauth]/route.ts 구조 변경
  • JWT 세션 관련 설정 파일과 Database 세션 관련 설정 파일 분리
  • 서버 컴포넌트에서 getServerSessionauth()로 세션 접근



참고

https://authjs.dev/getting-started/migrating-to-v5

profile
실패에 무딘 사람. 프론트엔드 개발자를 꿈꿉니다

0개의 댓글