보호된 라우트 생성
이전 절에서 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 // 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 // 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/navigation
의 useRouter
를 통해 리다이렉트를 수행합니다.
// 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.jwt
및callbacks.session
함수에서 사용자 정보를 처리할 때, 데이터베이스에서 가져온 사용자 역할 정보를 세션(token
,session
)에 추가합니다.src/app/api/auth/[...nextauth]/route.ts // 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 // 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 // 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 // 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는 서버 컴포넌트와 미들웨어를 통해 이 과정을 매우 효율적으로 만들어 줍니다.