Next14와 Next-auth를 이용해 OAuth 구현하기

Jeonghun·2024년 2월 21일
5

NextJS

목록 보기
3/6
post-custom-banner


최근 프로젝트에서 next-auth를 이용해 소셜 로그인 기능을 구현한 과정에 대해 공유하려고 합니다.

시작하기에 앞서, 본 게시글에서는 소셜 로그인 구현에 필요한 각 소셜 서비스의 CLIENT_ID와 CLIENT_SECRET_KEY의 발급 과정은 생략하였습니다. 해당 과정이 필요하신 분들은 다른 게시글을 참고하세요 !

기술 스택

사용된 기술 스택은 다음과 같습니다.

  1. Next14
  2. next-auth v4.24.6

1. Next-Auth

next-auth란 Next 프로젝트에서 로그인 및 소셜로그인을 쉽게 구현할 수 있는 다양한 기능을 제공하는 라이브러리입니다.

애플이나 구글 뿐만 아니라 정말 다양한 Provider 를 제공하고 있으며, 이를 이용해 원하는 소셜 로그인을 빠르게 구현할 수 있습니다.

2. 사용 방법

패키지 설치

아래 명령어를 통해 라이브러리를 설치합니다.

yarn add next-auth
// or
npm i next-auth

API route 폴더 구조

next-auth를 이용하기 위해서는 몇 가지의 필수적인 파일이 필요합니다.

저의 경우 Next14로 구성된 프로젝트를 진행중이기에 App Router 에서의 방식을 사용하였습니다.

app router의 기본 폴더 구조에서 src/app/api/auth/[...nextauth]/route.ts 와 같은 구조로 파일을 추가합니다.

그 후 route.ts 파일을 아래와 같이 작성합니다.

// src/app/api/auth/[...nextauth]/route.ts

import NextAuth from 'next-auth';

const handler = NextAuth({
	// 내부 코드 ...
});

export { handler as GET, handler as POST };

SessionProvider로 Wrapping

next는 기본적으로 서버 컴포넌트 를 지원합니다.

next-auth는 client 쪽에서 로그인한 유저의 정보에 접근할 수 있도록 useSession 이라는 hook을 제공하는데요, 이를 사용할 수 있도록 SessionProvider 로 앱의 최상단을 감싸주어야 합니다.

이 때, app router 에서는 App.tsx가 따로 존재하지 않기 때문에 src 폴더 바로 하위에 위치한 layout.tsx 에 코드를 작성합니다.

src/layout.tsx 파일의 경우 서버 사이드에서 동작하기 때문에, client 에서 동작하는 SessionProvider를 직접적으로 사용할 수 없습니다.

따라서 아래와 같이 별도의 파일로 분리해주어야 하며, use client 를 꼭 작성해주셔야 합니다.

// src/lib/next-auth/index.ts

'use client';

import React from 'react';
import { SessionProvider } from 'next-auth/react';

interface Props {
  children: React.ReactNote;
}

const AuthProvider = ({ children }: Props) => {
 return <SessionProvider>{children}</SessionProvider>
};

export default AuthProdiver;
// src/layout.tsx

import AuthProvider from '@/lib/next-auth';
// import ...

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="kr">
      <body className={pretendard.className}>
        <AuthProvider>{children}</AuthProvider>
      </body>
    </html>
  );
}

OAuthProvider 구성

이제 route.ts 파일에서 소셜 로그인을 구현하기 위해 필요한 Provider를 구성해야 합니다.

위에서 언급했듯이, next-auth는 정말 많은 Provider를 제공하기에 필요에 따라 다양한 소셜 로그인을 구현할 수 있습니다.

공식문서에서 제공되는 Provider를 확인하고 필요한 기능을 구현해보세요 !

// src/app/api/auth/[...nextauth]/route.ts

import NextAuth from 'next-auth';
import KakaoProvider from 'next-auth/providers/kakao';
import NaverProvider from 'next-auth/providers/naver';
// 필요에 따라 Provider를 추가할 수 있습니다.

const handler = NextAuth({
  providers: [
    KakaoProvider({
      clientId: process.env.KAKAO_CLIENT_ID as string,
      clientSecret: process.env.KAKAO_CLIENT_SECRET as string,
    }),
    NaverProvider({
      clientId: process.env.NAVER_CLIENT_ID as string,
      clientSecret: process.env.NAVER_CLIENT_SECRET as string,
    }),
  ],
});

export { handler as GET, handler as POST };

CLIENT_ID와 CLIENT_SECRET_KEY는 각 소셜 서비스의 개발자 센터에서 등록한 ID와 KEY를 넣어주시면 됩니다.

이 값들은 외부로 노출 되어서는 안되는 고유 값이기 때문에, .env 파일에서 환경 변수를 세팅할 때 NEXT_PUBLIC 을 붙이지 않습니다.

로그인 기능을 사용하기 위해, 각 개발자 센터에서 REDIRECT_URI를 등록해야 합니다.

이는 도메인의 뒤에 api/auth/callback/provider 를 붙이면 됩니다.

예를 들어 개발 환경의 경우 http://localhost:3000/api/auth/kakao, 배포 환경의 경우 localhost를 배포된 도메인으로 작성해주세요.

next-auth 에서는 기본적으로 로그인 페이지의 UI를 제공합니다.

기본적으로 제공되는 페이지의 url은 localhost:3000/api/auth/signin 이며, 해당 경로로 접속 시 Provider를 세팅한 소셜 로그인 버튼이 생성되어 있는걸 확인 할 수 있습니다.

저의 경우 기본 제공되는 페이지가 아닌 커스텀한 로그인 페이지를 사용했습니다. 그럴 경우 아래와 같이 route.ts 파일에서 NextAuth options 중 pages 옵션을 추가하면 됩니다.

// src/app/api/auth/[...nextauth]/route.ts

import NextAuth from 'next-auth';
import KakaoProvider from 'next-auth/providers/kakao';
import NaverProvider from 'next-auth/providers/naver';

const handler = NextAuth({
  pages: {
    signIn: 'login',
  },
  providers: [
    // Provider 세팅 ...
  ],
});

export { handler as GET, handler as POST };

제가 진행하는 프로젝트에서는 '/login' 경로에서 커스텀 로그인 페이지를 제공하고 있기에 pages 옵션의 경로에 'login' 을 넣어주었습니다.

이 부분은 여러분의 프로젝트의 요구 사항에 맞게 수정해주시면 됩니다.

환경 변수 세팅

next-auth를 이용하기 위해 각 소셜 서비스의 CLIENT_ID 와 SECRET_KEY 에 더해서 추가적으로 넣어줘야할 환경 변수가 있습니다.

바로 NEXTAUTH_SECRETNEXTAUTH_URL 인데요, SECRET의 경우 임의의 KEY를 만들어 넣어주면 되고, URL의 경우 프로젝트의 로그인 기능이 포함되어 있는 도메인 주소를 넣어주면 됩니다.

// .env

NEXTAUTH_SECRET='mysecretkey'
NEXTAUTH_URL='https://example.com'

// 개발 환경의 경우 URL은 'http://localhost:3000' 이 됩니다.

onClick handler

위 과정까지 오셨다면 소셜 로그인 기능을 사용할 준비는 모두 마쳤습니다.

이제 custom login page의 소셜 로그인 버튼에 로그인 함수를 onClick 함수로 전달합니다.

저의 경우 총 4가지 종류의 소셜 로그인 기능을 구현하여 로그인 함수는 별도의 util 함수로 만들어 관리했습니다.

// src/utils/social.ts

import { signIn } from 'next-auth/react'; // next-auth에서 제공하는 signIn 함수를 import 합니다.
import { Provider } from '@/models';

export const handleSocialSignin = async (provider: Provider) => signIn(provider);
// signIn함수의 인자는 Provider가 됩니다.
// 예를 들어, 카카오 로그인의 경우 signIn('kakao') 와 같이 작성합니다.
// SocialSigninGroup.tsx

const SocialSignInGroup = ({
  style,
  onSocialSignin, // login page에서 Props로 handleSocialSignin 함수를 전달 받습니다.
}: SocialSignInGroupProps) => {
  
  // social button의 type에 따라 signIn 함수에 Provider를 전달합니다.
  const onClick = (type: string) => {
    if (type === 'KAKAO') {
      onSocialSignin('kakao');
      return;
    }
    if (type === 'NAVER') {
      onSocialSignin('naver');
      return;
    }
    if (type === 'GOOGLE') {
      onSocialSignin('google');
      return;
    }
    if (type === 'APPLE') {
      onSocialSignin('apple');
      return;
    }
  };

  return (
    <FlatList style={style}>
      {buttonItems.map((item, index) => (
        <React.Fragment key={index}>
          {index !== 0 && <Separator />}
          // 소셜 로그인 버튼의 onClick 함수로 전달 받은 signIn 함수를 연결합니다.
          <SocialButton type={item.type} onClick={() => onClick(item.type)} />
        </React.Fragment>
      ))}
    </FlatList>
  );
};

export default SocialSignInGroup;

// styled..

로그인을 위해서는 next-auth 에서 제공하는 signIn 함수를 사용합니다.

이 때 signIn 함수의 인자로는 로그인 할 소셜 서비스의 provider를 전달합니다.

// example

import { signIn } from 'next-auth/react';

const handleKakaoSignIn = () => {
  signIn('kakao')
};

<button onClick={handleKakaoSignIn}>카카오 로그인 버튼</button>

signIn 함수에는 provider 외에도 callbackUrl을 전달할 수 있습니다.

// callbackUrl example

import { signIn } from 'next-auth/react';

const handleKakaoSignIn = () => {
  signIn('kakao', {
  	callbackUrl: '/something'
  })
};

callbackUrl은 signIn 함수가 성공적으로 실행된 뒤, redirect될 페이지의 url을 의미합니다.

예를 들어, 로그인 후 사용자가 사용자의 프로필 페이지로 이동해야 한다면 아래와 같이 작성해줄 수 있습니다.

// callbackUrl example

import { signIn } from 'next-auth/react';

const handleKakaoSignIn = () => {
  signIn('kakao', {
  	callbackUrl: '/profile'
  })
};

로그인에 성공하면 아래 이미지와 같이 쿠키에 next-auth.session.token 이라는 이름으로 토큰이 담기게 됩니다.

signIn callbacks 세팅

저희 프로젝트에서는 next-auth에서 제공하는 session.token을 이용하지 않고, 내부 백엔드 서버에서 받아오는 token 값을 이용해 유저 정보를 판별, 그에 따른 UI를 렌더링합니다.

따라서 이에 따른 추가 설정을 필요로 하는데요, 이는 provider를 세팅했던 route.ts 파일에서 가능합니다.

next-auth 에서 우리는 callbacks 속성을 통해 로그인 후 실행될 콜백 함수를 작성할 수 있습니다.

예를 들어, signIn('kakao') 를 통해 카카오 로그인을 한다고 가정했을 때, 로그인을 한 뒤 해당 로그인 정보가 signIn callbacks 함수로 전달됩니다.

// src/app/api/auth/[...nextauth]/route.ts

// import ..

const handler = NextAuth({
  secret: process.env.NEXTAUTH_SECRET,
  pages: {
    signIn: 'login',
  },
  providers: [
   // provider 세팅 ..
  ],
  callbacks: {
    async signIn({ account, user, profile }) {
      let body = {
        provider: '' as Provider,
        pId: '',
        oauthAccessToken: '',
        oauthRefreshToken: '',
      };

      if (account) {
        const { access_token, refresh_token, provider, providerAccountId } =
          account;

        body = {
          provider: provider as Provider,
          pId: providerAccountId as string,
          oauthAccessToken: access_token as string,
          oauthRefreshToken: refresh_token as string,
        };
      }

      try {
        await requestSocialSignin(body);
      } catch (e) {
        if ((e as AppError).message === 'NOT_FOUND_PROVIDER') {
          return `/signup/terms?socialBody=${JSON.stringify(body)}`;
        }

		 // 에러 처리 ..
      }
      return true;
    },
  },
});

export { handler as GET, handler as POST };

코드를 한 번 살펴봅시다.

소셜 로그인 성공시 signIn callback 함수에 사용자의 로그인 정보가 전달됩니다.

이 때, account 에는 provider, providerAccountId, access_token, refresh_token 과 같은 값이 들어있으며,

user 에는 소셜 로그인시 사용자가 정보 제공에 동의한 값 (ex/사용자 이름, 이메일, 프로필 사진 등)이 들어있습니다.

내부 소셜 로그인 API는 provider와 access_token 등이 필요했기에 account 에서 필요한 값을 추출하여 await requestSocialSignun(body) 를 통해 API를 호출하여 내부 token을 반환 받아 사용했습니다.

이 때, signIn callback 에서는 로그인이 가능한 상황일 땐 return true 를, 로그인이 불가능한 상황 (회원이 아니거나 하는 등의 에러가 발생하는 경우) 에서는 return false 혹은 리다이렉트를 위한 URL을 전달 할 수 있습니다.

next-auth의 callbacks에서는 signIn 외에 jwt, session callback을 제공하고 있습니다. 자세한 내용은 공식문서를 참고해보세요 !

내부 API 호출을 통해 반환받은 token은 interceptors 기능을 사용해 자동으로 response에 담긴 token을 추출하여 쿠키에 담아주도록 구현했습니다.

nextjs를 사용하시는 분들 중 fetch api를 사용하시는데 axios와 같은 interceptors 기능이 없어 불편함을 느끼셨다면, return-fetch 라이브러리를 사용해보세요 !


마치며

OAuth 기능을 구현한건 이번 프로젝트가 처음이었는데, 성공적으로 구현할 수 있어서 뿌듯했던 경험이었습니다.

구현 과정에서 삽질(?) 하는 시간이 조금 있었지만, 그 또한 성장의 밑바탕이 되는 값진 시간이었다고 생각합니다.

next-auth의 전반적인 동작 방식에 대해 이해하고, 이를 통해 논리적인 사고력을 기를 수 있었습니다 :)

참고 문헌
next-auth로 로그인 기능 구현하기

profile
안녕하세요, 프론트엔드 개발자 임정훈입니다.
post-custom-banner

2개의 댓글

comment-user-thumbnail
2024년 7월 25일

Apple 로그인도 잘 되나요?

1개의 답글