Next.js 기반으로 관리자 페이지, 관제 시스템 등 권한이 필요한 프로젝트 진행 시 Next-Auth 라는 npm 을 자주 사용하였다.
여러 프로젝트 진행하면서 고려해야 했던 포인트
Next-Auth npm
Next-Auth Homepage
Next-Auth 설치
npm i next-auth // or yarn add next-auth
Next-Auth 사용법
pages/api /auth 폴더를 만들고 […nextauth].js 파일을 만들면 된다. ( 타입 스크립트이면 ts)
providers에 이메일을 사용하여 로그인 할 수 있게 했는데 공식 홈에 Google, GitHub 등 원하는 providers를 넣으면 계정으로 로그인 할 수 있다.
providers 예시
import NextAuth from "next-auth" import GitHubProvider from "next-auth/providers/github"; import NaverProvider from "next-auth/providers/naver" import KakaoProvider from "next-auth/providers/kakao" export const authOptions = { ... providers: [ GitHubProvider({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }), NaverProvider({ clientId: process.env.NAVER_CLIENT_ID, clientSecret: process.env.NAVER_SECRET, }), KakaoProvider({ clientId: process.env.KAKAO_CLIENT_ID, clientSecret: process.env.KAKAO_SECRET, }), ], } export default NextAuth(authOptions)
이메일 providers & 소수 계정일 경우
아래 코드는 이메일 providers로 소수의 계정이 필요 한 경우 find() 함수를 활용해 일치하면 로그인 가능하게 하였다.
// pages/api/auth/[...nextauth].js import NextAuth from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; export default NextAuth({ providers: [ CredentialsProvider({ name: "유저 이메일,페스워드 방식", credentials: { email: { label: "유저 이메일", type: "email", placeholder: "user@email.com", }, password: { label: "패스워드", type: "password" }, }, async authorize(credentials, req) { const accountArray = [ { id: "test1", password: "test1234!", }, { id: "test2", password: "test1234!", } ]; const matchedAccount = accountArray.find( (account) => account.id === credentials.email && account.password === credentials.password ); if (matchedAccount) { const user = [ { id: 1, name: "테스터1", email: "test1", }, { id: 2, name: "테스터2", email: "test2", } ]; const matchedUser = user.find((x) => x.email === matchedAccount.id); if (matchedUser) { return Promise.resolve(matchedUser); } } return Promise.resolve(null); }, }), ], secret: process.env.NEXTAUTH_SECRET, });
session 기간
secret 다음에 maxAge를 설정하면 된다. 기본 값은 30일 이다.
session: { maxAge: 24 * 60 * 60, // 24시간 },
NEXTAUTH_SECRET / NEXTAUTH_URL
⭐ 두 개 다 필수로 있어야 함.
NEXTAUTH_SECRET은 무작위 문자열을 넣어주면 된다.
OpenSSL 설치되어 있는 경우
$ openssl rand -base64 32
Node.js를 사용하는 경우
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
명령 프롬프트(CMD)를 열고 명령어를 입력하면 무작위 문자열이 나온다.
Get-Random을 사용하여 PowerShell에서 무작위 바이트를 생성
[Convert]::ToBase64String((Get-Random -Count 32 -InputObject ([byte[]]@(0..255))))
NEXTAUTH_URL은 development, production 분리해서 url를 다르게 설정해줘야 한다.
.env.development
NEXTAUTH_SECRET=... NEXTAUTH_URL=http://localhost:3000
.env.production
NEXTAUTH_SECRET=... NEXTAUTH_URL=https://.... (배포 주소)
next.js에 nextauth 연결
client에서 session 정보를 불러올 수 있는 useSession 훅을 사용하기 위해 앱의 가장 최 상단(_app.tsx)에 SessionProvider를 넣어준다.
기본형
// pages/_app.jsx import { SessionProvider } from "next-auth/react" export default function App({ Component, pageProps: { session, ...pageProps }, }) { return ( <SessionProvider session={session}> <Component {...pageProps} /> </SessionProvider> ) }
비 로그인 시 홈 접근 방지
권한 여부에 대한 스타일, 페이지 차이
auth 여부에 따라 보여줄 수 있는 페이지 와 스타일 차이를 Component.auth 를 통해 처리 했다.
그래서 각 페이지마다 login.tsx 하단에 Login.auth = false; index.tsx 하단에 Home.auth = true; 와 같이 넣어 해당 조건에 맞는 스타일과 접근 권한 여부를 부여한다.
⭐ Auth
Component.auth true인 페이지를 불러와서 조건을 통과하면 status에 따른 스타일, 행동을 줄 수 있다. useSession 훅에서 required: true를 설정하는 것은 세션이 반드시 필요한지 여부를 나타냅니다. 이 옵션을 사용하면 세션이 없는 경우에 대한 처리를 지정할 수 있습니다.
하위 코드에서는 useSession 훅을 사용하여 세션 상태를 확인하고, onUnauthenticated 이면 로그인 페이지로 이동 시키고 세션이 로딩 상태이면 컴포넌트를 렌더링한다.
function Auth({ children }: PropsWithChildren) { const router = useRouter(); const { status } = useSession({ required: true, onUnauthenticated() { router.push(`/login`); }, }); if (status === "loading") { return ( <> <Global styles={loadingReset} /> <Loading /> </> ); } return <>{children}</>; }
Login.auth = false 예시
export default function Login() { ... return ( <> ... </> ); } Login.auth = false;
⭐ 타입 스크립트 사용 시
auth 에 대한 타입을 추가해줘야 한다.
type CustomAppProps = AppProps & { Component: NextComponentType & { auth?: boolean }; // add auth type }; export default function App({ Component, pageProps: { session, ...pageProps }, ...appProps }: CustomAppProps) { ... }
⭐ 특정 페이지 레이아웃 스타일 분리
appProps 를 통해 pathname이 /login 이면 LayoutComponent 가 스타일이 없는 React.Fragment가 되게 했다.
export default function App({ Component, pageProps: { session, ...pageProps }, ...appProps }: CustomAppProps) { const withoutLayout = appProps.router.pathname === "/login"; const LayoutComponent = withoutLayout ? React.Fragment : Layout; return ( <SessionProvider session={session}> {Component.auth ? ( <Auth> <LayoutComponent> <Global styles={[reset, SCoreDreamFont]} /> <Component {...pageProps} /> </LayoutComponent> </Auth> ) : ( <LayoutComponent> <Global styles={[loginReset, SCoreDreamFont]} /> <Component {...pageProps} /> </LayoutComponent> )} </SessionProvider> ); }
전체 코드
// _app.tsx import React, { useEffect } from "react"; import { SessionProvider, useSession } from "next-auth/react"; import Layout from "@/components/Layout"; import type { AppProps } from "next/app"; import { useRouter } from "next/router"; import { PropsWithChildren } from "react"; import { Global } from "@emotion/react"; import { loadingReset, loginReset, reset } from "@/styles/Global"; import Loading from "@/components/ScreenStructure/Loading"; import type { NextComponentType } from "next"; import { Hydrate, QueryClient, QueryClientProvider, } from "@tanstack/react-query"; import { SCoreDreamFont } from "@/styles/Fonts"; //Add custom appProp type then use union to add it type CustomAppProps = AppProps & { Component: NextComponentType & { auth?: boolean }; // add auth type }; export default function App({ Component, pageProps: { session, ...pageProps }, ...appProps }: CustomAppProps) { const withoutLayout = appProps.router.pathname === "/login"; const LayoutComponent = withoutLayout ? React.Fragment : Layout; const [queryClient] = React.useState(() => new QueryClient()); return ( <QueryClientProvider client={queryClient}> <SessionProvider session={session}> {Component.auth ? ( // 권한이 필요한 경우 <Auth> <LayoutComponent> <Hydrate state={pageProps.dehydratedState}> <Global styles={[reset, SCoreDreamFont]} /> <Component {...pageProps} /> </Hydrate> </LayoutComponent> </Auth> ) : ( // 권한이 필요하지 않은 경우 <LayoutComponent> <Global styles={[loginReset, SCoreDreamFont]} /> <Component {...pageProps} /> </LayoutComponent> )} </SessionProvider> </QueryClientProvider> ); } function Auth({ children }: PropsWithChildren) { const router = useRouter(); const { status } = useSession({ required: true, onUnauthenticated() { router.push(`/login`); }, }); if (status === "loading") { return ( <> <Global styles={loadingReset} /> <Loading /> </> ); } return <>{children}</>; }
로그인 되어 있을 때 로그인 페이지 접근 방지
next.config.js
로그인 되어 있는 상태에서 url에 "/login" 입력 시 로그인 페이지에 진입을 방지하기 위해 "/login" 입력하면 “/” 으로 가는 redirects를 추가한다.
// next.config.js /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, swcMinify: true, async redirects() { return [ { source: "/login", destination: "/", permanent: true, }, { source: "/error", destination: "/", permanent: true, }, ]; }, }; module.exports = nextConfig;
user 정보 가져오기 & 로그아웃은 다음 글 이어서…