픽토스 ver2 개발 중 모든 페이지로의 리다이렉트가 눈에 띄게 느린 문제를 겪었다.
프로젝트에서 OAuth 구현을 간편하게 하기 위해 next-auth를 사용했는데, Next.js의 미들웨어를 활용하여 Protected Route
를 구현한 부분에서 성능 문제가 발생한 것을 발견했다.
import { NextRequest, NextResponse } from 'next/server'
import { auth } from './app/api/auth/[...nextauth]/auth'
interface Routes {
[key: string]: boolean
}
const publicOnlyUrls: Routes = {
'/': true,
'/sign-in': true,
}
export async function middleware(request: NextRequest) {
const session = await auth()
const exists = publicOnlyUrls[request.nextUrl.pathname]
if (!session?.user.id) {
if (!exists) {
return NextResponse.redirect(new URL('/', request.url))
}
} else {
if (exists) {
return NextResponse.redirect(new URL('/main', request.url))
}
}
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
위의 미들웨어는 await auth()
를 통해 next-auth의 세션에 접근하고 있는데, 여기서 실행되는 JWT 콜백의 구현부에 문제가 있었다.
export const {
auth,
handlers: { GET, POST },
signIn,
signOut,
} = NextAuth({
providers: [Kakao, Google],
callbacks: {
jwt: async ({ token, account }) => {
if (account) {
try {
const { accessToken } = await signInAPI({
socialPlatform: account.provider.toUpperCase() as 'GOOGLE' | 'KAKAO',
accessToken: account.access_token as string,
})
token.account = account
token.accessToken = accessToken
} catch (error) {
throw new Error('Failed to get backend access token')
}
}
try {
const user = await getUser({
accessToken: token.accessToken as string,
})
token.userDTO = user
} catch (error) {
throw new Error('Failed to get user')
}
return token
},
// 생략...
},
}) satisfies NextAuthResult
로그인 시 백엔드 서버로부터 accessToken을 받아오고, 이를 통해 사용자의 정보 또한 요청한 후 클라이언트의 세션에 저장한다. 여기서 문제는 getUser 함수가 항상 실행된다는 것이다.
로그인 상황이 아닐 경우 매개변수의 account 속성 값은 undefined이며, 이로 인해 signInAPI는 실행되지 않지만 getUser 함수는 항상 실행된다. 즉, next server에 요청을 보낼 때마다 매번 사용자의 정보를 가져오려고 시도한다.
getUser 요청의 응답을 캐싱하더라도, edge runtime 환경에서 실행되는 미들웨어에서는 이 시간조차 크게 영향을 미친다.
router.push 혹은 Link 컴포넌트를 통해 next 서버에 페이지 요청 시, 미들웨어의 await auth() 실행이 끝난 후에야 사용자의 요청에 대한 응답이 처리되기 때문이다.
이러한 이유로 미들웨어는 최대한 가벼워야 한다.
getUser 요청을 하는 try-catch 블록 또한 if (account)
분기 문 내에 넣어 로그인 상황이 아닌 경우에는 실행되지 않도록 수정했다.
jwt: async ({ token, account }) => {
if (account) {
try {
const { accessToken } = await signInAPI({
socialPlatform: account.provider.toUpperCase() as 'GOOGLE' | 'KAKAO',
accessToken: account.access_token as string,
})
token.account = account
token.accessToken = accessToken
} catch (error) {
throw new Error('Failed to get backend access token')
}
try {
const user = await getUser({
accessToken: token.accessToken as string,
})
token.userDTO = user
} catch (error) {
throw new Error('Failed to get user')
}
}
return token
},
이를 통해 미들웨어의 성능을 최적화하고 페이지 로딩 속도를 개선할 수 있었다.