인프런 영문 브랜드 로고
인프런 영문 브랜드 로고
BEST
개발 · 프로그래밍

/

프론트엔드

Next + React Query로 SNS 서비스 만들기

리액트19 & 넥스트15 & 리액트쿼리5 & Next Auth5 & MSW2 & socket.io4 & zustand 스택으로 트위터(X.com)와 유사한 SNS 서비스를 만들어봅니다. 끝으로 검색엔진 최적화를 위한 SSR까지!

(4.5) 수강평 88개

수강생 3,134명

Thumbnail
새소식 목록
관리
작성

next-auth 서버 에러 받기 & session에 커스텀 데이터 넣기 & 권한에 따라 페이지 접근하기

안녕하세요. 제로초입니다.

많은 분들이 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에 에러 메시지를 적으면 되는데

const response = await signIn("credentials", {
  username: id,
  password,
  redirect: false
})

이제 로그인 실패 시 response에 에러 코드와 메시지가 들어있게 됩니다. 단, response.ok는 여전히 true로 나옵니다(redirect가 false면 무조건 true입니다)

 

session 객체에 커스텀 데이터 넣기

공식 문서에 따르면 현재 authorize 함수의 return에는 id, email, name, image만 넣을 수 있습니다. 이것만 해도 제한이 큰데 id는 넣을 수 있다고 하면서도 넣으면 useSession()의 데이터에서는 사라져버립니다.

  1. 그럼 id는 어디로 갔냐?

  2. 다른 커스텀 데이터는 어떻게 넣냐?

    하실 수 있는데 다음과 같이 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 객체에 넣어서 반환하는 겁니다.

  1. 아까 사라졌던 id는 이 메서드의 token.sub에 들어 있습니다.

  2. 여기서 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를 어떻게서든 활용해볼 수 있습니다.

다음에 또 업데이트 되는 사항이 있으면 알려드리겠습니다. 감사합니다!

댓글