NextAuth로는 다양한 소셜 로그인(OAuth) 및 커스텀 로그인을 구현할 수 있다.
Credential Providers를 통해 커스텀 로그인을 구현해 보았다.
NextAuth는 기본적으로 로그인 페이지를 제공해 준다. 하지만 그대로 사용하기엔 개발 중인 웹의 테마와 어울리지 않는다. 그래서 커스텀 로그인 페이지를 만들어서 적용해 주었다.
1. [...nextauth].ts 코드 수정
// pages/api/auth/[...nextauth].ts
import { connectDB } from '@/util/database'
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import bcrypt from 'bcrypt'
export const authOptions: any = {
providers: [
CredentialsProvider({
// 1. 로그인 페이지 폼 자동 생성
name: 'credentials',
credentials: {
id: { label: '아이디', type: 'text' },
password: { label: '비밀번호', type: 'password' },
},
// 2. 로그인 요청 시 실행
async authorize(credentials) {
if (credentials !== undefined) {
let db = (await connectDB).db('cinema')
let user = await db
.collection('user_cred')
.findOne({ id: credentials.id })
if (!user) {
console.log('해당 아이디가 없습니다.')
return null
}
const pwcheck = await bcrypt.compare(
credentials.password,
user.password,
)
if (!pwcheck) {
console.log('비밀번호가 틀렸습니다.')
return null
}
return user as any
}
},
}),
],
// 3. jwt 설정
session: {
strategy: 'jwt',
maxAge: 3 * 24 * 60 * 60, // 로그인 유지 기간 (=3일)
},
callbacks: {
// 4. jwt 만들 때 실행
jwt: async ({ token, trigger, user, session }: { token: any; trigger: any; user: any, session:any }) => {
if (user) {
token.user = {}
token.user.id = user.id
token.user.name = user.name
}
if (trigger === "update" && session.name) { // session 업데이트 (닉네임 수정)
token.user.name = session.name
}
return token
},
// 5. user session이 조회될 때 마다 실행
session: async ({ session, token }: { session: any; token: any }) => {
session.user = token.user
return session
},
},
// 커스텀 로그인 페이지를 위해 추가된 부분
pages: {
signIn: '/signin',
},
// -----------------------------------
secret: process.env.NEXTAUTH_SECRET,
}
export default NextAuth(authOptions)
2. 커스텀 로그인 페이지 만들기
app/signin/page.tsx
'use client'
import styles from '@/app/styles/my.module.css'
import Link from 'next/link'
import { signIn } from 'next-auth/react'
import { useState, FormEvent } from 'react'
import { useRouter } from 'next/navigation'
export default function SignIn() {
const [id, setId] = useState('')
const [password, setPassword] = useState('')
const [blankPw, setBlankPw] = useState(false)
const [blankId, setBlankId] = useState(false)
const [error, setError] = useState(false)
const router = useRouter()
const onChangeId = (e: React.ChangeEvent<HTMLInputElement>) => {
setId(e.target.value)
}
const onChangePw = (e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value)
}
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (id.length <= 0)
setBlankId(true)
if (password.length <= 0)
setBlankPw(true)
if (!blankId && !blankPw) {
// 로그인 부분
await signIn("credentials", {
id: id,
password: password,
redirect: false,
}).then((result) => {
if (result?.ok)
router.push('/')
if (result?.error)
setError(true)
});
}
}
return (
<div className={styles.wrapper}>
<form onSubmit={onSubmit} className={styles.form}>
<input
name="id"
type="text"
value={id}
onChange={onChangeId}
onBlur={()=>setBlankId(id.length <= 0)}
placeholder="아이디"
className={`${styles.input} ${blankId ? styles['input-error'] : ''}`}
/>
{blankId && <span>아이디를 입력해주세요.</span>}
<input
name="password"
type="password"
value={password}
onChange={onChangePw}
onBlur={()=>setBlankPw(password.length <= 0)}
placeholder="비밀번호"
className={`${styles.input} ${blankPw ? styles['input-error'] : ''}`}
/>
{blankPw && <span>비밀번호를 입력해주세요.</span>}
{error && <span>아이디 또는 비밀번호를 잘못 입력했습니다. 다시 확인해주세요.</span>}
<button type="submit">로그인</button>
<Link href='/register'>회원가입</Link>
</form>
</div>
)
}
로그인에 성공했을 경우에만 홈 화면으로 이동시키기 위해 redirect를 false로 설정하고, result를 이용하여 페이지 이동 또는 오류 메시지 출력을 구현하였다.
3. 로그인된 사용자 정보 출력하기
// app/my/page.tsx
import { getServerSession } from 'next-auth'
import { authOptions } from '@/pages/api/auth/[...nextauth]'
import NotAllowed from '../components/NotAllowed'
import MyPage from '../components/MyPage'
export default async function My() {
let session: any = await getServerSession(authOptions)
return session ? (
<>
<MyPage user={session.user} />
</>
) : (
<NotAllowed />
)
}
getServerSession을 통해 session 정보를 가져온다. getServerSession은 서버 컴포넌트에서만 사용이 가능하며, 클라이언트 컴포넌트에서 사용하려면 getSession을 사용해야 한다.
현재 로그인 상태인 경우에만 마이페이지를 출력하고, 그렇지 않을 경우 로그인을 유도하는 컴포넌트를 보여주도록 하였다.
4. 로그인된 사용자의 정보 업데이트 및 Session 업데이트
마이페이지 > 설정페이지(my/setting)에서 사용자의 닉네임을 바꿀 수 있도록 하였다. 닉네임을 바꾸면 DB 내에 있는 사용자의 닉네임 또한 변경되며, 닉네임은 session 정보에도 포함되어 있기 때문에 session 정보 또한 업데이트가 이루어져야 한다.
업데이트를 위해 useSession을 사용하는데 사용 전에 먼저 Provider를 불러온 후 layout.tsx에서 {children}을 감싸줘야 한다.
// app/providers.tsx
'use client'
import { SessionProvider } from 'next-auth/react'
type Props = {
children?: React.ReactNode
}
export const NextAuthProvider = ({ children }: Props) => {
return <SessionProvider>{children}</SessionProvider>
}
// app/layout.tsx
import Navbar from './components/Navbar'
import './globals.css'
import './fonts/font.css'
import { NextAuthProvider } from './providers'
export const metadata = {
title: 'Cinema',
description: 'Movie app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<Navbar />
<div className="container">
<NextAuthProvider>
{children}
</NextAuthProvider>
<footer> (생략) </footer>
</div>
</body>
</html>
)
}
사용자 정보가 필요한 컴포넌트를 Provider로 감싸주었다.
만약 Navbar에 사용자의 닉네임과 같은 로그인 여부에 따라 변경되는 요소가 있다면 Navbar 또한 감싸주어야 한다.
회원 정보 수정 폼
//app/components/SettingForm.tsx
'use client'
import styles from '@/app/styles/my.module.css'
import { useState, FormEvent } from 'react'
import { useRouter } from 'next/navigation'
import { useSession } from 'next-auth/react'
export default function SettingForm({ user }: { user: any }) {
const [name, setName] = useState(user.name)
const [blankName, setBlankName] = useState(false)
const [change, setChange] = useState(false) // 정보에 변화가 있는지
const { data: session, status, update } = useSession()
const router = useRouter()
const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
setChange(true)
setName(e.target.value)
}
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!blankName) {
fetch('/api/auth/changeInfo', {
method: 'POST',
body: JSON.stringify({ id: user.id, name }),
}).then((res) => {
if (res.status === 200) {
if (status === "authenticated") update({ name })
window.alert('변경되었습니다.')
router.refresh()
router.push('/my')
}
})
}
}
return (
<div className={styles.wrapper}>
<form onSubmit={onSubmit} className={styles.form}>
<div className={`${styles['info-wrapper']} ${styles.mb}`}>
<p>아이디</p>
<span>{user.id}</span>
</div>
<div className={styles['info-wrapper']}>
<p>닉네임</p>
<input
name="id"
type="text"
value={name}
onChange={onChangeName}
onBlur={() => setBlankName(name.length <= 0)}
placeholder="닉네임"
className={`${styles.input} ${
blankName ? styles['input-error'] : ''
}`}
/>
</div>
<span className={blankName ? '' : `${styles.hidden}`}>
닉네임을 입력해주세요.
</span>
<button type="submit" disabled={!change}>
변경사항 저장
</button>
</form>
</div>
)
}
폼 제출 시 changeInfo에서 DB 업데이트가 이루어지고, 성공적으로 수행되었다면 session 업데이트를 진행한다.
update 함수를 호출하면 [...nextauth].ts의
if (trigger === "update" && session.name) {
token.user.name = session.name
}
이 부분이 실행되면서 토큰의 닉네임이 업데이트되고, session을 조회할 때
session: async ({ session, token }: { session: any; token: any }) => {
session.user = token.user
return session
},
이 부분이 실행되면서 업데이트된 session을 받게 된다!
바꾼 닉네임이 DB에 적용되도록 하는 것은 쉬웠지만 session 업데이트가 쉽지 않았다.. 특히 session 업데이트가 서버 측에서 이루어져도 페이지를 새로고침 시켜주지 않으면 클라이언트 측에서는 반영이 되지 않는다.
session 업데이트 후 마이페이지로 이동하게 하였는데, useRouter()의 router.push()만으로는 새로고침 없이 페이지 이동이 되기 때문에 마이페이지 내에 있는 닉네임이 바로 수정되지 않았다. window.loaction.href를 통해 페이지 이동을 시켜봐도 마찬가지였다. 그래서 설정 페이지 내에서 router.refresh()를 통해 새로고침을 한 번 한 후에 router.push()로 페이지 이동을 하니 바뀐 결과가 바로 반영되었다. 살짝 느리거나 거슬리는 부분이 있을까 걱정했는데 아주 자연스럽게 페이지 이동이 이루어졌다!
결과물
References
'Develop > NextJS' 카테고리의 다른 글
[Next.js] 서버 & 클라이언트 컴포넌트에서 토큰 관리하기 (0) | 2024.07.24 |
---|---|
[Next.js] Data fetching과 Caching Mechanism (0) | 2024.06.12 |
[Next.js] Next.js 작업물 Vercel로 배포하기 (0) | 2023.07.21 |
[Next.js] Next.js에 MongoDB Atlas 연결하기 (+TypeScript) (0) | 2023.07.07 |
[Next.js] Next.js에 Prettier 적용하기 (2) | 2023.06.08 |