Files
sam-react-prod/claudedocs/auth/[IMPL-2025-11-07] route-protection-architecture.md
byeongcheolryu 65a8510c0b fix: 품목기준관리 실시간 동기화 수정
- BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영
- 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결)
- 항목 수정 기능 추가 (useTemplateManagement)
- 실시간 동기화 문서 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 22:19:50 +09:00

11 KiB

Route Protection Architecture - 최종 구조

개요

2단계 보호 시스템:

  1. Middleware (서버): 모든 페이지 요청 시 인증 확인
  2. 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에 포함되지 않음
    • /loginsrc/app/[locale]/login/page.tsx
    • /(auth)/login → 동일한 /login URL
  • (protected): Layout 기반 보호 그룹
    • /dashboardsrc/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 리다이렉트

결과: 쿠키 공유로 즉시 차단


새 페이지 추가 방법

보호된 페이지 추가

단계:

  1. (protected) 폴더 안에 페이지 생성
  2. 끝! (자동으로 보호됨)

예시:

# 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에 포함 안됨)


공개 페이지 추가

단계:

  1. (protected) 폴더 에 페이지 생성
  2. auth-config.tspublicRoutes에 추가 (필요시)

예시:

# 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]);
}

핵심 로직:

  1. checkAuth(): /api/auth/check 호출로 실시간 인증 확인
  2. pageshow 이벤트: event.persisted로 캐시 감지
  3. 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 리다이렉트
  • 새 보호된 페이지 추가

    • (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