안녕하세요. 제로초입니다.
많은 분들이 next-auth에서 한 번 고통을 겪어보셨을 것 같은데요. 아직 베타라서 그동안 좀 불안정한 게 많았으나 이제야 좀 잡혀가는 듯합니다. 그래서 강의에서는 아직 기능이 없어 다루지 못했다가 추가된 것들 세 가지를 소개해드리겠습니다.
signIn시 프론트에서 서버 에러 받기
로그인 시 서버에서는 다양한 에러를 줄 수 있습니다. 예를 들어 1. 유저가 없는 경우 2. 비밀번호가 틀린 경우 3. 기타 등등. 그런데 이런 걸 프론트에 넘겨야 상황에 맞는 메시지를 표시할 것 아니겠습니까? 근데 이 기본적인 기능이 지금까지 없다가 이제야 추가되었습니다.
auth.ts에서 다음과 같이 수정합니다.
import NextAuth, {CredentialsSignin} from "next-auth"
...
providers: [
CredentialsProvider({
async authorize(credentials) {
const authResponse = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/login`, {
...
})
// 여기 주목!!! 서버에서 에러가 발생할 때 그 에러 내용이 서버에 담겨 있을 겁니다.
console.log(authResponse.status, authResponse.statusText)
if (!authResponse.ok) {
const credentialsSignin = new CredentialsSignin();
if (authResponse.status === 404) {
credentialsSignin.code = 'no_user';
} else if (authResponse.status === 401) {
credentialsSignin.code = 'wrong_password';
}
throw credentialsSignin;
}
const user = await authResponse.json()
console.log('user', user);
// id, name, image, email만 허용
return {
id: user.id,
name: user.nickname,
image: user.image,
}
},
}),
]
이제 return null 하지말고 CredentialsSignin 에러를 throw 하면 됩니다. 에러의 속성인 code에 에러 메시지를 적으면 되는데 여기의 코드를 프론트에서 받으려면 signIn 부분을 redirect: true로 만들어주어야 합니다. redirect: false면 무조건 response.ok가 true면서 에러 정보는 없습니다.
const response = await signIn("credentials", {
username: id,
password,
redirect: true, // true!!!
})
이렇게 바꾸면 이제 로그인 실패 시 페이지가 리다이렉트되면서 쿼리스트링으로 에러 코드가 같이 나오게 됩니다.
새로고침이라 불편하긴 하지만 next.js에는 useSearchParams()라는 쿼리스트링 조회 훅이 있으므로 code 값을 가져올 수 있고 화면에 code에 따른 메시지를 표시할 수 있습니다. redirect가 된다는 불편함은 있지만 어쨌든 구현은 가능하게 됩니다.
session 객체에 커스텀 데이터 넣기
공식 문서에 따르면 현재 authorize 함수의 return에는 id, email, name, image만 넣을 수 있습니다. 이것만 해도 제한이 큰데 id는 넣을 수 있다고 하면서도 넣으면 useSession()의 데이터에서는 사라져버립니다.
그럼 id는 어디로 갔냐?
다른 커스텀 데이터는 어떻게 넣냐?
하실 수 있는데 다음과 같이 session을 직접 만드실 수 있습니다.
auth.ts
export const {
handlers: { GET, POST },
auth,
signIn,
} = NextAuth({
pages: {
signIn: '/i/flow/login',
newUser: '/i/flow/signup',
},
callbacks: {
async session({ session, token }) {
console.log('session callback', session, token);
const authResponse = await fetch(내정보를 가져오는 서버 API);
const userData = await authResponse.json();
(session as any).userData = userData;
return session;
}
},
providers: [
CredentialsProvider({
async authorize(credentials) {
...
// id, name, image, email만 허용
return {
id: user.id,
name: user.nickname,
image: user.image,
}
이렇게 callbacks 속성에 async session 메서드를 작성하시면 됩니다. 여기 안에서 내 정보를 서버로부터 한 번 더 불러오면 됩니다. 그리고 그 응답값을 session 객체에 넣어서 반환하는 겁니다.
아까 사라졌던 id는 이 메서드의 token.sub에 들어 있습니다.
여기서 return하는 값이 auth()나 useSession()의 데이터가 됩니다. authorize에서 return한 값이 최종 값이 아니라 여기서 한 번 더 수정되는 것입니다.
이렇게 하면 auth()나 useSession()에서 user.userData를 확인하실 수 있습니다.
권한에 따라 페이지 접근하기
이제 session 객체에 커스텀 데이터를 넣을 수 있게 되었으므로 권한에 따라 페이지를 접근할 수 있습니다. callbacks.session async 함수에서 role 같은 것을 서버로부터 받아서 넣어주면 됩니다. session.userData.role에 admin 또는 normal 권한이 있다고 칩시다. 그리고 role이 admin인 경우 어드민 페이지(/admin)에 접속 가능하고 normal이면 안 된다고 해봅시다. 이걸 어떻게 구현할 수 있을까요?
현재 middleware.ts는 다음과 같이 되어있는데, 이러면 config에 적은 라우트에서만 실행되므로 config를 전부 제거합니다.
import { auth } from "./auth"
import {NextResponse} from "next/server";
export async function middleware() {
const session = await auth();
if (!session) {
return NextResponse.redirect('http://localhost:3000/i/flow/login');
}
}
// See "Matching Paths" below to learn more
export const config = {
matcher: ['/compose/tweet', '/home', '/explore', '/messages', '/search'],
}
그리고 middleware 함수 내부에 수동으로 작성하면 됩니다. request.nextUrl.pathname에 현재 접근하고자 하는 pathname이 들어있습니다.
import { auth } from "./auth"
import {NextResponse} from "next/server";
export async function middleware() {
const session = await auth();
if (['/compose/tweet', '/home', '/explore', '/messages', '/search'].includes(request.nextUrl.pathname) && !session) {
return NextResponse.redirect('http://localhost:3000/i/flow/login');
}
}
이렇게 수정한 후 저희는 auth()의 session에서 session.userData.role로 권한에 접근할 수 있으므로 다음과 같이 검사 후 리다이렉트 시키면 됩니다.
import { auth } from "./auth"
import {NextResponse} from "next/server";
export async function middleware() {
const session = await auth();
if (['/compose/tweet', '/home', '/explore', '/messages', '/search'].includes(request.nextUrl.pathname) && !session) {
return NextResponse.redirect('http://localhost:3000/i/flow/login');
}
if (request.nextUrl.pathname.startsWith('/admin') && session?.userData.role !== 'admin') {
return NextResponse.redirect('http://localhost:3000/권한없음_알리는_모달_주소');
}
}
이렇게 부족한 next-auth를 어떻게서든 활용해볼 수 있습니다.
다음에 또 업데이트 되는 사항이 있으면 알려드리겠습니다. 감사합니다!