Next-Auth 를 이용한 회원가입 & 로그인

Habyte·2023년 6월 29일
3

쇼핑몰 프로젝트

목록 보기
1/2

서론

최근에 nextjs 프레임워크를 이용하여 쇼핑몰 웹앱 프로젝트를 새로 시작하면서 가장 먼저 고민하게된 문제는 회원가입 및 로그인이였다. 백엔드 서버를 따로 두지 않는 서버리스 구조로 프로젝트를 계획하던 중이라 빠르고 간편하게 인증과 인가 기능을 구현할 수 있는 next-auth 라이브러리를 도입해 보았다.

next-auth ?

next-auth 의 기본 개념과 환경 세팅은 이미 자세하게 잘 설명해놓은 글이 많기 때문에 생략하도록 하고 내가 참고 했던 글 몇개만 첨부하고 넘어가도록 하겠다.

https://next-auth.js.org/getting-started/introduction
https://velog.io/@dosomething/Next-auth-를-이용한-로그인-구현#-토큰-유효성-검사

프로젝트에 적용하기

쇼핑몰 사용자 연령대를 4, 50대로 잡았고 일단은 국내 사용자로 한정하여 네이버카카오 로그인이 가장 효과적일 것으로 생각되어 해당 provider 을 추가하였다.

// src/pages/api/auth/[...nextauth].ts

import NextAuth from "next-auth";
import type { NextAuthOptions } from "next-auth";
import NaverProvider from "next-auth/providers/naver";
import KakaoProvider from "next-auth/providers/kakao";

export const authOptions: NextAuthOptions = {
  providers: [
    NaverProvider({
      clientId: process.env.NAVER_ID!,
      clientSecret: process.env.NAVER_SECRET!,
    }),
    KakaoProvider({
      clientId: process.env.KAKAO_ID!,
      clientSecret: process.env.KAKAO_SECRET!,
    }),
  ],
};

export default NextAuth(authOptions);

위에서 적은 코드가 잘 작동하나 싶어 확인해 보았다.

nextauth_로그인

로그인을 했을 때 쿠키에 정보가 잘 저장되고 로그아웃 하면 사라진다. 잘 동작한다!

Session 에서 문제 발생

그럼 세션값에 정보가 잘 들어있는지 확인하기 위해 아무 페이지에서 useSession() 을 사용해서 세션을 불러왔는데 왠걸 카카오에서는 잘찍히는 name 프로퍼티가 네이버에서는 undefined 가 떳다...

왜 이런 문제가 발생하는지 살펴 보기위해 로그인 이후 해당 정보를 포함하는 signIn() 콜백 함수안에서 user 객체 콘솔을 찍어보았다. 역시나 이름이 들어 있어야 할 nameundefined 값이 들어있었고 profile 객체 안에 이름값이 들어 있었다.

이런 일이 발생하는 이유는 next-auth 에서 워낙 다양한 플랫폼의 oauth 를 지원하다 보니 플랫폼마다 주는 데이터의 형식 차이로 발생하는 문제가 아닌가 라고 유추해 보았다.

// 네이버 로그인 user 객체
{
  id: 'qp47I3BLNuXxweV56HTk...',
  name: undefined,
  email: 'example1234@naver.com',
  image: undefined
}

// 네이버 로그인 profile 객체
{
  resultcode: '00',
  message: 'success',
  response: {
    id: 'qp47I3BLNuXxweV56HTk...',
    email: 'example1234@naver.com',
    name: '공기밥'
  }
}

이유가 어찌되었든 두 플랫폼 모두 빈 값없이 사용자 정보를 세션에 담도록 다음과 같이 코드를 변경해 주었고 이후 세션에 네이버 로그인 역시 이름값이 잘 담겨서 전달되는 것을 확인했다.

export const authOptions: NextAuthOptions = {
  providers: [
    NaverProvider({
      clientId: process.env.NAVER_ID!,
      clientSecret: process.env.NAVER_SECRET!,
    }),
    KakaoProvider({
      clientId: process.env.KAKAO_ID!,
      clientSecret: process.env.KAKAO_SECRET!,
    }),
  ],
  callbacks: {
    async signIn({ user, profile }) {
      // profile 객체에 이름이나 이메일 값이 있으면 해당 값을 user 객체에 저장
      if (profile) {
        user.name = profile.response?.name || user.name;
        user.email = profile.response?.email || user.email;
      }

      return true;
    },
  },
};

export default NextAuth(authOptions);

DB 와 로그인 정보 연동하기

이번 프로젝트에서 회원가입은 별도의 추가 정보없이 간단히 이름과 이메일 정보만 필요로 했다. 따라서 oauth 로그인 할 때 DB에 사용자 정보가 없는 사람은 바로 회원가입이 이루어지는 방식으로 구현했다.

next-auth 에서는 자동으로 데이터베이스와 연결해주는 adapter 라는 기능을 지원한다. 이 프로젝트는 MongoDB 를 사용하고 있었고 @auth/mongodb-adapter 도 지원하고 있어서 프로젝트에 적용해 보았지만 여러 에러들을 마주했다. 또한 DB에 생성되는 모델의 형태도 정해져 있어 내가 원하는 DB 구조가 아니었기 때문에 adapter 를 사용하지 않기로 결정했다.

사용자가 로그인 하면 호출되는 signIn 콜백 안에서 DB를 체크하고 새로운 유저를 등록하는 로직을 집어 넣어서 간단하게 연동시켰다. prisma ORM 을 사용해서 데이터베이스 모델을 관리했고 DB에 생성된 유저 idrole 정보를 세션에 추가해서 클라이언트에서 나중에 접근할 수 있도록 했다.

최종 코드

// src/pages/api/auth/[...nextauth].ts

import NextAuth from "next-auth";
import type { NextAuthOptions } from "next-auth";
import NaverProvider from "next-auth/providers/naver";
import KakaoProvider from "next-auth/providers/kakao";
import prisma from "common/lib/prisma";

export const authOptions: NextAuthOptions = {
  providers: [
    NaverProvider({
      clientId: process.env.NAVER_ID!,
      clientSecret: process.env.NAVER_SECRET!,
    }),
    KakaoProvider({
      clientId: process.env.KAKAO_ID!,
      clientSecret: process.env.KAKAO_SECRET!,
    }),
  ],
  callbacks: {
    async signIn({ user, profile }) {
      if (profile) {
        user.name = profile.response?.name || user.name;
        user.email = profile.response?.email || user.email;
      }

      try {
        // 데이터베이스에 유저가 있는지 확인
        let db_user = await prisma.user.findUnique({
          where: { email: user.email! },
        });

        // 없으면 데이터베이스에 유저 추가
        if (!db_user) {
          db_user = await prisma.user.create({
            data: {
              name: user.name!,
              email: user.email!,
              cart: {
                create: {},
              },
            },
          });
        }

        // 유저 정보에 데이터베이스 아이디, 역할 연결
        user.id = db_user.id;
        user.role = db_user.role;

        return true;
      } catch (error) {
        console.log("로그인 도중 에러가 발생했습니다. " + error);
        return false;
      }
    },
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
        token.role = user.role;
      }

      return token;
    },
    async session({ session, token }) {
      // 세션에 유저 정보 저장
      if (session.user) {
        session.user.id = token.id as string;
        session.user.role = token.role as string;
      }

      return session;
    },
  },
  secret: process.env.NEXTAUTH_SECRET,
};

export default NextAuth(authOptions);

middleware 을 통한 페이지 접근 권한 설정

next-auth 에서 제공하는 getToken() 함수를 이용해서 사용자 로그인 토큰에 접근할 수 있다는 점을 이용하여 페이지별 접근 권한 설정을 middleware.ts 파일을 통해 해주었다. 앞서 설정해 주었던 NEXTAUTH_SECRET 환경변수를 사용해서 토큰을 복호화하고 사용자 역할 정보를 가져와서 사용했다.

// src/middleare.ts

import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";

export async function middleware(req: NextRequest) {
  const token = await getToken({
    req,
    secret: process.env.NEXTAUTH_SECRET,
  });
  const { pathname } = req.nextUrl;

  if (pathname.startsWith("/login")) {
    if (token) {
      return NextResponse.redirect(new URL("/", req.url));
    }
  }

  if (pathname.startsWith("/my")) {
    if (!token) {
      return NextResponse.redirect(new URL("/login", req.url));
    }
  }

  if (pathname.startsWith("/admin")) {
    if (token?.role !== "ADMIN") {
      return NextResponse.redirect(new URL("/", req.url));
    }
  }
}

export const config = {
  matcher: ["/login", "/my", "/admin"],
};

배포시 마주했던 에러

Vercel 을 사용해서 배포 테스트를 하던 도중 NEXTAUTH_SECRET 환경 변수를 추가해 주지 않아서 에러가 발생했다. next-auth 를 사용하기 위해 두가지 환경 변수가 필요한데, 토큰 암호화 복호화를 위한 NEXTAUTH_SECRET, 그리고 서비스 도메인 정보를 담은 NEXTAUTH_URL 이 필요하다.

NEXTAUTH_SECRET 어디서 얻음?

NEXTAUTH_SECRET 를 얻는 방법은 공식 홈페이지에 잘 나와있는데 아래 openssl 커맨드를 사용하면 된다.

$ openssl rand -base64 32

vscode 커맨드 창이나 맥 터미널 어디에서 실행해도 상관없고 실행후 아래에 출력되는 랜덤한 문자열 값을 복사해서 사용하면 된다.

시크릿 키 얻는법

profile
인생 저장소

0개의 댓글