보호된 라우트 생성
이전 절에서는 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) 내에서 현재 사용자의 세션 정보를 가져오는 데 사용됩니다.
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 파일 생성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을 사용하여 인증 여부를 직접 확인할 필요가 없습니다 (필요하다면 사용자 정보는 가져올 수 있음).
// 이 파일에는 더 이상 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를 통해 리다이렉트를 수행합니다.
"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)에 추가합니다.
// ... (기존 코드) ...
callbacks: {
async jwt({ token, user, account }) {
if (account && user) {
const ROLE_BY_EMAIL: Record<string, 'admin' | 'user'> = {
'admin@example.com': 'admin',
};
const userRole = ROLE_BY_EMAIL[user.email ?? ''] ?? 'user';
token.role = userRole; // JWT 토큰에 역할 추가
}
return token;
},
async session({ session, token }) {
session.user.role = token.role; // 세션 객체에 역할 추가
return session;
},
},
// ... (기존 코드) ...참고: 예제는 재현 가능한 매핑(ROLE_BY_EMAIL)으로 역할을 결정합니다. 실제 서비스에서는 데이터베이스 또는 IDP claim의 role 필드를 같은 위치에서 읽어오면 됩니다.
-
서버 컴포넌트
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 파일을 생성하여 권한 없음 메시지를 표시하는 페이지를 만듭니다.
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페이지에 접근할 수 없을 것입니다.)
다음 다이어그램은 요청이 middleware.ts, 서버 컴포넌트, 클라이언트 UI 중 어디에서 판단되는지 실행 위치별로 비교합니다.
이후 실패 응답은 로그인 없음, 권한 부족, 소유권 불일치, 정상 통과로 나누어 고릅니다.
보호된 라우트 생성은 애플리케이션의 보안을 강화하고 사용자 경험을 관리하는 데 필수적입니다. Next.js App Router는 서버 컴포넌트와 미들웨어를 통해 이 과정을 매우 효율적으로 만들어 줍니다.
아래 다이어그램은 보호된 라우트 생성을 권한 경계, 우회 경로, 실패 응답 기준으로 프로젝트 안에 배치합니다.