icon
10장 : 인증 및 권한 관리

보호된 라우트 생성


이전 절에서 NextAuth.js의 기본 설정과 signIn(), signOut() 함수를 사용하여 로그인 및 로그아웃 기능을 구현했습니다. 이제 애플리케이션의 핵심적인 부분인 보호된 라우트(Protected Routes), 즉 로그인된 사용자 또는 특정 권한을 가진 사용자만 접근할 수 있는 페이지를 만드는 방법을 자세히 다루겠습니다. Next.js App Router는 서버 컴포넌트와 미들웨어 기능을 통해 강력하고 유연한 보호된 라우트 구현을 가능하게 합니다.

이 절에서는 다음 내용을 중점적으로 살펴봅니다.

  • 서버 컴포넌트에서 getServerSession을 사용한 라우트 보호
  • 미들웨어(Middleware)를 활용한 전역 라우트 보호
  • 클라이언트 컴포넌트에서 라우트 보호
  • 권한 기반 접근 제어의 기초

getServerSession을 사용한 라우트 보호

Next.js App Router의 가장 큰 장점 중 하나는 서버 컴포넌트에서 데이터를 가져오는 것 외에도 인증 상태를 직접 확인하여 페이지 접근을 제어할 수 있다는 점입니다. 이는 클라이언트 측에서 발생할 수 있는 "깜빡임(Flash of Unstyled Content)" 없이 서버에서 즉시 리다이렉트가 가능하게 하여 더 안전하고 사용자 경험 측면에서도 우수합니다.

getServerSession 함수는 서버 컴포넌트(또는 Route Handler, Server Action) 내에서 현재 사용자의 세션 정보를 가져오는 데 사용됩니다.

기본 사용법

src/app/dashboard/page.tsx (보호할 서버 컴포넌트 예시)
import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation'; // Next.js에서 제공하는 서버 측 리다이렉트 함수
import { authOptions } from '../api/auth/[...nextauth]/route'; // NextAuth.js 설정 파일 임포트

export default async function DashboardPage() {
  // 1. 서버에서 현재 세션 정보 가져오기
  const session = await getServerSession(authOptions);

  // 2. 세션이 없는 경우 로그인 페이지로 리다이렉트
  if (!session) {
    // 사용자가 로그인 성공 후 다시 이 페이지로 돌아올 수 있도록 callbackUrl 설정
    redirect('/api/auth/signin?callbackUrl=/dashboard');
  }

  // 3. 로그인된 경우에만 페이지 콘텐츠 렌더링
  return (
    <div style={{ padding: '20px', maxWidth: '800px', margin: '20px auto', border: '1px solid #28a745', borderRadius: '10px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)', textAlign: 'center' }}>
      <h1 style={{ color: '#28a745', marginBottom: '20px' }}>대시보드</h1>
      <p className="text-gray-700">환영합니다, <span style={{ fontWeight: 'bold' }}>{session.user?.name}</span>님! 이곳은 로그인 사용자만 접근할 수 있는 보호된 대시보드 페이지입니다.</p>
      {session.user?.image && (
        <img
          src={session.user.image}
          alt="User Avatar"
          style={{ width: '80px', height: '80px', borderRadius: '50%', marginTop: '20px', display: 'block', margin: '0 auto' }}
        />
      )}
      <p style={{ marginTop: '15px', fontSize: '0.9em', color: '#555' }}>
        (이 페이지는 서버에서 인증 상태를 확인하여 보호됩니다.)
      </p>
    </div>
  );
}

장점

  • 강력한 보안: 서버에서 요청이 들어오자마자 인증 상태를 확인하므로, 클라이언트 측 자바스크립트가 로드되기 전에 접근을 제어할 수 있습니다.
  • SEO 친화적: 로그인되지 않은 사용자에게는 보호된 콘텐츠가 노출되지 않으므로, 검색 엔진이 로그인 페이지나 관련 없는 내용을 인덱싱하는 것을 방지할 수 있습니다.
  • 부드러운 사용자 경험: 리다이렉트가 서버에서 발생하므로, 사용자는 보호된 페이지의 콘텐츠가 잠시 노출되는 것을 경험하지 않습니다.

미들웨어를 활용한 전역 라우트 보호

Next.js의 미들웨어(Middleware) 는 요청이 완료되기 전에 코드를 실행할 수 있게 해주는 강력한 기능입니다. 이를 사용하여 모든 (또는 특정 패턴의) 라우트에 대한 인증 및 권한 검사를 중앙에서 처리할 수 있습니다. 미들웨어는 src/middleware.ts 파일에 정의됩니다.

미들웨어의 장점

  • 중앙 집중식 관리: 모든 보호된 라우트에 대한 인증 로직을 한 곳에서 관리하여 코드 중복을 줄이고 일관성을 유지할 수 있습니다.
  • 유연한 보호: 특정 라우트 패턴(matcher 설정)에만 미들웨어를 적용하여 필요한 부분만 보호할 수 있습니다.
  • 성능 최적화: 서버 컴포넌트에서 각각 getServerSession을 호출하는 것보다 미들웨어에서 한 번 처리하는 것이 특정 상황에서 더 효율적일 수 있습니다.

미들웨어 구현 단계

src/middleware.ts 파일 생성

src/middleware.ts
import { withAuth } from 'next-auth/middleware'; // NextAuth.js 미들웨어 헬퍼 함수

export default withAuth(
  // `withAuth` 함수에 인증되지 않은 사용자를 리다이렉트할 경로를 지정합니다.
  // 이 예시에서는 `/login` 페이지로 리다이렉트하고, 현재 요청 URL을 `callbackUrl` 쿼리 파라미터로 전달합니다.
  {
    pages: {
      signIn: '/auth/signin', // NextAuth.js 기본 로그인 페이지
      // 또는 커스텀 로그인 페이지: signIn: '/login',
    },
  }
);

// 미들웨어가 적용될 라우트 경로를 정의합니다.
// 여기서 정의된 경로는 인증이 필요한 경로를 의미합니다.
export const config = {
  matcher: [
    '/dashboard/:path*', // /dashboard로 시작하는 모든 경로
    '/profile/:path*',   // /profile로 시작하는 모든 경로
    '/admin/:path*',     // /admin으로 시작하는 모든 경로 (권한 검사는 추가 필요)
    // 여기에 보호할 다른 라우트 경로를 추가하세요.
  ],
};

보호할 페이지 생성 또는 수정: middleware.ts에 정의된 matcher 경로에 해당하는 페이지들은 이제 자동으로 미들웨어에 의해 보호됩니다. 이 페이지들에서는 더 이상 getServerSession을 사용하여 인증 여부를 직접 확인할 필요가 없습니다 (필요하다면 사용자 정보는 가져올 수 있음).

예시: 미들웨어로 보호되는 대시보드 페이지

src/app/dashboard/page.tsx (수정)
// 이 파일에는 더 이상 getServerSession 및 redirect 로직이 필요 없습니다.
// 미들웨어에서 이미 인증 여부를 확인했기 때문입니다.
import { getServerSession } from 'next-auth'; // 사용자 정보를 가져오기 위해 필요할 수 있음
import { authOptions } from '../api/auth/[...nextauth]/route';

export default async function DashboardPage() {
  // 이미 인증된 상태이므로, 세션 정보를 가져와서 활용하기만 하면 됩니다.
  const session = await getServerSession(authOptions);

  return (
    <div style={{ padding: '20px', maxWidth: '800px', margin: '20px auto', border: '1px solid #28a745', borderRadius: '10px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)', textAlign: 'center' }}>
      <h1 style={{ color: '#28a745', marginBottom: '20px' }}>대시보드</h1>
      <p className="text-gray-700">환영합니다, <span style={{ fontWeight: 'bold' }}>{session?.user?.name || '사용자'}</span>님! 이곳은 미들웨어에 의해 보호되는 대시보드 페이지입니다.</p>
      {session?.user?.image && (
        <img
          src={session.user.image}
          alt="User Avatar"
          style={{ width: '80px', height: '80px', borderRadius: '50%', marginTop: '20px', display: 'block', margin: '0 auto' }}
        />
      )}
      <p style={{ marginTop: '15px', fontSize: '0.9em', color: '#555' }}>
        (미들웨어에서 인증을 처리하여 서버 컴포넌트가 더 간결해졌습니다.)
      </p>
    </div>
  );
}

고려 사항

  • 미들웨어는 모든 요청에 대해 실행되므로, 과도한 로직이나 데이터베이스 호출은 애플리케이션 성능에 영향을 줄 수 있습니다.
  • 권한 기반 접근 제어(admin 역할 확인 등)는 미들웨어 내에서도 구현할 수 있지만, 복잡해질 경우 별도의 함수로 분리하거나, 페이지 자체에서 getServerSession을 통해 더 상세하게 제어하는 것을 고려할 수 있습니다.

클라이언트 컴포넌트에서 라우트 보호

이 방식은 이전 절에서 간략히 다루었듯이, 클라이언트 컴포넌트에서 useSession 훅을 사용하여 인증 상태를 확인하고, next/navigationuseRouter를 통해 리다이렉트를 수행합니다.

src/app/settings/page.tsx (클라이언트 컴포넌트 예시)
"use client";

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

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

  // 세션 로딩 중이 아니고, 세션이 없다면 로그인 페이지로 리다이렉트
  useEffect(() => {
    if (status === 'loading') return; // 로딩 중에는 아무것도 하지 않음
    if (!session) {
      router.push('/api/auth/signin?callbackUrl=/settings'); // NextAuth.js 기본 로그인 페이지로 리다이렉트
    }
  }, [session, status, router]);

  // 로딩 상태 처리
  if (status === 'loading') {
    return (
      <div style={{ padding: '20px', maxWidth: '800px', margin: '20px auto', border: '1px solid #ccc', borderRadius: '10px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)', textAlign: 'center' }}>
        <p>설정 페이지를 로딩 중입니다...</p>
      </div>
    );
  }

  // 로그인되지 않은 경우 (리다이렉트되기 전 잠시 표시될 수 있음)
  if (!session) {
    return (
      <div style={{ padding: '20px', maxWidth: '800px', margin: '20px auto', border: '1px solid #dc3545', borderRadius: '10px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)', textAlign: 'center' }}>
        <h1 style={{ color: '#dc3545', marginBottom: '20px' }}>접근 거부</h1>
        <p>이 페이지에 접근하려면 로그인해야 합니다.</p>
      </div>
    );
  }

  // 로그인된 경우 설정 페이지 콘텐츠 렌더링
  return (
    <div style={{ padding: '20px', maxWidth: '800px', margin: '20px auto', border: '1px solid #007bff', borderRadius: '10px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)', textAlign: 'center' }}>
      <h1 style={{ color: '#007bff', marginBottom: '20px' }}>사용자 설정</h1>
      <p>환영합니다, <span style={{ fontWeight: 'bold' }}>{session.user?.name}</span>님! 이곳은 사용자 설정을 변경할 수 있는 페이지입니다.</p>
      <p style={{ marginTop: '10px', fontSize: '0.9em', color: '#555' }}>
        (이 페이지는 클라이언트 컴포넌트에서 인증 상태를 확인하여 보호됩니다.)
      </p>
    </div>
  );
}

장점

  • 클라이언트 컴포넌트에서 동적인 UI를 구성하며 라우트 보호를 쉽게 적용할 수 있습니다.

단점

  • 보안 취약점 (상대적): 클라이언트 측에서 JavaScript가 로드되어야만 리다이렉트 로직이 실행되므로, 잠시 동안 보호된 콘텐츠가 노출될 위험이 있습니다.
  • SEO 불리: 검색 엔진 봇은 JavaScript를 실행하지 않을 수 있으므로, 보호된 콘텐츠를 적절히 인덱싱하지 못할 수 있습니다.

결론적으로, 가능한 한 서버 컴포넌트의 getServerSession 또는 미들웨어를 사용하여 라우트를 보호하는 것이 Next.js App Router에서 권장되는 방식입니다. 클라이언트 컴포넌트 방식은 사용자 친화적인 메시지를 표시하거나, 이미 서버에서 인증된 후 추가적인 클라이언트 측 권한 검사가 필요할 때 보조적으로 사용될 수 있습니다.


권한 기반 접근 제어의 기초

단순히 로그인 여부뿐만 아니라, 특정 '역할(role)'을 가진 사용자에게만 접근을 허용하는 것을 권한 기반 접근 제어(Role-Based Access Control, RBAC) 라고 합니다.

구현 방법

세션에 사용자 역할 추가: src/app/api/auth/[...nextauth]/route.ts 파일의 callbacks.jwtcallbacks.session 함수에서 사용자 정보를 처리할 때, 데이터베이스에서 가져온 사용자 역할 정보를 세션(token, session)에 추가합니다.

src/app/api/auth/[...nextauth]/route.ts (수정 예시)
// ... (기존 코드) ...

callbacks: {
  async jwt({ token, user, account }) {
    if (account && user) {
      // DB에서 사용자 역할 정보 가져오기 (예시)
      // 실제 환경에서는 DB 쿼리 등을 통해 사용자 ID에 맞는 역할을 가져와야 합니다.
      const userRole = user.email === 'admin@example.com' ? 'admin' : 'user';
      token.role = userRole; // JWT 토큰에 역할 추가
    }
    return token;
  },
  async session({ session, token }) {
    session.user.role = token.role; // 세션 객체에 역할 추가
    return session;
  },
},
// ... (기존 코드) ...

참고: 위 예시는 user.email에 따라 역할을 임시로 부여하는 예시입니다. 실제로는 데이터베이스에서 사용자 엔티티에 저장된 role 필드를 가져와야 합니다. 이를 위해서는 NextAuth.js의 데이터베이스 어댑터 설정이 필요합니다.

보호된 라우트에서 역할 확인

  • 서버 컴포넌트

    src/app/admin/page.tsx (수정)
    import { getServerSession } from 'next-auth';
    import { redirect } from 'next/navigation';
    import { authOptions } from '../api/auth/[...nextauth]/route';
    export default async function AdminPage() {
      const session = await getServerSession(authOptions);
      if (!session) {
        redirect('/api/auth/signin?callbackUrl=/admin');
      }
      // 'admin' 역할이 아니면 접근 거부
      if (session.user?.role !== 'admin') {
        redirect('/unauthorized'); // 권한 없음 페이지로 리다이렉트
      }
      return (
        <div style={{ padding: '20px', maxWidth: '800px', margin: '20px auto', border: '1px solid #ffc107', borderRadius: '10px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)', textAlign: 'center' }}>
          <h1 style={{ color: '#ffc107', marginBottom: '20px' }}>관리자 페이지</h1>
          <p>환영합니다, <span style={{ fontWeight: 'bold' }}>{session.user?.name}</span>님! 귀하는 <span style={{ fontWeight: 'bold' }}>{session.user?.role}</span> 권한을 가지고 있습니다.</p>
          <p style={{ marginTop: '10px', fontSize: '0.9em', color: '#555' }}>
            (이 페이지는 'admin' 역할만 접근할 수 있습니다.)
          </p>
        </div>
      );
    }
  • 미들웨어: 미들웨어에서도 withAuth 함수에 callbacks.authorized 옵션을 사용하여 역할 기반 접근 제어를 구현할 수 있습니다.

    src/middleware.ts (수정 예시)
    import { withAuth } from 'next-auth/middleware';
    import { NextResponse } from 'next/server'; // Next.js API에서 제공하는 응답 객체
    export default withAuth(
      // `authorized` 콜백에서 요청에 대한 권한을 검사합니다.
      function middleware(req) {
        const token = req.nextauth.token;
        // 예시: '/admin' 경로에 대해서는 'admin' 역할만 허용
        if (req.nextUrl.pathname.startsWith('/admin') && token?.role !== 'admin') {
          return NextResponse.rewrite(new URL('/unauthorized', req.url)); // 권한 없음 페이지로 리다이렉트 (URL 변경)
        }
        // 그 외의 경우는 정상적으로 요청 처리
        return NextResponse.next();
      },
      {
        pages: {
          signIn: '/api/auth/signin',
        },
        callbacks: {
          // 인증 여부만 확인 (세션이 있으면 true)
          // 여기서는 토큰의 존재 여부만으로 인증을 판단하고,
          // 실제 권한 검사는 `middleware` 함수 내부에서 수행합니다.
          authorized: ({ token }) => !!token,
        },
      }
    );
    export const config = {
      matcher: [
        '/dashboard/:path*',
        '/profile/:path*',
        '/admin/:path*',
      ],
    };

    미들웨어에서 권한을 검사하는 방식은 매우 강력하지만, 복잡한 권한 로직을 모두 미들웨어에서 처리하기보다는, 페이지 컴포넌트 내부에서 getServerSession을 통해 상세한 권한 검사를 수행하는 것이 더 관리하기 좋을 때도 있습니다.


실습: 보호된 라우트 및 권한 확인

이전 절의 NextAuth.js 기본 설정이 완료되어 있고, GitHub 로그인 Provider가 작동하는지 확인합니다.

src/app/dashboard/page.tsx를 위 getServerSession 예시로 생성/수정합니다.

src/app/settings/page.tsx를 위 클라이언트 컴포넌트 예시로 생성합니다.

src/app/admin/page.tsx를 위 서버 컴포넌트 + 역할 확인 예시로 생성합니다.

  • 중요: src/app/api/auth/[...nextauth]/route.ts 파일의 callbacks 부분에 token.role = userRole;와 같이 역할을 할당하는 로직을 추가해야 합니다.

src/app/unauthorized/page.tsx 파일을 생성하여 권한 없음 메시지를 표시하는 페이지를 만듭니다.

src/app/unauthorized/page.tsx
export default function UnauthorizedPage() {
  return (
    <div style={{ padding: '20px', maxWidth: '600px', margin: '20px auto', border: '1px solid #dc3545', borderRadius: '10px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)', textAlign: 'center' }}>
      <h1 style={{ color: '#dc3545', marginBottom: '20px' }}>권한 없음 (Unauthorized)</h1>
      <p>이 페이지에 접근할 권한이 없습니다.</p>
      <p style={{ marginTop: '15px' }}>
        <a href="/" style={{ color: '#007bff', textDecoration: 'underline' }}>홈으로 돌아가기</a>
      </p>
    </div>
  );
}

src/middleware.ts 파일을 생성하여 전역적인 라우트 보호를 테스트해 볼 수 있습니다. (미들웨어와 getServerSession을 동시에 사용할 경우, 미들웨어에서 인증을 처리하므로 getServerSession 호출 로직을 제거해야 함).

개발 서버를 실행하고 다음 경로로 접속하며 테스트합니다.

  • 로그인하지 않은 상태에서 /dashboard, /settings, /admin에 접속하여 리다이렉트되는지 확인합니다.
  • 로그인한 상태에서 /dashboard, /settings에 접속하여 콘텐츠가 보이는지 확인합니다.
  • 로그인한 상태에서 /admin에 접속하여, 만약 admin 역할이 아니라면 /unauthorized 페이지로 리다이렉트되는지 확인합니다. (GitHub 로그인만 사용한다면, 일반 사용자 계정으로는 admin 페이지에 접근할 수 없을 것입니다.)

보호된 라우트 생성은 애플리케이션의 보안을 강화하고 사용자 경험을 관리하는 데 필수적입니다. Next.js App Router는 서버 컴포넌트와 미들웨어를 통해 이 과정을 매우 효율적으로 만들어 줍니다.