테마 전환

NextAuth.js 입문 가이드: Credentials 로그인 설정과 세션 관리 완벽 정리

Easton editorial illustration: component assembly loom

NextAuth.js 공식 문서를 열면 화면을 빽빽하게 채운 설정 항목에 눈이 어지럽습니다. Provider, Session, Adapter, JWT, Callbacks… 단어는 하나씩 다 아는데 이어 놓으면 무슨 말인지 알기 어렵습니다. 문서에서는 “기본적으로 JWT를 사용한다”고 했다가, 다음 단락에서는 “데이터베이스를 사용하면 자동으로 Session으로 전환된다”고 합니다. 그렇다면 도대체 무엇을 써야 할까요?

저도 당시에는 정말 포기하고 그냥 Clerk를 쓸까 생각했습니다. 하지만 다시 생각해 보니 Clerk는 빠르긴 해도 월간 활성 사용자 10,000명의 무료 한도를 넘으면 비용을 내야 하고, 마음대로 바꿀 수 없는 부분도 많습니다. NextAuth.js는 시작하기 조금 어렵지만 완전히 무료이고 모든 것을 직접 제어할 수 있습니다.

이 글에서는 모든 설정 항목을 나열하지 않습니다. 그렇게 하면 너무 괴로우니까요. 대신 가장 흔한 Credentials 로그인 상황, 즉 사용자 이름과 비밀번호 로그인, 세션 관리, 데이터베이스 통합에 집중합니다. 이 몇 가지 핵심 문제만 이해하면 NextAuth.js도 어렵지 않습니다.

NextAuth.js의 핵심 개념 이해하기

NextAuth.js는 도대체 무엇을 하나요?

한 문장으로 말하면 NextAuth.js는 “인증 미들웨어”로서 “누가 로그인했는지”와 “로그인 상태를 어떻게 저장할지”를 관리합니다. 어떤 데이터베이스를 쓰는지, UI가 어떻게 생겼는지는 관여하지 않고 사용자 신원을 확인하고 로그인 상태를 기억하는 일만 담당합니다.

Clerk, Supabase Auth와의 차이는 무엇일까요? 간단히 정리하면 다음과 같습니다.

  • Clerk: 보기 좋은 로그인 UI와 사용자 관리 화면을 제공합니다. 30분이면 설정할 수 있지만 비용이 듭니다(월간 활성 사용자 10,000명 초과 시).
  • Supabase Auth: Supabase 데이터베이스를 사용한다면 인증 기능을 거의 덤으로 얻을 수 있고, 통합도 매우 매끄럽습니다.
  • NextAuth.js: 완전 무료 오픈 소스이지만 로그인 화면, 사용자 가입, 데이터베이스 로직을 모두 직접 작성해야 합니다.

2025년의 흐름은 꽤 흥미롭습니다. NextAuth.js를 여전히 많이 사용하지만 Clerk처럼 “바로 쓸 수 있는” 솔루션이 빠르게 시장을 넓히고 있습니다. 인증 코드를 작성하는 데 며칠을 쓰기보다 핵심 기능에 시간을 투자하려는 선택은 충분히 이해됩니다. 그래도 개인 프로젝트이거나 모든 것을 직접 제어하고 싶다면 NextAuth.js는 여전히 배울 가치가 있습니다.

반드시 이해해야 할 세 가지 개념

1. Provider(제공자): 사용자는 어떻게 로그인하나요?

로그인 방식을 말합니다. NextAuth.js는 50개가 넘는 방식을 지원하며, 가장 흔한 것은 다음과 같습니다.

  • OAuth 제공자: Google, GitHub, Facebook 등. 한 번 클릭하면 로그인되며 비밀번호를 직접 관리하지 않아도 됩니다.
  • Credentials 제공자: 사용자 이름과 비밀번호를 사용하는 가장 전통적인 방식이며, 이 글의 주제입니다.

한 가지 함정이 있습니다. Credentials 제공자는 가장 유연하지만 직접 작성할 코드도 가장 많습니다. 공식 문서에서도 보안 위험을 모두 개발자가 책임져야 하므로 사용을 권장하지 않을 정도입니다. 비밀번호 암호화, 무차별 대입 공격 방지, 세션 관리까지 모두 직접 챙겨야 합니다.

2. Session(세션): 로그인 후 사용자를 어떻게 기억하나요?

한 번 로그인한 사용자가 요청할 때마다 비밀번호를 다시 입력하게 할 수는 없습니다. Session은 “로그인 상태를 기억하는” 메커니즘입니다. 방식은 두 가지입니다.

  • JWT Session: 로그인 정보를 암호화해 cookie에 넣고 서버에는 아무것도 저장하지 않습니다.
  • Database Session: cookie에는 ID 하나만 넣고 실제 로그인 정보는 데이터베이스에 저장합니다.

둘 중 무엇을 선택할지는 NextAuth.js 초보자가 가장 헷갈려 하는 부분입니다. 다음 절에서 자세히 살펴보겠습니다.

3. Adapter(어댑터): 사용자 데이터는 어디에 저장하나요?

email이나 가입 시간 같은 사용자 정보를 데이터베이스에 저장하려면 Adapter가 필요합니다. NextAuth.js는 Prisma, MongoDB, MySQL 등 다양한 데이터베이스를 지원합니다.

다만 중요한 점이 있습니다. Credentials 제공자는 사용자 정보를 데이터베이스에 자동으로 저장하지 않습니다. 공식 문서에도 Credentials를 사용하면 사용자 계정을 직접 관리해야 한다고 명시되어 있습니다. NextAuth.js는 로그인 검증만 맡으며 가입이나 저장은 담당하지 않습니다.

JWT vs Session: 무엇을 선택해야 할까요?

가장 핵심적인 문제입니다. 저도 처음에는 비교 글을 십여 개 읽고도 결정하지 못했습니다. 둘의 본질적인 차이를 이해하고 나서야 선택할 수 있었습니다.

JWT Session: 여권 방식

해외여행을 떠났다고 생각해 봅시다. 여권에는 사진, 이름, 유효 기간이 적혀 있습니다. 입국 심사 때마다 세관 직원은 여권만 보면 되고 별도 시스템을 조회할 필요가 없습니다. JWT도 이와 같습니다. 사용자가 로그인하면 서버가 userId, email 등의 정보를 담은 암호화된 token(여권과 같은 것)을 만들고 브라우저 cookie에 저장합니다.

장점:

  • 빠릅니다. 데이터베이스를 조회하지 않고 token을 복호화하면 사용자가 누구인지 알 수 있습니다.
  • 저렴합니다. session 저장용 데이터베이스가 필요 없어 특히 serverless 배포에 적합합니다.
  • 확장성이 좋습니다. 사용자 수가 늘어나도 session 테이블이 폭증할 걱정이 없습니다.

단점:

  • 사용자를 강제로 로그아웃시킬 수 없습니다. 계정 탈취를 발견해 특정 로그인을 무효화하고 싶어도 token이 만료되기 전에는 불가능합니다(블랙리스트를 관리할 수는 있지만, 그러면 다시 데이터베이스가 필요합니다).
  • 동시 로그인 기기 수를 제한할 수 없습니다. “최대 3대의 기기에서만 동시 로그인” 같은 기능을 구현하기 어렵습니다.
  • token 만료 전에는 정보를 갱신할 수 없습니다. token에 사용자 역할을 저장한 뒤 역할이 바뀌어도 token이 만료되기 전까지는 예전 역할이 보입니다.

Database Session: 호텔 카드 키 방식

호텔에서는 객실 번호만 담긴 카드 키를 주고, 여권이나 결제 내역 같은 상세 정보는 호텔 시스템에 저장합니다. 카드를 사용할 때마다 시스템이 객실 번호를 조회해 출입 권한을 확인합니다. Database Session도 같습니다. cookie에는 session ID만 저장하고 실제 사용자 정보는 데이터베이스에 둡니다.

장점:

  • 언제든 로그인을 취소할 수 있습니다. 사용자가 “모든 기기에서 로그아웃”을 누르면 데이터베이스의 session 레코드를 삭제하면 됩니다.
  • 로그인 기기 수를 제한할 수 있습니다.
  • 사용자 정보를 실시간으로 갱신할 수 있습니다. 예를 들어 권한이 바뀌면 다음 요청부터 즉시 반영됩니다.

단점:

  • 느립니다. 요청마다 데이터베이스를 조회해야 합니다.
  • session 테이블을 관리해야 합니다(만료된 session 생성 및 정리).
  • 사용자가 늘면 데이터베이스 부하가 커집니다.

결정 트리(핵심)

그래서 무엇을 골라야 할까요? 저는 다음과 같이 권합니다.

애플리케이션에 "사용자 강제 재로그인" 기능이 필요한가요?(예: 비밀번호 변경, 계정 정지)
  ├─ 예 → Database Session 사용
  └─ 아니요 → 다음 항목 확인

데이터베이스를 생략하거나 serverless로 배포하고 싶나요?
  ├─ 예 → JWT 사용
  └─ 아니요 → 다음 항목 확인

개인 프로젝트/MVP이며 빠른 출시가 목표인가요?
  ├─ 예 → JWT 사용(간단하고 편리함)
  └─ 아니요 → Database Session 사용(기업용 애플리케이션에 더 안정적)

제 프로젝트도 처음에는 JWT를 사용했습니다. 나중에 사용자를 강제로 로그아웃시켜야 하는 “사용자 관리 화면”을 추가하면서 Database Session으로 바꿨는데, 전환 비용이 꽤 컸습니다. 처음부터 요구 사항을 충분히 생각해 보길 권합니다.

특수한 경우: Credentials + Database Session

Credentials 제공자를 사용하면서 Database Session도 쓰고 싶다면 큰 문제를 만나게 됩니다. 공식 문서에서는 지원하지 않는다고 설명합니다.

구체적으로 OAuth 제공자(Google, GitHub)는 Database Session을 문제없이 사용할 수 있지만 Credentials는 그렇지 않습니다. NextAuth.js는 Credentials의 유연성이 너무 높아 session 레코드를 자동으로 만들 수 없다고 보기 때문입니다.

해결 방법은 있지만 signIn callback에서 session을 직접 생성하는 코드를 작성해야 합니다. GitHub에는 참고할 수 있는 논의가 많습니다(예: 이 issue). 하지만 초보자라면 JWT를 사용하거나 OAuth 제공자로 바꾸는 편을 권합니다. 굳이 난도를 높일 필요는 없습니다.

Credentials 제공자 전체 설정

이론은 여기까지 하고 이제 코드를 작성해 보겠습니다. 간단한 버전부터 완성형까지 세 가지 예시를 소개하니 현재 단계에 맞게 선택하면 됩니다.

기본 설정: 최소 실행 버전

먼저 의존성을 설치합니다.

npm install next-auth

그다음 파일을 만듭니다. Next.js 13+에서 App Router를 사용한다면 경로는 app/api/auth/[...nextauth]/route.ts이고, Pages Router를 사용한다면 pages/api/auth/[...nextauth].js입니다. 여기서는 App Router를 사용합니다.

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

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

const handler = NextAuth({
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: "이메일", type: "email" },
        password: { label: "비밀번호", type: "password" }
      },
      async authorize(credentials) {
        // 테스트를 위해 사용자를 하드코딩합니다
        if (credentials?.email === "[email protected]" &&
            credentials?.password === "123456") {
          return {
            id: "1",
            name: "테스트 사용자",
            email: "[email protected]"
          }
        }
        return null  // 로그인 실패
      }
    })
  ],
  session: {
    strategy: "jwt"  // JWT session 사용
  },
  pages: {
    signIn: '/login'  // 사용자 지정 로그인 페이지(선택 사항)
  }
})

export { handler as GET, handler as POST }

환경 변수 .env.local:

NEXTAUTH_SECRET=your-super-secret-key-change-this
NEXTAUTH_URL=http://localhost:3000

핵심 포인트:

  • NEXTAUTH_SECRET: token 암호화에 사용합니다. 프로덕션 환경에서는 반드시 설정해야 하며 로컬 개발에서도 설정하는 편이 좋습니다. 생성 명령: openssl rand -base64 32
  • authorize 함수: 사용자 신원을 검증하는 핵심 로직입니다. 사용자 객체를 반환하면 성공, null을 반환하면 실패입니다.

이 버전은 실행되지만 사용자 데이터가 하드코딩되어 있어 실제로 쓸 수는 없습니다. 다음 단계에서는 데이터베이스를 연결합니다.

핵심 설정 항목 자세히 알아보기

완성형 예시로 넘어가기 전에 가장 헷갈리기 쉬운 설정 몇 가지를 살펴보겠습니다.

1. session.strategy: “jwt” 또는 “database”?

앞서 설명했듯 기본값은 “jwt”입니다. Adapter(데이터베이스 연결)를 사용하면 “database”로 자동 전환됩니다. 하지만 Credentials 제공자와 JWT를 함께 사용하려면 strategy: "jwt"를 명시해야 합니다. 그렇지 않으면 오류가 발생할 수 있습니다.

2. callbacks: session에 사용자 지정 필드를 어떻게 추가하나요?

기본적으로 useSession이 반환하는 사용자 정보는 name, email, image뿐입니다. userIdrole을 추가하려면 어떻게 해야 할까요?

callbacks를 사용하면 됩니다.

callbacks: {
  async jwt({ token, user }) {
    // user는 로그인할 때만 값이 있습니다
    if (user) {
      token.userId = user.id  // token에 userId 추가
    }
    return token
  },
  async session({ session, token }) {
    // token의 userId를 session에 추가
    session.user.userId = token.userId
    return session
  }
}

이제 프론트엔드에서 const { data: session } = useSession()을 호출하면 session.user.userId에 값이 들어 있습니다.

3. pages: 로그인 페이지 사용자 지정

NextAuth.js에는 보기 좋지 않은 기본 로그인 페이지가 있습니다(주소는 /api/auth/signin). 직접 만든 로그인 UI를 사용하려면 pages: { signIn: '/login' }을 설정합니다.

로그인 페이지에서는 다음을 호출해야 합니다.

import { signIn } from "next-auth/react"

const handleSubmit = async (e) => {
  e.preventDefault()
  const result = await signIn('credentials', {
    redirect: false,
    email,
    password
  })

  if (result?.error) {
    // 로그인 실패
  } else {
    // 로그인 성공 후 이동
  }
}

완성형 예시: 사용자 가입 + 로그인 + 세션 관리

이번에는 실제로 사용할 수 있는 버전을 살펴보겠습니다. Prisma + PostgreSQL을 사용한다고 가정합니다.

1. 먼저 사용자 테이블 만들기

prisma/schema.prisma:

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  password  String
  name      String?
  createdAt DateTime @default(now())
}

npx prisma migrate dev를 실행해 테이블을 생성합니다.

2. 가입 API

app/api/register/route.ts:

import { NextResponse } from "next/server"
import bcrypt from "bcryptjs"
import { prisma } from "@/lib/prisma"  // prisma client가 있다고 가정

export async function POST(req: Request) {
  try {
    const { email, password, name } = await req.json()

    // 사용자가 이미 존재하는지 확인
    const existingUser = await prisma.user.findUnique({
      where: { email }
    })

    if (existingUser) {
      return NextResponse.json(
        { error: "이미 가입된 이메일입니다" },
        { status: 400 }
      )
    }

    // 비밀번호 암호화(중요!)
    const hashedPassword = await bcrypt.hash(password, 10)

    // 사용자 생성
    const user = await prisma.user.create({
      data: {
        email,
        password: hashedPassword,
        name
      }
    })

    return NextResponse.json({
      user: {
        id: user.id,
        email: user.email,
        name: user.name
      }
    })
  } catch (error) {
    return NextResponse.json(
      { error: "가입 실패" },
      { status: 500 }
    )
  }
}

핵심: 비밀번호는 반드시 암호화해야 합니다. bcrypt 또는 argon2를 사용하고 데이터베이스에 평문으로 저장하지 마세요.

3. NextAuth 설정(데이터베이스 연결)

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

import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import bcrypt from "bcryptjs"
import { prisma } from "@/lib/prisma"

const handler = NextAuth({
  providers: [
    CredentialsProvider({
      credentials: {
        email: { label: "이메일", type: "email" },
        password: { label: "비밀번호", type: "password" }
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          return null
        }

        // 데이터베이스에서 사용자 조회
        const user = await prisma.user.findUnique({
          where: { email: credentials.email }
        })

        if (!user) {
          return null  // 사용자가 존재하지 않음
        }

        // 비밀번호 검증
        const isValid = await bcrypt.compare(
          credentials.password,
          user.password
        )

        if (!isValid) {
          return null  // 잘못된 비밀번호
        }

        // 사용자 정보 반환(비밀번호는 포함하지 않음!)
        return {
          id: user.id,
          email: user.email,
          name: user.name
        }
      }
    })
  ],
  session: {
    strategy: "jwt",
    maxAge: 30 * 24 * 60 * 60  // 30일
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.userId = user.id
      }
      return token
    },
    async session({ session, token }) {
      session.user.userId = token.userId as string
      return session
    }
  },
  pages: {
    signIn: '/login'
  }
})

export { handler as GET, handler as POST }

4. 프론트엔드에서 session 사용하기

서버 컴포넌트(Server Component)에서는 다음과 같이 사용합니다.

import { getServerSession } from "next-auth"

export default async function ProfilePage() {
  const session = await getServerSession()

  if (!session) {
    redirect('/login')
  }

  return <div>환영합니다, {session.user.name}</div>
}

클라이언트 컴포넌트(Client Component)에서는 다음과 같이 사용합니다.

'use client'
import { useSession } from "next-auth/react"

export default function Dashboard() {
  const { data: session, status } = useSession()

  if (status === "loading") {
    return <div>불러오는 중...</div>
  }

  if (!session) {
    return <div>먼저 로그인해 주세요</div>
  }

  return <div>사용자 ID: {session.user.userId}</div>
}

주의:

  • 서버에서는 getServerSession()을 사용합니다.
  • 클라이언트에서는 useSession()을 사용합니다.
  • 클라이언트 컴포넌트 바깥을 <SessionProvider>로 한 번 감싸야 합니다(일반적으로 layout에 추가).

세션 관리 전략과 자주 겪는 문제

JWT 세션을 올바르게 사용하는 방법

JWT를 선택했다면 제대로 활용해야 합니다. 몇 가지 핵심을 살펴보겠습니다.

1. JWT에 추가 정보(userId, role 등) 저장하기

앞의 callbacks 예시에서 살펴봤듯 jwt callback은 token에 값을 추가하고, session callback은 token의 값을 session에 넣는 데 사용합니다.

실제 프로젝트에서는 역할도 저장해야 할 수 있습니다.

callbacks: {
  async jwt({ token, user }) {
    if (user) {
      token.userId = user.id
      token.role = user.role  // 데이터베이스에 role 필드가 있다고 가정
    }
    return token
  },
  async session({ session, token }) {
    session.user.userId = token.userId as string
    session.user.role = token.role as string
    return session
  }
}

2. Token 만료 시간 설정

기본값은 30일이지만 변경할 수 있습니다.

session: {
  strategy: "jwt",
  maxAge: 7 * 24 * 60 * 60  // 7일
}

한 가지 주의할 점이 있습니다. 사용자가 브라우저를 닫았다가 다시 열어도 token은 남아 있습니다(직접 로그아웃하지 않는 한). “브라우저를 닫으면 로그아웃”되게 하려면 프론트엔드에서 처리해야 하며 백엔드 JWT만으로는 할 수 없습니다.

3. JWT의 가장 큰 한계: 능동적으로 취소할 수 없음

JWT에서 가장 골치 아픈 부분입니다. 어떤 사용자의 계정이 탈취된 것을 발견해 즉시 로그아웃시키고 싶어도 불가능합니다. Token이 아직 만료되지 않았다면 token을 가진 사람은 계속 접근할 수 있습니다.

우회 방법:

  • token 만료 시간을 짧게 설정합니다(예: 1시간). 사용자 경험을 일부 희생해 보안을 높이는 방식입니다.
  • token 블랙리스트를 관리합니다. 다만 그러면 데이터베이스가 필요해 JWT의 장점이 사라집니다.
  • Database Session을 사용합니다. 가장 확실한 해결책입니다.

Database Session 구현(Credentials 제공자 포함)

처음부터 Database Session을 선택했다면 설정은 오히려 더 간단합니다. 단, OAuth 제공자(Google, GitHub)를 사용할 때의 이야기입니다.

Adapter를 설치합니다.

npm install @next-auth/prisma-adapter

설정은 다음과 같습니다.

import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { prisma } from "@/lib/prisma"

const handler = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!
    })
  ]
  // strategy는 "database"로 자동 전환되므로 직접 설정할 필요가 없음
})

데이터베이스에는 User, Session, Account 등의 테이블이 자동으로 생성됩니다.

하지만 Credentials 제공자를 사용하면 복잡해집니다. 공식 문서에는 Credentials provider가 database session을 지원하지 않는다고 명시되어 있습니다.

온라인에는 여러 해결 방법이 있으며, 핵심은 signIn callback에서 session 레코드를 직접 만드는 것입니다. 초보자에게는 권하지 않습니다. 실수하기 너무 쉽기 때문입니다. 꼭 Credentials + Database Session을 사용해야 한다면 다음 GitHub 논의를 참고하세요.

아니면 이 조합을 기본으로 지원하는 Clerk나 Supabase Auth를 고려해 보세요.

가장 자주 만나는 다섯 가지 문제

제가 모두 직접 겪었던 문제입니다. 미리 알면 시간을 상당히 절약할 수 있습니다.

1. NEXTAUTH_SECRET 설정을 잊어 프로덕션에서 오류 발생

로컬 개발에서는 NEXTAUTH_SECRET을 설정하지 않아도 됩니다(NextAuth.js가 경고를 표시하지만 실행은 됩니다). 하지만 Vercel, Railway 등의 플랫폼에 배포할 때 이 환경 변수가 없으면 바로 500 오류가 발생합니다.

생성 명령은 openssl rand -base64 32입니다. 생성한 값을 배포 플랫폼의 환경 변수에 추가하세요.

2. Credentials 제공자는 기본적으로 database session을 사용할 수 없음

앞에서 설명했지만 다시 강조하겠습니다. Adapter와 Credentials를 함께 설정하면 오류가 발생합니다. session: { strategy: "jwt" }를 명시하거나 session 생성 로직을 직접 구현해야 합니다.

3. 서버 컴포넌트에서 useSession을 사용할 수 없음

Next.js 13+ App Router의 기본값은 Server Component입니다. 서버 컴포넌트에서 const session = useSession()을 작성하면 hook은 클라이언트에서만 사용할 수 있으므로 오류가 발생합니다.

서버에서는 getServerSession()을 사용해야 합니다.

import { getServerSession } from "next-auth"

const session = await getServerSession()

클라이언트에서는 'use client' 지시문을 추가한 뒤 useSession()을 사용합니다.

4. Cookie 교차 도메인 문제

개발 환경은 localhost:3000, 프로덕션 환경은 example.com이어서 cookie의 domain이 서로 다르면 로그인 상태가 사라질 수 있습니다.

해결 방법은 프로덕션 환경의 NEXTAUTH_URL 환경 변수를 올바른 도메인으로 설정하는 것입니다. localhost를 하드코딩하지 마세요.

5. JWEDecryptionFailed 오류

오류: JWEDecryptionFailed: decryption operation failed

원인: NEXTAUTH_SECRET을 변경했지만 브라우저에는 이전 token이 남아 있습니다. 새 secret으로 기존 token을 복호화할 수 없어 오류가 발생합니다.

해결 방법: 브라우저 cookie를 삭제하거나 다시 로그인합니다.

보너스: Middleware로 경로 보호하기

특정 페이지에 로그인한 사용자만 접근하도록 만들고 싶다면 Middleware를 사용할 수 있습니다.

middleware.ts:

export { default } from "next-auth/middleware"

export const config = {
  matcher: ["/dashboard/:path*", "/profile/:path*"]
}

이제 /dashboard/profile 아래의 모든 페이지에서 로그인 상태를 검사하고, 로그인하지 않은 사용자는 자동으로 로그인 페이지로 이동합니다.

실제 프로젝트를 위한 조언과 심화 학습 경로

어떤 방식을 선택해야 할까요?

지금까지의 내용을 바탕으로 상황별 선택을 정리해 보겠습니다.

상황 1: 개인 프로젝트/MVP/블로그

  • 추천: NextAuth.js + JWT + Credentials
  • 이유: 완전 무료이고 설정이 간단하며 session 테이블을 관리할 필요가 없습니다.
  • 단점: 나중에 “사용자 강제 로그아웃” 같은 기능을 추가하려면 전환 비용이 큽니다.

상황 2: 기업용 애플리케이션/SaaS 제품(예산 있음)

  • 추천: Clerk
  • 이유: 30분이면 설정할 수 있고 UI가 보기 좋으며 사용자 관리 기능이 완성되어 있어 개발 시간을 40~80시간 절약할 수 있습니다.
  • 단점: 월간 활성 사용자 10,000명을 초과하면 비용이 들고 사용자 지정에 제약이 있습니다.
  • 추가 안내: Supabase 데이터베이스를 사용한다면 Supabase Auth도 좋습니다(무료 한도 50,000 MAU).

상황 3: 기업용 애플리케이션(예산 없음/완전한 제어 필요)

  • 추천: NextAuth.js + Database Session + OAuth 제공자(Google/GitHub)
  • 이유: 완전 무료이면서 기능이 충분하고 사용자를 강제로 로그아웃시킬 수 있습니다.
  • 단점: 로그인 UI를 직접 개발하고 데이터베이스도 관리해야 합니다.

상황 4: 기존 사용자 시스템이 있고 인증 계층만 추가하려는 경우

  • 추천: NextAuth.js + Credentials + JWT
  • 이유: 유연하고 기존 데이터베이스 구조를 침범하지 않습니다.
  • 주의: 비밀번호 암호화, 무차별 대입 공격 방지 등의 보안 조치를 직접 구현해야 합니다.

제 경험을 들자면 첫 프로젝트에서는 JWT를 사용했다가 6개월 뒤 사용자 관리 기능이 필요해져 Database Session으로 전환하는 데 이틀이 걸렸습니다. 두 번째 프로젝트는 처음부터 요구 사항을 평가해 곧바로 Database Session을 선택했고 많은 수고를 덜었습니다.

심화 학습 경로

Credentials 로그인을 실행하는 데 성공했다면 다음 내용을 배워 보세요.

1. OAuth 제공자(더 간단하며 먼저 배우길 권장)

  • Google, GitHub 로그인은 Credentials보다 훨씬 간단합니다.
  • 비밀번호 암호화나 사용자 가입을 관리할 필요 없이 OAuth 제공자가 대신 처리합니다.
  • 한 번의 클릭으로 로그인할 수 있어 사용자 경험도 더 좋습니다.

2. 미들웨어(Middleware): 경로 보호

  • 앞에서 살펴본 것처럼 코드 한 줄로 전체 디렉터리를 보호할 수 있습니다.
  • 페이지마다 session을 따로 확인하는 것보다 훨씬 편리합니다.

3. 다중 역할 권한 관리(RBAC)

  • session에 role 필드를 저장합니다.
  • role에 따라 서로 다른 콘텐츠나 권한을 제공합니다.
  • 더 나아가 세밀한 권한 제어를 위한 CASL 라이브러리를 배울 수 있습니다.

4. 이메일 인증과 비밀번호 재설정

  • NextAuth.js의 Email Provider(이메일 로그인)
  • 비밀번호 찾기 기능 직접 구현(재설정 링크 발송)

참고 자료

공식 문서(필수):

실용 튜토리얼:

GitHub 논의(문제가 생겼을 때 검색):

경쟁 제품 비교(선택에 도움):

결론

길게 설명했지만 NextAuth.js의 핵심은 세 가지 결정입니다.

  1. 어떤 로그인 방식을 사용할 것인가? Credentials 또는 OAuth? 대부분의 경우 OAuth(Google/GitHub)가 더 간단합니다.
  2. 세션을 어떻게 저장할 것인가? JWT 또는 Database? 개인 프로젝트는 JWT, 기업용 애플리케이션은 Database가 적합합니다.
  3. 사용자를 어떻게 검증할 것인가? Credentials는 검증 로직을 직접 작성하고, OAuth는 제공자에게 맡깁니다.

제 조언은 먼저 이 글의 “최소 실행 버전”을 작동시켜 보는 것입니다. 로그인에 성공한 뒤 기능을 하나씩 추가하세요. 처음부터 모든 설정을 이해하려고 하면 금방 지칠 수 있습니다.

NextAuth.js에는 분명 학습 곡선이 있지만, 익히고 나면 인증 흐름을 완전히 직접 제어할 수 있습니다. 빠르게 만들고 싶다면 Clerk가 좋은 선택이고, 비용을 아끼면서 배우고 싶다면 NextAuth.js에 시간을 투자할 가치가 있습니다.

마지막으로 댓글에서 어떤 문제를 겪었는지 알려 주세요. 어느 설정 단계에서 막혔나요? JWT와 Session 중 무엇을 고를지 고민되나요?

추천 글:

  • 다음 글에서는 경로 보호, A/B 테스트 등의 상황을 포함한 “Next.js Middleware 완벽 가이드”를 다룰 예정입니다.

  • 사용자 권한 관리에 관심이 있다면 이전에 작성한 “RBAC 권한 설계 실전”도 확인해 보세요.

NextAuth.js Credentials 로그인 설정 전체 과정

설치부터 사용자 이름과 비밀번호 로그인, 세션 관리 구현까지 다루는 전체 절차

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: NextAuth.js 설치 및 초기화

    의존성 설치:
    • npm install next-auth
    • app/api/auth/[...nextauth]/route.ts 생성

    기본 설정:
    • NEXTAUTH_URL 설정(로컬: http://localhost:3000)
    • NEXTAUTH_SECRET 설정(임의 문자열 생성)
    • providers 배열 설정
  2. 2

    Step 2: Credentials Provider 설정

    인증 로직 구현:
    1. CredentialsProvider의 authorize 함수에서 사용자 검증
    2. 데이터베이스에서 사용자 조회(또는 하드코딩으로 테스트)
    3. 비밀번호 검증(bcrypt 등의 라이브러리 사용)
    4. 사용자 객체 반환(id, name, email 등 포함)

    주의:
    • authorize 함수는 사용자 객체 또는 null을 반환해야 함
    • 반환된 객체는 session에 저장됨
    • 비밀번호 검증은 서버에서 수행해야 함
  3. 3

    Step 3: 세션 전략 선택(JWT 또는 Session)

    JWT 전략(기본값):
    • 상태를 저장하지 않는 애플리케이션에 적합
    • 세션 정보를 JWT token에 저장
    • 데이터베이스 불필요
    • 설정: session: { strategy: 'jwt' }

    Session 전략(데이터베이스 필요):
    • 로그인을 취소해야 하는 애플리케이션에 적합
    • 세션 정보를 데이터베이스에 저장
    • Adapter 설정 필요(Prisma, MongoDB 등)
    • 설정: session: { strategy: 'database' }
  4. 4

    Step 4: Callbacks로 사용자 지정 흐름 설정

    자주 쓰는 Callbacks:
    • signIn: 로그인 허용 여부 제어
    • jwt: JWT token 내용 사용자 지정(JWT 전략)
    • session: session 내용 사용자 지정

    예시:
    callbacks: {
    async jwt({ token, user }) {
    if (user) token.role = user.role
    return token
    },
    async session({ session, token }) {
    session.user.role = token.role
    return session
    }
    }
  5. 5

    Step 5: 로그인 페이지와 컴포넌트 만들기

    NextAuth.js API 사용:
    • signIn('credentials', { username, password }): 로그인 실행
    • signOut(): 로그아웃
    • useSession(): 현재 세션 가져오기
    • SessionProvider: 애플리케이션을 감싸 세션 컨텍스트 제공

    예시:
    const { data: session } = useSession()
    if (session) {
    return <div>로그인됨: {session.user.name}</div>
    }
  6. 6

    Step 6: 테스트 및 디버깅

    테스트 항목:
    • 올바른 사용자 이름과 비밀번호로 로그인되는지 테스트
    • 잘못된 자격 증명이 거부되는지 테스트
    • 세션이 유지되는지 테스트
    • 로그아웃 시 세션이 삭제되는지 테스트

    디버깅 방법:
    • 브라우저 콘솔 오류 확인
    • 서버 로그 확인
    • NextAuth.js 디버그 모드 사용
    • 환경 변수가 올바르게 설정되었는지 확인

FAQ

NextAuth.js는 Clerk, Supabase Auth와 무엇이 다른가요?
NextAuth.js:
• 완전 무료 오픈 소스인 자체 호스팅 방식
• 로그인 UI와 사용자 관리를 직접 구현해야 함
• 대신 모든 것을 직접 제어할 수 있음

Clerk:
• 완성된 UI와 관리 화면 제공
• 월간 활성 사용자 10,000명을 초과하면 유료

Supabase Auth:
• Supabase 데이터베이스를 사용하는 프로젝트에 적합
• 통합은 간단하지만 데이터베이스에 종속됨
JWT와 Session 중 무엇을 선택해야 하나요?
JWT는 상태를 저장하지 않는 애플리케이션과 개인 프로젝트에 적합하며 데이터베이스가 필요 없지만, 개별 세션을 취소할 수 없습니다. Session은 기업용 애플리케이션이나 로그인을 취소해야 하는 상황에 적합합니다. 데이터베이스 Adapter가 필요하지만 세션을 정밀하게 제어할 수 있습니다.
Credentials 로그인은 안전한가요?
안전하지만 다음 사항을 지켜야 합니다:
1) 비밀번호를 반드시 암호화해 저장(bcrypt 등의 라이브러리 사용)
2) 검증 로직을 반드시 서버에서 수행
3) HTTPS로 전송
4) 비밀번호 강도 요구 사항 구현
5) 무차별 대입 공격을 막기 위한 CAPTCHA 도입 고려
로그인 페이지를 사용자 지정하려면 어떻게 하나요?
NextAuth 설정에서 pages 옵션을 사용합니다: pages: { signIn: '/auth/login' }.

그런 다음 사용자 지정 로그인 페이지를 만들고 signIn('credentials', { username, password })로 로그인을 실행합니다.

UI를 완전히 직접 만들고 NextAuth.js API만 사용할 수도 있습니다.
현재 로그인한 사용자를 어떻게 가져오나요?
클라이언트에서는 useSession() hook을 사용합니다:
const { data: session } = useSession()

서버에서는 getServerSession()을 사용합니다:
const session = await getServerSession(authOptions)

session 객체에는 사용자 정보(id, name, email 등)가 들어 있습니다.
역할과 권한 제어는 어떻게 구현하나요?
callbacks에서 JWT 또는 Session 내용을 사용자 지정해 role 필드를 추가합니다. 그런 다음 페이지나 API 경로에서 session.user.role을 확인하고 역할에 따라 접근 허용 여부를 결정합니다. Middleware로 전역 권한 검사를 구현할 수도 있습니다.
NextAuth.js는 어떤 데이터베이스를 지원하나요?
NextAuth.js는 Adapter를 통해 Prisma(PostgreSQL, MySQL, SQLite 등), MongoDB, TypeORM, Drizzle ORM 등 다양한 데이터베이스를 지원합니다. Adapter를 사용하면 Session 전략으로 자동 전환되며 세션 정보는 데이터베이스에 저장됩니다.

5분 읽기 · 게시일: 2025년 12월 19일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog