사이드 프로젝트를 진행하며 네이버 소셜로그인 파트를 담당하게 되었습니다.
네이버 소셜 로그인을 구현하는 방법을 찾아보던 중, Next-Auth라는 라이브러리를 알게되었습니다.
Next-Auth는 네이버 뿐만이 아닌, 구글, 카카오 등 다양한 소셜 로그인을 지원하는 3rd-party 라이브러리입니다. 이를 통해 팀원들도 간단하게 소셜 로그인을 구현할 수 있을 것 같아 도입을 제안하고 사용중에 있습니다.
npm install next-auth
yarn add next-auth
import NextAuth from 'next-auth';
import NaverProvider from 'next-auth/providers/naver';
const handler = NextAuth({
providers: [
NaverProvider({
clientId: process.env.NAVER_CLIENT_ID!,
clientSecret: process.env.NAVER_CLIENT_SECRET!,
profile(profile) {
return {
id: profile.response?.id ? profile.response?.id : profile.id,
name: profile.response?.name ? profile.response?.name : profile.name,
email: profile.response?.email ? profile.response?.email : profile.email,
image: profile.response?.profile_image ? profile.response?.profile_image : profile.image,
};
},
}),
],
callbacks: {
async signIn() {
try {
return true;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_) {
return false;
}
},
async redirect({ url, baseUrl }) {
// 에러가 있는 경우 처리
if (url.includes('error=Callback')) {
return `${baseUrl}/login`;
}
// 취소된 경우 처리
if (url.includes('error=AccessDenied')) {
return `${baseUrl}/login`;
}
// 기본 리다이렉트
if (url.startsWith(baseUrl)) {
return url;
}
return baseUrl;
},
async jwt({ token, user }) {
// 네이버 로그인 시 받아오는 정보를 토큰에 저장
if (user) {
token.id = user.id;
token.name = user.name;
token.email = user.email;
token.image = user.image;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user = {
...session.user,
name: token.name as string,
email: token.email as string,
image: token.image as string,
};
}
return session;
},
},
pages: {
signIn: '/login',
error: '/login', // 에러 페이지를 로그인 페이지로 지정
},
});
export { handler as GET, handler as POST };
App Router에서는
Get
과Post
메서드를 명시적으로 export해야 사용할 수 있습니다.
//JWT 토큰 암호화를 위해 설정해아하는 값
NEXTAUTH_SECRET='mysecretkey'
//배포 환경에서는 실제 URL로 변경해야합니다.
NEXTAUTH_URL='http://localhost:3000'
NAVER_CLIENT_ID=네이버 클라이언트 ID..
NAVER_CLIENT_SECRET=네이버 시크릿 ID..
// /app/providers.tsx
'use client';
import { SessionProvider } from 'next-auth/react';
export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
// /app/layout.tsx
import type { Metadata } from 'next';
import './globals.css';
import TanstackQueryProvider from '@/providers/TanstackQueryProvider';
import MswInitializer from '@/providers/MswInitializer';
import { Providers } from '@/app/providers';
export const metadata: Metadata = {
title: '빵잇나우',
description: '빵잇나우',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<MswInitializer />
<TanstackQueryProvider>
<Providers>{children}</Providers>
</TanstackQueryProvider>
</body>
</html>
);
}
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import Image from 'next/image';
import Topbar from '@/components/topbar/Topbar';
import Button from '@/components/button/Button';
import naverIcon from '@/assets/icons/naver.svg';
import Alert from '@/components/common/Alert';
import { signIn, signOut } from 'next-auth/react';
export default function LoginPage() {
const handleSignIn = async (provider: string) => {
try {
signIn(provider, {
callbackUrl: '/',
redirect: true,
});
} catch (_) {
setAlertTitle('로그인 실패');
setAlertSubtitle('로그인 중 문제가 발생했습니다. 다시 시도해주세요.');
setShowAlert(true);
}
};
return (
<div className="flex flex-col max-h-[100%] bg-white">
<div className="flex flex-col items-center justify-center gap-4 mt-6">
<div className="flex items-center w-full my-3 px-5">
<div className="flex-1 h-px bg-gray-300" />
<span className="px-3 text-sm text-gray-500">또는</span>
<div className="flex-1 h-px bg-gray-300" />
</div>
<div className="flex gap-4">
<button
onClick={() => handleSignIn('naver')}
className="w-14 h-14 bg-green-500 rounded-full flex items-center justify-center">
<Image src={naverIcon} width={24} height={24} alt="네이버 로그인" />
</button>
<button
onClick={() => signOut()}
className="w-14 h-14 bg-green-500 rounded-full flex items-center justify-center">
로그아웃
</button>
</div>
</div>
</div>
);
}
useSeession
훅을 사용하여 사용자 정보를 가져올 수 있습니다.// app/components/UserProfile.js
"use client";
import { useSession } from "next-auth/react";
export default function UserProfile() {
const { data: session } = useSession();
if (!session) {
return <p>로그인되지 않았습니다.</p>;
}
return (
<div>
<p>사용자 이름: {session.user.name}</p>
<p>이메일: {session.user.email}</p>
</div>
);
}
// app/dashboard/page.js
import { getServerSession } from "next-auth";
import { authOptions } from "../api/auth/[...nextauth]/route";
export default async function Header() {
const session = await getServerSession(authOptions);
if (!session) {
return <p>로그인해주세요.</p>;
}
return (
<div>
<p>환영합니다, {session.user.name}님!</p>
</div>
);
}
NextAuth를 사용하니 회사에서 Okta를 사용한 Oauth를 직접 구현했을 때보다 훨씬 간편하게 구현할 수 있었습니다. 게다가 다양한 로그인을 지원하니 여러 로그인을 구현할 때는 속도가 훨씬 빨라져 좋은 라이브러리라고 생각됩니다. 다음에는 백엔드와 연동하는 방법을 기술하여 Spring Session에 저장하는 방식을 기술해보겠습니다.