SHIN STORYSHIN STORY
홈포스트C#TypeScriptNext.jsNode.js시리즈
</>SHIN STORY

sdf

탐색

  • 홈
  • 모든 포스트
  • 시리즈
  • 검색

카테고리

  • C#
  • TypeScript
  • Next.js
  • Node.js
  • 알고리즘
  • 개발 도구

© 2025 Shin Blog. All rights reserved.

GitHubRSS
목록으로
Next.js#TypeScript#Next.js#NextAuth

4편. NextAuth.js로 관리자 인증 구현하기

SHIN

2025년 3월 15일9분 읽기0
📚

Next.js App Router로 풀스택 블로그 만들기

9편
  1. 11편. 프로젝트 소개와 기술 스택 선정
  2. 22편. 프로젝트 초기 설정과 디렉토리 구조
  3. 33편. Prisma 스키마 설계와 마이그레이션
  4. 4편. NextAuth.js로 관리자 인증 구현하기현재
  5. 55편. 관리자 대시보드 — 포스트 CRUD 구현
  6. 66편. 블로그 프론트엔드와 마크다운 렌더링
  7. 77편. 검색·카테고리·태그·시리즈 기능 구현
  8. 98편. SEO 최적화 — 메타태그·sitemap·RSS
  9. 99편. Vercel 배포와 성능 최적화

4편. NextAuth.js로 관리자 인증 구현하기

NextAuth.js v5 설정

v5는 App Router에 맞게 전면 재작성되었습니다. 설정 파일 하나에서 핸들러, 헬퍼, 미들웨어를 모두 내보냅니다.

CODE
// src/lib/auth.ts
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import bcrypt from 'bcryptjs'
import { prisma } from './prisma'

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Credentials({
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize({ email, password }) {
        if (!email || !password) return null
        const user = await prisma.user.findUnique({ where: { email: String(email) } })
        if (!user?.password) return null
        const ok = await bcrypt.compare(String(password), user.password)
        if (!ok) return null
        return { id: user.id, name: user.name, email: user.email, role: user.role }
      },
    }),
  ],
  callbacks: {
    jwt({ token, user }) {
      if (user) token.role = (user as any).role
      return token
    },
    session({ session, token }) {
      if (session.user) (session.user as any).role = token.role
      return session
    },
  },
  pages: { signIn: '/admin/login' },
})

API Route 핸들러

CODE
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/lib/auth'
export const { GET, POST } = handlers

단 두 줄로 모든 인증 엔드포인트가 생성됩니다.

미들웨어로 라우트 보호

CODE
// src/middleware.ts
import { auth } from '@/lib/auth'
import { NextResponse } from 'next/server'

export default auth((req) => {
  const isAdminRoute = req.nextUrl.pathname.startsWith('/admin')
  const isLoginPage = req.nextUrl.pathname === '/admin/login'

  if (isAdminRoute && !isLoginPage && !req.auth) {
    return NextResponse.redirect(new URL('/admin/login', req.url))
  }
  if (isLoginPage && req.auth) {
    return NextResponse.redirect(new URL('/admin', req.url))
  }
})

export const config = {
  matcher: ['/admin/:path*'],
}

미들웨어는 Edge Runtime에서 실행되어 응답 속도가 매우 빠릅니다.

로그인 페이지

CODE
// src/app/admin/login/page.tsx
'use client'
import { signIn } from 'next-auth/react'
import { useState } from 'react'
import { useRouter } from 'next/navigation'

export default function LoginPage() {
  const [error, setError] = useState('')
  const router = useRouter()

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const fd = new FormData(e.currentTarget)
    const result = await signIn('credentials', {
      email: fd.get('email'),
      password: fd.get('password'),
      redirect: false,
    })
    if (result?.error) {
      setError('이메일 또는 비밀번호가 올바르지 않습니다.')
    } else {
      router.push('/admin')
    }
  }

  return (
    <form onSubmit={handleSubmit} className="max-w-sm mx-auto mt-20 space-y-4">
      <h1 className="text-2xl font-bold">관리자 로그인</h1>
      {error && <p className="text-red-400 text-sm">{error}</p>}
      <input name="email" type="email" placeholder="이메일" className="input w-full" />
      <input name="password" type="password" placeholder="비밀번호" className="input w-full" />
      <button type="submit" className="btn-primary w-full">로그인</button>
    </form>
  )
}

서버 컴포넌트에서 세션 확인

CODE
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'

export default async function AdminLayout({ children }: { children: React.ReactNode }) {
  const session = await auth()
  if (!session) redirect('/admin/login')

  return (
    <div className="flex">
      <Sidebar />
      <main className="flex-1 p-8">{children}</main>
    </div>
  )
}

미들웨어와 레이아웃 양쪽에서 인증을 확인하면 더 안전합니다.

비밀번호 해싱

CODE
import bcrypt from 'bcryptjs'

// 저장 시
const hashed = await bcrypt.hash(plainPassword, 12) // cost factor 12

// 검증 시
const match = await bcrypt.compare(plainPassword, hashed)

cost factor가 높을수록 안전하지만 해싱 시간이 길어집니다. 12가 일반적인 균형점입니다.

다음 편에서는 관리자 대시보드의 포스트 작성/수정/삭제 기능을 구현합니다.

공유
S

SHIN

.NET 개발자입니다

GitHub

이전 포스트

3편. Prisma 스키마 설계와 마이그레이션

다음 포스트

5편. 관리자 대시보드 — 포스트 CRUD 구현

같은 카테고리 포스트

9편. Vercel 배포와 성능 최적화

2025년 4월 10일· 7분

8편. SEO 최적화 — 메타태그·sitemap·RSS

2025년 4월 5일· 2분

7편. 검색·카테고리·태그·시리즈 기능 구현

2025년 3월 30일· 8분

댓글