저번에 CredentialsProvider를 만들고 옵션을 세팅해줬다. 이제 전에 만들어둔 login페이지와 연결해주도록 하겠다.
엄청난 로직을 추가해주는것이 아니라 nextAuth에서 제공하는 함수를 사용하면 된다. 그래서 로그인하는 함수는 signIn이다.
const onSubmit = async (data: LogInFormValueType) => {
try {
const response = await LoginAccess(data.id, data.password)
const { accessToken } = response.data
handleLoginSuccess(accessToken)
} catch (error: unknown) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handleLoginError(error as AxiosError<any>)
}
}
전에 팀원이 작성한 로그인 폼 이벤트이다. 이제 여기에 nextAuth를 적용해보겠다.
const onSubmit = async (data: LogInFormValueType) => {
const result = await signIn('credentials', {
id: data.id,
password: data.password,
redirect: false,
})
if (result?.ok) {
router.push('/mydashboard')
} else if (result?.error) {
handleLoginError(result.error)
}
}
우선 전의 코드에서 api요청하는 함수는 nextAuth provider옵션을 넣어줬기 때문에 삭제해줬다. 그리고 폼에서 나오는 id와 password를 두번째 파라미터로 넣어주면 된다. 그러면 입력된 값을 토대로 nextAuth에서 인증 절차를 거치는 것이다.
signIn함수도 마찬가지로 여러 옵션을 추가해서 동작할 수 있다. 나는 redirect옵션은 꺼줬다. 이 옵션은 페이지를 옮겨주는 것이다. 그래서 인증에 성공하면 원하는 페이지를 추가 옵션으로 지정해 이동이 가능하지만 인증에 실패했을때에도 로그인 페이지로 리다이렉트 시키기 때문에 꺼뒀다. 왜냐하면 구현한 로그인 페이지에서는 로그인 실패시에 에러 메시지와 토스트를 렌더링하기 때문이다.
그리고 signIn의 결과에는 ok와 error가 있다. 이 결과값에 따라서 동작할 로직을 작성했다. 인증에 성공하면 router에의해 mydashboard페이지로 이동하고 실패했을때에는 error메세지를 보여준다.
로그인을 구현했으니 이젠 로그아웃기능이다. 현재 nextAuth로 로그인하면 쿠키에 세션 데이터가 저장된다.

이 정보를 토대로 인증을 해주기 때문에 로그아웃은 해당 정보를 전부 지워주면 로그아웃이 적용된다.
const handleLogout = () => {
signOut()
}
아주 간단하게 로그아웃이 가능하다.

로그아웃했을때 session-token이 사라진 모습이다.
nextAuth에서는 미들웨어 기능을 지원한다. 미들웨어란 클라이언트와 서버사이에서 중간 매개체 역할을 하는 것이다. 하나의 필터를 설정해주는 것과 같다고 생각하면 된다. 그래서 우리는 미들웨어로 리다이렉션 기능을 추가할 것이다.
nextAuth를 적용하기 전에는 컴포넌트에서 localstorage에 인증 토큰이 없으면 router를 통해 login페이지로 이동시켰다.
const { pending, requestFunction: fetchData } = useAsync(async () => {
try {
const accessToken = localStorage.getItem('accessToken')
...
} else {
router.push('/login')
}
하지만 불필요한 요청이 가고 페이지전환이 다시 이뤄지기 때문에 딜레이되는 시간이 발생한다.
그래서 nextAuth의 미들웨어를 통해 인증되지 않으면 아예 페이지를 다른 곳으로 리다이렉트시키도록 하겠다. 우선 우리가 리다이렉트 시켜야할 페이지는 인증이 필요한 mydashboard,dashboard,dashboard/edit,mypage이다.
가장먼저 미들웨어 파일을 프로젝트 파일에middleware.ts 이름으로 생성해준다.
export { default } from 'next-auth/middleware'
export const config = {
matcher: ['/dashboard/:id*', '/mypage', '/mydashboard'],
}
그리고 이렇게 코드를 작성해주면 된다. 그럼 지정해준 경로의 페이지에서 인증여부를 확인해 페이지를 인증페이지인 login으로 리다이렉트 시켜준다. /dashboard/:id*의 의미는 뒤에 어떤 값이 오던지 해당 경로에서는 리다이렉트 시키는 것이다.
전에도 로그인하지 않으면 접근하지 못하도록 막아뒀지만 이제는 미들웨어에서 걸러져 페이지가 로딩된다.
이제 인증에 성공한 값들을 클라이언트에서 사용해보겠다. 가장먼저 app.tsx에 세션값을 사용하기 위한 provider를 설정해준다.
<SessionProvider session={session}>
<Provider store={store}>
<TotalProvider>
</SessionProvider>
그러면 provider내부에서 세션의 값을 사용할 수 있다.
provider 콜백을 통해 백엔드에서 받은 토큰을 세션에 넣는 과정을 구현해줬다. 조금더 자세히 살펴보겠다.
callbacks: {
async jwt({ token, user }: { token: JWT; user: User }) {
const copyToken = { ...token }
if (user) {
copyToken.accessToken = user?.accessToken
copyToken.id = user?.user?.id
}
return copyToken
},
},
우선 해당 콜백에서 백엔드에서 받은 토큰을 지정해주고 있다. 하지만 리턴해야하는 token값에는 accessToken이라는 항목이 없고 인증결과로 받은 user정보에도 토큰값이 없다. 그래서 타입을 추가해줘서 해당 값을 잘 받아 사용하도록 해줘야한다.
우선 types폴더에 next-auth.d.ts를 만들어주면 nextAuth에 타입을 적용시켜준다.
import NextAuth, { DefaultUser } from 'next-auth'
import { JWT } from 'next-auth/jwt'
declare module 'next-auth' {
interface User {
user: {
id: string
} & DefaultUser['user']
accessToken: string
}
}
declare module 'next-auth/jwt' {
interface JWT {
accessToken?: string
id: number
}
}
nextAuth에서 지정한 타입에 원하는 값을 추가해주는 것이다. 이제 인증후에 받는 user정보에서 id값과 토큰값을 추가해서 받을수 있다. 그리고 토큰에도 두가지 값을 추가해준다.
이제 세션에 보내는 토큰에 두가지 값을 설정해서 보내준다.
추가적으로 copytoken을 만든 이유는 원본을 보호하려는 목적이다. 원래는 token을 받아서 token자체를 수정하고 token을 리턴했지만 원본 데이터를 수정하고 리턴하는 것은 옳지 않아 copyToken이라는 값을 만들어 리턴해줬다.
이제 토큰으로 보내준 값을 세션 콜백으로 받아보겠다. 세션도 accessToken이라는 값이 없기 때문에 타입을 추가해줘야한다.
interface Session {
user: {} & DefaultSession['user']
accessToken?: string
}
그러면 세션에도 accessToken을 지정할 수 있다.
async session({ session, token }: { session: Session; token: JWT }) {
const copySession = { ...session }
copySession.user.id = token.id
copySession.accessToken = token.accessToken
return copySession
},
그리고 받은 token에서 accessToken을 받아 세션에 넣어준다. 그러면 useSession이나 getSession을 통해 세션에 있는 값을 사용할 수가 있다.

우리가 지정해준 userId와 accessToken을 확인할 수 있다. 이렇게 가져온 토큰값으로 api 헤더에 토큰을 넣어주겠다.
전에 axios interceptor를 사용해서 해더에 자동으로 토큰 설정을 해줬다. 그래서 이 기능을 활용해서 헤더 설정을 해주겠다.
INSTANCE_URL.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
const session = await getSession()
...
if (session) {
const accessToken = session.accessToken
config.headers.Authorization = `Bearer ${accessToken}`
}
return config
},
우선 getSession 사용해 위에서 확인한 세션의 값을 가져온다. 그리고 그중 accessToken값을 가져와 헤더값으로 설정하는 것이다. 이제 간단하게 axios 헤더에 토큰을 넣어줄수 있다.
전에 전역 변수를 세팅하면서 옵션으로 유저id 데이터를 만들어줬다.
useEffect(() => {
const loadUser = async () => {
const result = await getUserInfo()
setUserId(result.id)
}
if (localStorage.getItem('accessToken')) {
loadUser()
}
}, [])
그래서 토큰이 존재할때에만 userId를 설정해주도록 만들어줬다. 왜냐하면 유저 정보를 받아오는 api도 로그인 이후에 사용이 가능하기 때문이다. 그래서 이 부분도 localstorage를 지워주고 세션의 값을 사용하기로 결정했다.
const { status } = useSession()
useEffect(() => {
const loadUser = async () => {
const result = await getUserInfo()
setUserId(result.id)
}
if (status === 'authenticated') {
loadUser()
}
}, [status])
이 기능은 useSession 훅을 사용해 세션의 상태를 기준으로 작성했다. useSession도 세션의 값을 가져와 사용할 수 있지만 getSession과 다르게 상태값도 포함하고 있다.
"loading" | "authenticated" | "unauthenticated"
이렇게 3가지 상태인데 처음에는 loading상태이다. 이후에 세션값이 세팅되면 authenticated가된다. 에러시에는 unauthenticated가 된다. 그래서 세션이 세팅됬다는 것은 로그인, 인증에 성공했다는 의미이고 인증에 성공했을때에 유저 정보를 불러오는 api를 실행시켜주면 된다.
이제 인증과 관련된 로직은 모두 작성이 되었다. 하지만 문제가 있다.
async authorize(credentials) {
if (!credentials) return null
const { id, password } = credentials
const response = await LoginAccess(id, password)
return response.data
},
인증에 실패하면 에러를 발생시킨다.

근데 우리는 전에 로그인 페이지에서 백엔드에서 받아오는 에러 메시지로 인풋 하단에 에러 메세지를 렌더링했는데 이렇게 동작하면 credentialsError로 렌더링된다. 그래서 이 부분을 리팩토링하기로 했다.
우선 우리가 이 에러를 커스텀 할수는 없다. 결과가 저렇게 나오기 때문이다. 그래서 인증에 실패했을때 error가 발생하기 때문에 try...catch문을 활용하기로 했다.
async authorize(credentials) {
if (!credentials) return null
try {
const { id, password } = credentials
const response = await LoginAccess(id, password)
return response.data
} catch (error) {
const message = error.response?.data?.message
throw new Error(message)
}
},
그러면 로그인 실패했을때 발생하는 에러를 캐치해서 에러 메시지를 바탕으로 에러를 발생시킨다. 이렇게 하면 에러에 있는 메세지를 전달받아 렌더링이 가능하다.
이 부분에서 애먹었다. try...catch문에서 error의 타입이 unknown으로 타입 에러가 계속 발생했다. 그말인 즉슨 error의 타입을 명시해줘야한다는 점이다. 물론 error를 any타입으로 받아와서 처리해도 되지만 이건 타입스크립트를 사용하는 목적에 어긋나기 때문에 수정하기로 했다.
class CustomError extends Error {
response?: {
data: {
message: string
}
}
}
우선 class 타입을 만들어줬다. interface와 class의 차이는 렌더링했을때 남아 있는지에 대한 여부이다. class는 렌더링되어도 남아서 타입을 명시해준다. 그리고 로그인 실패시에 발생하는 에러중 메세지가 담긴 부분만 타입을 extends 시켜줬다. 그리고 해당 customError타입으로 에러를 받도록 했다.
} catch (error) {
const customError = error as CustomError
const message = customError.response?.data.message
throw new Error(message)
그러면 에러의 타입이 명시되어서 문제가 발생하지 않는다.
아쉬운점은 as로 타입을 지정해줬다는 것이다. any보다는 낫지만 이것또한 완벽한 방법은 아니다. 원래는 instanceof를 사용해서 타입을 판별해서 에러 타입을 명시해주려 했지만 잘 동작하지 않아서 일단 as로 타입 지정을 해줬다.

이젠 에러 메세지가 잘 출력된다.
길고 길었던 nextAuth가 잘 설정되었다. 처음 사용해보는 기능이기도 했고 혼자 공부해서 적용해보려니 어려웠다. 하지만 원래 목적이였던 로컬스토리지 사용을 안하고 인증 절차를 구현했다는 점에서 뿌듯하다. 약간 아쉬운 것은 다른 페이지에서 다시 접근했을때 인증 토큰이 다르면 만료된 토큰쪽은 로그아웃이 되어야하는데 아직은 안된다. 이 부분을 좀 설정해주면 자동 로그아웃도 되고 좋지 않을까 싶다.
새로운 것을 배우고 적용하는 과정은 재밌다. 앞으로 배우는 내용들도 잘 익혀두면 좋을것 같다.