Next.js + Next Auth를 이용한 교육 정리 포스트이다.
Next Auth 5 버전을 기준으로 작성되었다.
next.js에서 주로 사용하는 오픈소스 인증 라이브러리이다.
기본적인 사용법은 다음과 같다.
공식문서에는 auth.ts라고 나와있는데, 프로젝트에서 사용하는 auth config파일이라고 생각하면 편하다.
NextAuth() 호출로 반환되는 handlers, signIn, signOut, auth, update를 프로젝트에서 사용할 수 있다.
import NextAuth from "next-auth"
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [],
})
return
- handlers: 프로젝트의 인증 관리를 위한 API 라우트(GET, POST 함수) 객체입니다.
- signIn: 사용자 로그인을 시도하는 비동기 함수입니다.
- signOut: 사용자 로그아웃을 시도하는 비동기 함수입니다.
- auth: 세션 정보를 반환하는 비동기 함수입니다.
- unstable_update(update): 세션 정보를 갱신하는 비동기 함수입니다.
호출 옵션으로 다음과 같은 항목을 지정할 수 있습니다.
providers를 제외하면, 모두 선택 항목이다.
params
- providers: Credentials, Google, GitHub 등의 인증 공급자를 지정합니다. (필수값)
- session: 세션 관리 방식을 지정합니다.
- pages: 사용자 정의 페이지 경로를 지정하며, 로그인 페이지의 기본값은 /auth/signin입니다.
- callbacks: 인증 및 세션 관리 중 호출되는 각 핸들러를 지정합니다.
- callbacks.signIn: 사용자 로그인을 시도했을 때 호출되며, true를 반환하면 로그인 성공, false를 반환하면 로그인 실패로 처리됩니다.
- callbacks.redirect: 페이지 이동 시 호출되며, 반환하는 값은 리다이렉션될 URL입니다.
- callbacks.jwt: JWT가 생성되거나 업데이트될 때 호출되며, 반환하는 값은 암호화되어 쿠키에 저장됩니다.
- callbacks.session: jwt 콜백이 반환하는 token을 받아, 세션이 확인될 때마다 호출되며, 반환하는 값은 클라이언트에서 확인할 수 있습니다. (2번 이상 호출될 수 있습니다)
import NextAuth from 'next-auth'
export const {
handlers,
signIn,
signOut,
auth,
unstable_update: update // Beta!
} = NextAuth({
providers: [
// ...
],
session: {
strategy: 'jwt', // JSON Web Token 사용
maxAge: 60 * 60 * 24 // 세션 만료 시간(sec)
},
pages: {
signIn: '/signin' // Default: '/auth/signin'
},
callbacks: {
signIn: async () => {
return true
},
jwt: async ({ token, user }) => {
return token
},
session: async ({ session, token }) => {
return session
}
}
})
사용자가 로그인(회원가입) => signIn => (redirect) => jwt => session
세션 업데이트 => jwt => session
세션 확인 => session
...
ref) 참고 : https://nextjs.org/docs/app/building-your-application/routing/route-handlers
라우트 핸들러는 app directory에서만 동작한다.
Route Handler는 임의의 http 요청에 대해 리스너를 시행시키는 것을 이야기하며 이 리스너에서 next.js에서 정의된 Response 객체를 반환하게 된다.
Auth.js는 /api/auth/ 이하 경로에서 인증을 처리한다. => next.js에서 동적경로로 route.ts파일을 매핑해줘야한다.
모든 하위 경로의 동적 일치(Catch all API routes)로 라우트를 제공하고, 기본 구성에서 반환하는 handlers 객체로 라우트의 GET과 POST 함수를 매핑한다.
라우트는 레이아웃이나 페이지같은 클라이언트 측 네비게이션에 참여하지 않고, page.js파일과 같은 경로에 존재 할 수 없다.
/app/api/auth/[...nextauth]/route.ts 이와 같이 동적으로, auth하위 경로에 대한 핸들러를 정의해준다.
/app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth'
export const { GET, POST } = handlers
// export const runtime = 'edge' // Optional!
ref) https://nextjs.org/docs/app/building-your-application/routing/middleware
Next.js 12버전부터 추가된 내장기능 => 매 요청이 완료되기전에 middleware.ts에 있는 코드를 실행해 전처리 과정을 진행 할 수 있다. (화면에서 경로를 이동하는 요청포함)
root 경로 아래에 middleware.ts 파일을 정의한다.
아래코드처럼 경로별 인증 여부를 확인한다고 할 때, matcher에 해당하는 경로에 올바른 권한이 있다면 , NextResponse.next()를 호출해 다음 요청으로 진행하고,
아니라면 다시 redirect시키기 가능.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { match } from 'path-to-regexp'
import { getSession } from '@/serverActions/auth'
const matchersForAuth = [
'/dashboard/:path*',
'/myaccount/:path*',
'/settings/:path*',
'...'
]
export async function middleware(request: NextRequest) {
if (isMatch(request.nextUrl.pathname, matchersForAuth)) {
return (await getSession()) // 세션 정보 확인
? NextResponse.next()
: NextResponse.redirect(new URL('/signin', request.url))
// : NextResponse.redirect(new URL(`/signin?callbackUrl=${request.url}`, request.url))
}
return NextResponse.next()
}
function isMatch(pathname: string, urls: string[]) {
return urls.some(url => !!match(url)(pathname))
}
ref) https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
ref) 서버컴포넌트 이해하기 : https://yozm.wishket.com/magazine/detail/2271/
Next.js는 기본적으로 ssr방식이고, 아무 선언을 하지 않았다면, 서버에서 렌더링되는 서버컴포넌트로 정의된다.
클라이언트 측에서 상호작용이 없는 경우 => 서버컴포넌트로 정의, 필요한 데이터를 가져와서 ssr 방식으로 렌더링하는 방식을 통해 리소스 절약이 가능하다.
기본적으로 use~~ hook을 사용 할 수 없다. 해당 hook들은 클라이언트측에서만 가능한 기능이기 때문.
// src/components/UserProfile.js
import { fetchUserData } from '../actions';
export default async function UserProfile({ userId }) {
const user = await fetchUserData(userId);
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
CSR방식으로 렌더링되는 컴포넌트들이다. (기본적인 React가 동작하는 컴포넌트방식과 유사함)
클라이언트 상호작용이나 hook을 사용해야 할때 'use client'로 선언해 클라이언트 컴포넌트로 사용한다.
// src/components/Counter.js
'use client'
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Next.js의 서버 액션
'use server' 키워드 함께 사용함 => 서버에서 실행되는 로직인데, 서버와 클라이언트 컴포넌트 모두 사용 할 수 있다.
데이터베이스에서 데이터 가져오기, 양식 제출 처리, 서버측 계산 수행과 같은 작업을 처리할 수 있습니다.
서버 작업은 민감한 논리와 작업이 서버에 유지되도록 하여 보안을 유지하는 데 도움이 되며 작업을 클라이언트에서 서버로 오프로드하여 성능을 향상시킬 수 있습니다.
클라이언트에서만 사용 가능한 api에는 접근 할 수 없다.(window, localStroage)
공통로직을 서버 클라이언트 컴포넌트에서 재사용 가능하고, 보안이 강화된다.
// src/actions.js
'use server';
export async function fetchUserData(userId) {
// Perform server-side operation
const userData = await getUserFromDatabase(userId);
return userData;
}
'use server'
import { auth, signIn, signOut, update } from '@/auth'
export const signInWithCredentials = async (formData: FormData) => {
await signIn('credentials', options)
// ...
}
export const signInWithGoogle = async () => {
await signIn('google', options)
// ...
}
export const signInWithGitHub = async () => {
await signIn('github', options)
// ...
}
export const signOutWithForm = async (formData: FormData) => {
await signOut()
}
export {
auth as getSession,
update as updateSession
}
/serverActions/auth.tsTS
'use server'
import { auth, signIn, signOut, update } from '@/auth'
export const signInWithCredentials = async (formData: FormData) => {
await signIn('credentials', {
username: formData.get('username') || '', // `'null'` 문자 방지
email: formData.get('email') || '',
password: formData.get('password') || '',
redirectTo: '/'
})
}
export const signOutWithForm = async (formData: FormData) => {
await signOut()
}
export {
auth as getSession,
update as updateSession
}
//signup
<form
action={signInWithCredentials}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'start',
gap: '10px'
}}>
...
</form>
authorize 함수의 credentials 매개변수는 서버 액션에서 호출한 signIn('credentials', 사용자정보) 메소드의 두 번째 인수(사용자정보)임.
또한 authorize 함수는 회원가입 및 로그인에 성공한 경우, 사용자의 ID(id), 표시 이름(name), 이메일(email), 프로필 이미지(image)의 정해진 속성으로 정보를 반환해야함.
signInWithCredentials가 호출되면 => providers Credentails로 로그인을 시도하게 됨.
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
export const {
// ...
} = NextAuth({
providers: [
Credentials({
authorize: async credentials => {
const { displayName, email, password } = credentials
let user = { id: '', name: '', email: '', image: '' }
// 사용자 이름이 있는 경우, 회원가입!
if (displayName) {
// <회원가입 로직 ...>
return user
}
// <로그인 로직 ...>
return user
}
})
],
// ...
})
프로젝트에서 API를 액세스 토큰과 함께 요청해야 하는 경우, 사용자 세션 정보에 액세스 토큰을 추가할 수 있다.
authorize 함수에서 반환하는 사용자 정보에 accessToken 속성을 추가하고, 세션까지 전달해야함.
authorize 함수에서 반환하는 사용자 정보는, 로그인이 성공하면 callbacks.jwt 함수의 user 변수로 전달되고, callbacks.jwt 함수에서 반환하는 토큰 정보는 callbacks.session 함수의 token 변수로 전달됨.
마지막으로 callbacks.session 함수에서 반환하는 세션 정보는 각 페이지에서 사용할 수 있다. (getSession)
해당 속성은 auth 라이브러리 interface를 override해서 정의한다.
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
export const {
// ...
} = NextAuth({
providers: [
Credentials({
authorize: async credentials => {
const { username, email, password } = credentials
let user = { id: '', name: '', email: '', image: '' }
// 사용자 이름이 있는 경우, 회원가입!
if (username) {
// <회원가입 로직 ...>
return {
...user,
accessToken: '<ACCESS_TOKEN>'
}
}
// <로그인 로직 ...>
return {
...user,
accessToken: '<ACCESS_TOKEN>'
}
}
})
],
// ...
callbacks: {
signIn: async () => {
return true
},
jwt: async ({ token, user }) => {
if (user?.accessToken) {
token.accessToken = user.accessToken
}
return token
},
session: async ({ session, token }) => {
if (token?.accessToken) {
session.accessToken = token.accessToken
}
return session
},
// ...
}
})
일단 google 소셜로그인을 사용하려면 => Google로부터 OAuth에 대한 동의를 받아야함.
해당과정은 ref의 블로그를 참고
'use server'
import { auth, signIn, signOut, update } from '@/auth'
export const signInWithGoogle = async () => {
await signIn('google', { redirectTo: '/' })
}
export const signOutWithForm = async (formData: FormData) => {
await signOut()
}
export {
auth as getSession,
update as updateSession
}
provider로 Google을 사용해서 로직을 작성한다.
만약 signin 함수에서 => error가 발생한다면 해당 쿼리스트링으로 redirect 시키는데, 실제로 error가 발생한 것이 아니므로 해당 에러를 처리하는 페이지를 따로 만들어야함.
sign in 함수가 true을 반환한다면 로그인을 시도했던 페이지로 돌아간다.
import NextAuth from 'next-auth'
import Google from 'next-auth/providers/google'
export const {
// ...
} = NextAuth({
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
authorization: {
params: {
prompt: 'consent' // 사용자에게 항상 동의 화면을 표시하도록 강제!
}
}
})
],
session: {
strategy: 'jwt',
maxAge: 60 * 60 * 24 // 1 day
},
pages: {
signIn: '/signin'
},
callbacks: {
signIn: async ({ account, profile }) => {
if (account?.provider === 'google') {
// <사용자 확인 후 회원가입 또는 로그인...>
return !!profile?.email_verified
}
if (error) {
return `/error?message=${encodeURIComponent('<ERROR_MESSAGE>')}`
} //error경로에 있는 page로 리다이렉트
return true
},
jwt: async ({ token, user, trigger, session }) => {
if (user?.accessToken) {
token.accessToken = user.accessToken
}
if (trigger === 'update' && session) {
token = { ...token, ...session.user }
}
return token
},
session: async ({ session, token }) => {
if (token?.accessToken) {
session.accessToken = token.accessToken
}
return session
}
}
})
ref) https://www.heropy.dev/p/MI1Khc
https://authjs.dev/getting-started/migrating-to-v5