- BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영 - 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결) - 항목 수정 기능 추가 (useTemplateManagement) - 실시간 동기화 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
11 KiB
11 KiB
Route Protection Architecture - 최종 구조
개요
2단계 보호 시스템:
- Middleware (서버): 모든 페이지 요청 시 인증 확인
- Layout Hook (클라이언트): 보호된 페이지의 브라우저 캐시 방지
폴더 구조
src/app/[locale]/
├── (auth)/ # 게스트 전용 페이지
│ └── login/
│ └── page.tsx # 로그인 페이지 (컴포넌트 재사용)
│
├── (protected)/ # ✅ 보호된 페이지 그룹
│ ├── layout.tsx # 🔒 useAuthGuard() 여기서만!
│ └── dashboard/
│ └── page.tsx # useAuthGuard() 불필요
│
├── login/ # 직접 접근용 로그인 페이지
│ └── page.tsx
│
├── signup/ # 직접 접근용 회원가입 페이지
│ └── page.tsx
│
├── page.tsx # 홈페이지 (공개)
└── layout.tsx # 루트 레이아웃
Route Group 설명:
(auth): 괄호로 감싸져 있어 URL에 포함되지 않음/login→src/app/[locale]/login/page.tsx/(auth)/login→ 동일한/loginURL
(protected): Layout 기반 보호 그룹/dashboard→src/app/[locale]/(protected)/dashboard/page.tsx- Layout의
useAuthGuard()가 자동 적용
보호 레이어 상세
Layer 1: Middleware (서버 사이드)
파일: src/middleware.ts
역할:
- 모든 HTTP 요청 차단 (페이지, API, 리소스)
- HttpOnly 쿠키 검증
- 인증 실패 시
/login리다이렉트
적용 범위:
- URL 직접 입력
- 링크 클릭
- 새로고침 (F5)
- 프로그래매틱 네비게이션
코드:
// src/middleware.ts
function checkAuthentication(request: NextRequest) {
const tokenCookie = request.cookies.get('user_token');
if (tokenCookie?.value) {
return { isAuthenticated: true, authMode: 'bearer' };
}
return { isAuthenticated: false, authMode: null };
}
// 보호된 경로 체크
if (!isAuthenticated && !isPublicRoute && !isGuestOnlyRoute) {
return NextResponse.redirect(new URL('/login', request.url));
}
Layer 2: Protected Layout (클라이언트 사이드)
파일: src/app/[locale]/(protected)/layout.tsx
역할:
- 페이지 마운트 시 인증 재확인
- 브라우저 BFCache (뒤로가기 캐시) 감지 및 새로고침
- 다른 탭에서 로그아웃 시 동기화
적용 범위:
(protected)폴더 하위 모든 페이지- 브라우저 뒤로가기
- 페이지 캐시 복원
코드:
// src/app/[locale]/(protected)/layout.tsx
"use client";
import { useAuthGuard } from '@/hooks/useAuthGuard';
export default function ProtectedLayout({ children }) {
useAuthGuard(); // 모든 하위 페이지에 자동 적용
return <>{children}</>;
}
시나리오별 동작
✅ 시나리오 1: URL 직접 입력 (비로그인)
http://localhost:3000/dashboard 입력
↓
🛡️ Middleware 실행
→ 쿠키 없음
→ /login 리다이렉트
↓
로그인 페이지 표시
(Layout Hook은 실행되지 않음)
결과: Middleware만으로 차단 완료 ✅
✅ 시나리오 2: 정상 로그인 후 접근
로그인 성공 → /dashboard 이동
↓
🛡️ Middleware 실행
→ 쿠키 있음
→ 통과
↓
(protected)/layout.tsx 마운트
→ useAuthGuard() 실행
→ /api/auth/check 호출
→ 인증 성공
↓
dashboard/page.tsx 렌더링
결과: 이중 검증 통과 ✅
✅ 시나리오 3: 로그아웃 후 뒤로가기 (핵심!)
/dashboard 접속 (로그인 상태)
↓
Logout 버튼 클릭
→ /api/auth/logout 호출
→ HttpOnly 쿠키 삭제
→ /login 이동
↓
브라우저 뒤로가기 버튼 클릭
↓
⚠️ 브라우저 캐시에서 /dashboard 복원
→ 서버 요청 없음
→ Middleware 실행 안됨 ❌
↓
🛡️ (protected)/layout.tsx 복원
→ useAuthGuard() 실행
→ pageshow 이벤트 감지
→ event.persisted === true (캐시됨)
→ window.location.reload() 실행
↓
새로고침 → 서버 요청 발생
↓
🛡️ Middleware 실행
→ 쿠키 없음
→ /login 리다이렉트
↓
로그인 페이지 표시
결과: Layout Hook이 캐시 우회 → Middleware 재실행 ✅
✅ 시나리오 4: 다른 탭에서 로그아웃
탭 A: /dashboard 접속 (로그인 상태)
탭 B: 로그아웃
↓
탭 A: 페이지 새로고침 또는 네비게이션
↓
🛡️ Middleware 실행
→ 쿠키 없음 (탭 B에서 삭제됨)
→ /login 리다이렉트
결과: 쿠키 공유로 즉시 차단 ✅
새 페이지 추가 방법
보호된 페이지 추가
단계:
(protected)폴더 안에 페이지 생성- 끝! (자동으로 보호됨)
예시:
# Profile 페이지 생성
mkdir -p src/app/[locale]/(protected)/profile
// src/app/[locale]/(protected)/profile/page.tsx
"use client";
export default function Profile() {
// useAuthGuard() 불필요! Layout에서 자동 처리
return <div>Profile Content</div>;
}
URL: /profile (Route Group 괄호는 URL에 포함 안됨)
공개 페이지 추가
단계:
(protected)폴더 밖에 페이지 생성auth-config.ts의publicRoutes에 추가 (필요시)
예시:
# About 페이지 생성 (공개)
mkdir -p src/app/[locale]/about
// src/app/[locale]/about/page.tsx
export default function About() {
return <div>About Us (Public)</div>;
}
// src/lib/api/auth/auth-config.ts
export const AUTH_CONFIG = {
publicRoutes: [
'/about', // 추가
],
// ...
};
구현 상세
useAuthGuard Hook
파일: src/hooks/useAuthGuard.ts
export function useAuthGuard() {
const router = useRouter();
useEffect(() => {
// 1. 페이지 로드 시 인증 확인
const checkAuth = async () => {
const response = await fetch('/api/auth/check');
if (!response.ok) {
router.replace('/login');
}
};
checkAuth();
// 2. 브라우저 캐시 감지 및 새로고침
const handlePageShow = (event: PageTransitionEvent) => {
if (event.persisted) {
console.log('🔄 캐시된 페이지 감지: 새로고침');
window.location.reload();
}
};
window.addEventListener('pageshow', handlePageShow);
return () => {
window.removeEventListener('pageshow', handlePageShow);
};
}, [router]);
}
핵심 로직:
checkAuth():/api/auth/check호출로 실시간 인증 확인pageshow이벤트:event.persisted로 캐시 감지window.location.reload(): 강제 새로고침으로 Middleware 재실행
Auth Check API
파일: src/app/api/auth/check/route.ts
export async function GET(request: NextRequest) {
const token = request.cookies.get('user_token')?.value;
if (!token) {
return NextResponse.json(
{ error: 'Not authenticated', authenticated: false },
{ status: 401 }
);
}
return NextResponse.json(
{ authenticated: true },
{ status: 200 }
);
}
역할:
- HttpOnly 쿠키 읽기
- 인증 상태 반환 (200 or 401)
보안 장점
✅ 이전 (각 페이지에 Hook)
각 페이지마다 useAuthGuard() 수동 추가
→ 누락 위험 ⚠️
→ 보일러플레이트 코드 증가
✅ 현재 (Layout 기반)
(protected)/layout.tsx에서 한 번만
→ 새 페이지 자동 보호
→ 누락 불가능
→ 코드 중복 제거
설정 파일
auth-config.ts
파일: src/lib/api/auth/auth-config.ts
export const AUTH_CONFIG = {
// 🔓 공개 라우트 (인증 불필요)
publicRoutes: [],
// 🔒 보호된 라우트 (참고용, 실제로는 기본 정책으로 보호)
protectedRoutes: [
'/dashboard',
'/profile',
'/settings',
'/admin',
// ... 모든 보호된 경로
],
// 👤 게스트 전용 라우트 (로그인 후 접근 불가)
guestOnlyRoutes: [
'/login',
'/signup',
'/forgot-password',
],
// 리다이렉트 설정
redirects: {
afterLogin: '/dashboard',
afterLogout: '/login',
unauthorized: '/login',
},
};
테스트 체크리스트
필수 테스트
-
URL 직접 입력 (비로그인)
/dashboard입력 →/login리다이렉트
-
로그인 후 접근
- 로그인 →
/dashboard정상 표시
- 로그인 →
-
로그아웃 후 뒤로가기
- 로그아웃 → 뒤로가기 → 캐시 감지 → 새로고침 →
/login리다이렉트
- 로그아웃 → 뒤로가기 → 캐시 감지 → 새로고침 →
-
다른 탭에서 로그아웃
- 탭 A:
/dashboard유지 - 탭 B: 로그아웃
- 탭 A: 새로고침 →
/login리다이렉트
- 탭 A:
-
새 보호된 페이지 추가
(protected)/profile생성 → 자동 보호 확인
트러블슈팅
문제: 로그아웃 후 뒤로가기 시 페이지 보임
원인: Layout이 Client Component가 아님
해결:
// (protected)/layout.tsx 파일 상단에 추가
"use client";
문제: 404 에러 (페이지를 찾을 수 없음)
원인: 폴더 이름 오타 또는 Route Group 괄호 누락
확인:
# 올바른 경로
src/app/[locale]/(protected)/dashboard/page.tsx
# 잘못된 경로
src/app/[locale]/protected/dashboard/page.tsx # 괄호 없음
문제: 무한 리다이렉트
원인: /login 페이지에도 보호 적용됨
확인:
/login이(protected)폴더 밖에 있는지 확인guestOnlyRoutes에/login포함 확인
성능 고려사항
API 호출 최소화
useAuthGuard는 페이지 마운트 시 1회만 호출- 브라우저 캐시 복원 시에만 추가 호출 (새로고침)
사용자 경험
- 인증 확인은 비동기로 처리 (UI 블로킹 없음)
router.replace()사용으로 뒤로가기 히스토리 오염 방지
향후 페이지 추가 계획
즉시 적용 가능 (보호됨)
(protected) 폴더에 추가하면 자동 보호:
(protected)/
├── profile/ # 사용자 프로필
├── settings/ # 설정
├── admin/ # 관리자
│ ├── users/
│ ├── tenants/
│ └── reports/
├── inventory/ # 재고 관리
├── finance/ # 재무
├── hr/ # 인사
└── crm/ # CRM
요약
✅ 최종 아키텍처
보호 정책:
1. Middleware (서버): 모든 요청 차단
2. Layout (클라이언트): 캐시 우회 및 실시간 동기화
폴더 구조:
- (protected)/layout.tsx: 한 곳에서만 관리
- (protected)/**/page.tsx: 자동으로 보호됨
장점:
✅ 코드 중복 제거
✅ 누락 불가능
✅ 브라우저 캐시 문제 해결
✅ 확장성 (새 페이지 자동 보호)
✅ 유지보수성 향상
참고 문서
- HttpOnly Cookie 구현:
claudedocs/httponly-cookie-implementation.md - Auth Guard 사용법:
claudedocs/auth-guard-usage.md - Middleware 설정:
src/middleware.ts - Auth 설정:
src/lib/api/auth/auth-config.ts