[Next.js] NextAuth, SessionProvider를 이용한 사용자 인증 및 페이지 접근 통제




Next.js에서 Next Auth와 Session Provider를 이용하여 사용자 인증 및 페이지 접근통제를 테스트한 내용을 정리합니다.


SessionProvider란?

SessionProvider는 NextAuth에서 제공하는 컴포넌트로, Next.js 애플리케이션의 전역에서 세션(Session) 정보를 관리하고 사용할 수 있게 해줍니다.

이를 통해 로그인된 사용자의 정보를 페이지나 컴포넌트에서 쉽게 사용할 수 있으며, 페이지 접근 통제를 구현할 때 중요한 역할을 합니다.

SessionProvider는 전역 상태로 세션을 관리하며, 로그인 여부를 기반으로 페이지를 렌더링하거나 특정 컴포넌트의 접근을 제어할 수 있습니다.

이번 글에서는 App Router 구조를 사용하는 Next.js 프로젝트에서 SessionProvider를 설정하고, 간단한 접근 제어를 구현하는 방법을 알아보겠습니다.


프로젝트 생성

npx create-next-app@latest 명령어를 이용해서 Next.js 초기 프로젝트를 생성합니다.

> npx create-next-app@latest

Need to install the following packages:
create-next-app@14.2.15
Ok to proceed? (y) y
√ What is your project named? ... next-auth-provider
√ Would you like to use TypeScript? ... No / Yes
√ Would you like to use ESLint? ... No / Yes
√ Would you like to use Tailwind CSS? ... No / Yes
√ Would you like to use `src/` directory? ... No / Yes
√ Would you like to use App Router? (recommended) ... No / Yes
√ Would you like to customize the default import alias (@/*)? ... No / Yes
Creating a new Next.js app in F:\jackerlab_project\AppsInApp\nextjs_auth\next-auth-provider.

Using npm.

Initializing project with template: app-tw


Installing dependencies:
- react
- react-dom
- next

Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- postcss
- tailwindcss
- eslint
- eslint-config-next

npm WARN deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm WARN deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
npm WARN deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm WARN deprecated eslint@8.57.1: This version is no longer supported. Please see stead
https://eslint.org/version-support for other options.                              nstead
                                                                                   https://eslint.org/version-support for other options.
added 361 packages, and audited 362 packages in 2m

137 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
Success! Created next-auth-provider

Next Auth 라이브러리 설치

> npm install next-auth

added 37 packages, and audited 67 packages in 42s

7 packages are looking for funding
  run `npm fund` for details

2 low severity vulnerabilities

To address all issues, run:   
  npm audit fix

Run `npm audit` for details.  

디렉토리 구조

SessionProvider를 사용하여 페이지 접근 통제를 구현하기 위해 필요한 디렉토리 구조는 아래와 같습니다.

/app
 ├── /api
 │     └── /auth
 │           └── [...nextauth]
 │                 └── route.ts    // NextAuth 설정 파일
 ├── /auth
 │     └── signin/page.tsx          // 로그인 페이지
 ├── /admin
 │     └── page.tsx                 // 관리자 페이지
 ├── /lib
 │     └── provider.tsx               // Provider 설정 파일
 ├── /public
 │     └── page.tsx                  // 공용 페이지
 ├── /layout.tsx                    // 전역 레이아웃 파일
 └── /page.tsx                      // 메인 페이지 파일

코드 작성

1. NextAuth 설정 (API 설정)

/app/api/auth/[...nextauth]/route.ts에서 NextAuth 설정을 합니다.

이 예시에서는 데이터베이스를 사용하지 않으므로, 소셜 로그인이나 로컬 인증을 설정하지 않고 기본적인 인증 설정만 합니다.

import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

const users = [
  {
    email: "user@example.com",
    password: "password123",
  },
  {
    email: "admin@example.com",
    password: "password123",
  },
];

const handler = NextAuth({
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: "Email", type: "text", placeholder: "you@example.com" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        // 사용자 인증 로직을 여기에 구현
        const user = users.find(
          (user) =>
            user.email === credentials?.email &&
            user.password === credentials?.password
        );

        // 유효한 사용자일 경우 사용자 객체를 반환
        if (user) {
          return { email: user.email }; // 인증 성공 시 사용자 이메일 반환
        }

        // 유효하지 않은 경우 null 반환
        return null;
      },
    }),
  ],
  pages: {
    signIn: '/auth/signin',
  },
  secret: process.env.NEXTAUTH_SECRET,
});

export { handler as GET, handler as POST };

2. 로그인 페이지 구현

로그인 페이지는 /app/auth/signin/page.tsx에 구현합니다.

사용자가 인증이 되지 않은 상태에서 접근을 시도할 때 이 페이지로 리다이렉트됩니다.

'use client';

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

export default function SignIn() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const result = await signIn('credentials', {
      redirect: false,
      email,
      password,
    });

    if (result?.error) {
      setError(result.error);
    } else {
      router.push('/');
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-100">
      <div className="bg-white p-8 rounded-lg shadow-lg w-full max-w-md">
        <h1 className="text-2xl font-bold text-center mb-6">Sign In</h1>
        {error && <p className="text-red-500 text-center mb-4">{error}</p>}
        <form onSubmit={handleSubmit} className="space-y-4">
          <div>
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="Email"
              className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>
          <div>
            <input
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              placeholder="Password"
              className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>
          <button
            type="submit"
            className="w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition duration-300"
          >
            Sign In
          </button>
        </form>
      </div>
    </div>
  );
}

3. Public 페이지 구현

관리자 페이지는 /app/public/page.tsx에 구현합니다.

이 페이지는 로그인 상관없이 접근이 가능합니다.

'use client';

export default function PublicPage() {
  return (
    <div className="min-h-screen flex flex-col items-center justify-center bg-gray-100">
      <div className="bg-white p-8 rounded-lg shadow-md w-full max-w-lg text-center">
        <h1 className="text-3xl font-bold mb-4 text-gray-800">Public Page</h1>
        <p className="text-lg text-gray-600">
          This page is accessible to everyone.
        </p>
      </div>
    </div>
  );
}

4. Admin 페이지 구현

관리자 페이지는 /app/admin/page.tsx에 구현합니다.

이 페이지는 세션 상태에 따라 접근이 제어됩니다.

'use client';

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

export default function AdminPage() {
  const { data: session, status } = useSession();

  if (status === 'loading') {
    return (
      <div className="min-h-screen flex items-center justify-center bg-gray-100">
        <p className="text-xl text-gray-600">Loading...</p>
      </div>
    );
  }

  if (!session) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-gray-100">
        <p className="text-xl text-red-600">Access denied. You are not logged in.</p>
      </div>
    );
  }

  return (
    <div className="min-h-screen flex flex-col items-center justify-center bg-gray-100">
      <div className="bg-white p-8 rounded-lg shadow-md w-full max-w-lg text-center">
        <h1 className="text-3xl font-bold mb-4 text-gray-800">Admin Dashboard</h1>
        <p className="text-lg text-gray-600">
          Welcome, {session.user?.email}! You have admin access.
        </p>
      </div>
    </div>
  );
}

5. 전역 레이아웃에서 SessionProvider 적용

/app/layout.tsx에서 SessionProvider를 사용하여 전역적으로 세션 정보를 관리하도록 설정합니다.

이렇게 하면 모든 페이지에서 세션에 접근할 수 있습니다.

import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";

import Provider from "@/app/lib/provider"; // 새로운 Provider 컴포넌트 임포트

const geistSans = localFont({
  src: "./fonts/GeistVF.woff",
  variable: "--font-geist-sans",
  weight: "100 900",
});
const geistMono = localFont({
  src: "./fonts/GeistMonoVF.woff",
  variable: "--font-geist-mono",
  weight: "100 900",
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <Provider>
          {children}
        </Provider>
      </body>
    </html>
  );
}

6. 메인 페이지에서 세션 사용

/app/page.tsx에서는 useSession 훅을 사용하여 현재 사용자의 세션 정보를 확인하고, 로그인 여부에 따라 다른 내용을 렌더링할 수 있습니다.

'use client';

import { signOut, useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';

export default function HomePage() {
  const { data: session, status } = useSession();
  const router = useRouter();

  if (status === 'loading') {
    return (
      <div className="min-h-screen flex items-center justify-center bg-gray-100">
        <p className="text-xl font-semibold text-gray-600">Loading...</p>
      </div>
    );
  }

  return (
    <div className="min-h-screen flex flex-col items-center justify-center bg-gray-100">
      <h1 className="text-3xl font-bold text-gray-800 mb-4">Welcome to the Home Page</h1>

      {session ? (
        <div className="flex flex-col items-center">
          <p className="text-lg text-gray-600 mb-4">You are logged in as: <strong>{session.user?.email}</strong></p>
          <button
            onClick={() => signOut()}
            className="mb-4 px-6 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition duration-300"
          >
            Logout
          </button>
        </div>
      ) : (
        <div className="flex flex-col items-center">
          <p className="text-lg text-gray-600 mb-4">You are not logged in.</p>
          <button
            onClick={() => router.push('/auth/signin')}
            className="mb-4 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition duration-300"
          >
            Go to Login
          </button>
        </div>
      )}

      <div className="flex space-x-4 mt-6">
        <button
          onClick={() => router.push('/admin')}
          className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition duration-300"
        >
          Admin Page
        </button>
        <button
          onClick={() => router.push('/public')}
          className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition duration-300"
        >
          Public Page
        </button>
      </div>
    </div>
  );
}

7. Provider 설정

/app/lib/provider.tsx 파일에서는 NextAuth의 세션 관리를 위한 Provider 설정을 포함합니다.

// lib/provider.tsx
'use client';

import { SessionProvider } from "next-auth/react";
import { ReactNode } from "react";

interface ProviderProps {
  children: ReactNode;
}

export default function Provider({ children }: ProviderProps) {
  return <SessionProvider>{children}</SessionProvider>;
}

테스트

최초 화면


로그인 화면

계정 정보는 /app/api/auth/[...nextauth]/route.ts 코드에 설정되어 있습니다.


로그인 후, 메인 페이지 화면

Public Page

사용자 인증 없이 누구나 접근이 가능한 페이지 입니다.


Admin Page

비 로그인

로그인


마치며

이렇게 설정하면 Next.js 프로젝트에서 SessionProvider를 사용하여 세션 관리와 페이지 접근 통제를 효과적으로 구현할 수 있습니다.

사용자가 로그인하면 관리자 페이지에 접근할 수 있으며, 세션 상태에 따라 다른 내용을 렌더링할 수 있습니다.

이를 통해 더 안전하고 사용자 친화적인 애플리케이션을 구축할 수 있습니다.




1 thought on “[Next.js] NextAuth, SessionProvider를 이용한 사용자 인증 및 페이지 접근 통제”

Leave a Comment