# 에러 및 특수 페이지 구성 가이드 ## 📋 개요 Next.js 15 App Router에서 404, 에러, 로딩 페이지 등 특수 페이지 구성 방법 및 우선순위 규칙 --- ## 🎯 생성된 페이지 목록 ### 1. 404 Not Found 페이지 | 파일 경로 | 적용 범위 | 레이아웃 포함 | |-----------|----------|-------------| | `app/[locale]/not-found.tsx` | 전역 (모든 경로) | ❌ 없음 | | `app/[locale]/(protected)/not-found.tsx` | 보호된 경로만 | ✅ DashboardLayout | ### 2. Error Boundary 페이지 | 파일 경로 | 적용 범위 | 레이아웃 포함 | |-----------|----------|-------------| | `app/[locale]/error.tsx` | 전역 에러 | ❌ 없음 | | `app/[locale]/(protected)/error.tsx` | 보호된 경로 에러 | ✅ DashboardLayout | ### 3. Loading 페이지 | 파일 경로 | 적용 범위 | 레이아웃 포함 | |-----------|----------|-------------| | `app/[locale]/(protected)/loading.tsx` | 보호된 경로 로딩 | ✅ DashboardLayout | --- ## 📁 파일 구조 ``` src/app/ ├── [locale]/ │ ├── not-found.tsx # ✅ 전역 404 (레이아웃 없음) │ ├── error.tsx # ✅ 전역 에러 (레이아웃 없음) │ │ │ └── (protected)/ │ ├── layout.tsx # 🎨 공통 레이아웃 (인증 + DashboardLayout) │ ├── not-found.tsx # ✅ Protected 404 (레이아웃 포함) │ ├── error.tsx # ✅ Protected 에러 (레이아웃 포함) │ ├── loading.tsx # ✅ Protected 로딩 (레이아웃 포함) │ │ │ ├── dashboard/ │ │ └── page.tsx # 실제 대시보드 페이지 │ │ │ └── [...slug]/ │ └── page.tsx # 🔄 Catch-all (메뉴 기반 라우팅) │ # - 메뉴에 있는 경로 → EmptyPage │ # - 메뉴에 없는 경로 → not-found.tsx ``` --- ## 🔍 페이지별 상세 설명 ### 1. not-found.tsx (404 페이지) #### 전역 404 (`app/[locale]/not-found.tsx`) ```typescript // ✅ 특징: // - 서버 컴포넌트 (async/await 가능) // - 'use client' 불필요 // - 레이아웃 없음 (전체 화면) // - metadata 지원 가능 export default function NotFoundPage() { return (
404 - 페이지를 찾을 수 없습니다
); } ``` **트리거:** - 존재하지 않는 URL 접근 - `notFound()` 함수 호출 #### Protected 404 (`app/[locale]/(protected)/not-found.tsx`) ```typescript // ✅ 특징: // - DashboardLayout 자동 적용 (사이드바, 헤더) // - 인증된 사용자만 볼 수 있음 // - 보호된 경로 내 404만 처리 export default function ProtectedNotFoundPage() { return (
보호된 경로에서 페이지를 찾을 수 없습니다
); } ``` --- ### 2. error.tsx (에러 바운더리) #### 전역 에러 (`app/[locale]/error.tsx`) ```typescript 'use client'; // ✅ 필수! export default function GlobalError({ error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { return (

오류 발생: {error.message}

); } ``` **Props:** - `error`: 발생한 에러 객체 - `message`: 에러 메시지 - `digest`: 에러 고유 ID (서버 로깅용) - `reset`: 에러 복구 함수 (컴포넌트 재렌더링) **특징:** - **'use client' 필수** - React Error Boundary는 클라이언트 전용 - 하위 경로의 모든 에러 포착 - 이벤트 핸들러 에러는 포착 불가 - 루트 layout 에러는 포착 불가 (global-error.tsx 필요) #### Protected 에러 (`app/[locale]/(protected)/error.tsx`) ```typescript 'use client'; // ✅ 필수! export default function ProtectedError({ error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { return ( // DashboardLayout 자동 적용됨
보호된 경로에서 오류 발생
); } ``` --- ### 3. loading.tsx (로딩 상태) #### Protected 로딩 (`app/[locale]/(protected)/loading.tsx`) ```typescript // ✅ 특징: // - 서버/클라이언트 모두 가능 // - React Suspense 자동 적용 // - DashboardLayout 유지 export default function ProtectedLoading() { return (
페이지를 불러오는 중...
); } ``` **동작 방식:** - `page.js`와 하위 요소를 자동으로 `` 경계로 감쌈 - 페이지 전환 시 즉각적인 로딩 UI 표시 - 네비게이션 중단 가능 --- ## 🔄 우선순위 규칙 Next.js는 **가장 가까운 부모 세그먼트**의 파일을 사용합니다. ### 404 우선순위 ``` /dashboard/settings 접근 시: 1. dashboard/settings/not-found.tsx (가장 높음) 2. dashboard/not-found.tsx 3. (protected)/not-found.tsx ✅ 현재 사용됨 4. [locale]/not-found.tsx (폴백) 5. app/not-found.tsx (최종 폴백) ``` ### 에러 우선순위 ``` /dashboard 에서 에러 발생 시: 1. dashboard/error.tsx 2. (protected)/error.tsx ✅ 현재 사용됨 3. [locale]/error.tsx (폴백) 4. app/error.tsx (최종 폴백) 5. global-error.tsx (루트 layout 에러만) ``` --- ## 🎨 레이아웃 적용 규칙 ### 레이아웃 없는 페이지 (전역) ``` app/[locale]/not-found.tsx app/[locale]/error.tsx ``` **특징:** - 전체 화면 사용 - 사이드바, 헤더 없음 - 로그인 전/후 모두 접근 가능 **용도:** - 로그인 페이지에서 404 - 전역 에러 (로그인 실패 등) ### 레이아웃 포함 페이지 (Protected) ``` app/[locale]/(protected)/not-found.tsx app/[locale]/(protected)/error.tsx app/[locale]/(protected)/loading.tsx ``` **특징:** - DashboardLayout 자동 적용 - 사이드바, 헤더 유지 - 인증된 사용자만 접근 **용도:** - 대시보드 내 404 - 보호된 페이지 에러 - 페이지 로딩 상태 --- ## 🚨 'use client' 규칙 | 파일 | 필수 여부 | 이유 | |------|-----------|------| | `error.tsx` | ✅ **필수** | React Error Boundary는 클라이언트 전용 | | `global-error.tsx` | ✅ **필수** | Error Boundary + 상태 관리 | | `not-found.tsx` | ❌ 선택 | 서버 컴포넌트 가능 (metadata 지원) | | `loading.tsx` | ❌ 선택 | 서버 컴포넌트 가능 (정적 UI 권장) | **에러 예시:** ```typescript // ❌ 잘못된 코드 - error.tsx에 'use client' 없음 export default function Error({ error, reset }) { // Error: Error boundaries must be Client Components } // ✅ 올바른 코드 'use client'; export default function Error({ error, reset }) { // 정상 작동 } ``` --- ## 🔄 Catch-all 라우트와 메뉴 기반 라우팅 ### 개요 `app/[locale]/(protected)/[...slug]/page.tsx` 파일은 **메뉴 기반 동적 라우팅**을 구현합니다. ### 동작 로직 ```typescript 'use client'; import { notFound } from 'next/navigation'; import { EmptyPage } from '@/components/common/EmptyPage'; export default function CatchAllPage({ params }: PageProps) { const [isValidPath, setIsValidPath] = useState(null); useEffect(() => { // 1. localStorage에서 사용자 메뉴 데이터 가져오기 const userData = JSON.parse(localStorage.getItem('user')); const menus = userData.menu || []; // 2. 요청된 경로가 메뉴에 있는지 확인 const requestedPath = `/${slug.join('/')}`; const isPathInMenu = checkMenuRecursively(menus, requestedPath); // 3. 메뉴 존재 여부에 따라 분기 setIsValidPath(isPathInMenu); }, [params]); // 메뉴에 없는 경로 → 404 if (!isValidPath) { notFound(); } // 메뉴에 있지만 구현되지 않은 페이지 → EmptyPage return ; } ``` ### 라우팅 결정 트리 ``` 사용자가 /base/product/lists 접근 │ ├─ 1️⃣ localStorage에서 user.menu 읽기 │ └─ 메뉴 데이터: [{path: '/base/product/lists', ...}, ...] │ ├─ 2️⃣ 경로 검증 │ ├─ ✅ 메뉴에 경로 존재 │ │ └─ EmptyPage 표시 (구현 예정 페이지) │ │ │ └─ ❌ 메뉴에 경로 없음 │ └─ notFound() 호출 → not-found.tsx │ └─ 3️⃣ 최종 결과 ├─ 메뉴에 있음: EmptyPage (DashboardLayout 포함) └─ 메뉴에 없음: not-found.tsx (DashboardLayout 포함) ``` ### 사용 예시 #### 케이스 1: 메뉴에 있는 경로 (구현 안됨) ```bash # 사용자 메뉴에 /base/product/lists가 있는 경우 http://localhost:3000/ko/base/product/lists → ✅ EmptyPage 표시 (사이드바, 헤더 유지) ``` #### 케이스 2: 메뉴에 없는 엉뚱한 경로 ```bash # 사용자 메뉴에 /fake-page가 없는 경우 http://localhost:3000/ko/fake-page → ❌ not-found.tsx 표시 (사이드바, 헤더 유지) ``` #### 케이스 3: 실제 구현된 페이지 ```bash # dashboard/page.tsx가 실제로 존재 http://localhost:3000/ko/dashboard → ✅ Dashboard 컴포넌트 표시 ``` ### 메뉴 데이터 구조 ```typescript // localStorage에 저장되는 메뉴 구조 (로그인 시 받아옴) { menu: [ { id: "1", label: "기초정보관리", path: "/base", children: [ { id: "1-1", label: "제품관리", path: "/base/product/lists" }, { id: "1-2", label: "거래처관리", path: "/base/company/lists" } ] }, { id: "2", label: "시스템관리", path: "/system", children: [ { id: "2-1", label: "사용자관리", path: "/system/user/lists" } ] } ] } ``` ### 장점 1. **동적 메뉴 관리**: 백엔드에서 메뉴 구조 변경 시 프론트엔드 코드 수정 불필요 2. **권한 기반 라우팅**: 사용자별 메뉴가 다르면 접근 가능한 경로도 다름 3. **명확한 UX**: - 메뉴에 있는 페이지 (미구현) → "준비 중" 메시지 - 메뉴에 없는 페이지 → "404 Not Found" ### 디버깅 개발 모드에서는 콘솔에 디버그 로그가 출력됩니다: ```typescript console.log('🔍 요청된 경로:', requestedPath); console.log('📋 메뉴 데이터:', menus); console.log(' - 비교 중:', item.path, 'vs', path); console.log('📌 경로 존재 여부:', pathExists); ``` --- ## 💡 실전 사용 예시 ### 1. 404 테스트 ```typescript // 존재하지 않는 경로 접근 /non-existent-page → app/[locale]/not-found.tsx 표시 // 보호된 경로에서 404 /dashboard/unknown-page → app/[locale]/(protected)/not-found.tsx 표시 (레이아웃 포함) ``` ### 2. 에러 발생 시뮬레이션 ```typescript // page.tsx export default function TestPage() { // 의도적으로 에러 발생 throw new Error('테스트 에러'); return
페이지
; } // → error.tsx가 에러 포착 ``` ### 3. 프로그래매틱 404 ```typescript import { notFound } from 'next/navigation'; export default function ProductPage({ params }: { params: { id: string } }) { const product = getProduct(params.id); if (!product) { notFound(); // ← not-found.tsx 표시 } return
{product.name}
; } ``` ### 4. 에러 복구 ```typescript 'use client'; export default function Error({ error, reset }: { error: Error; reset: () => void }) { return (

오류 발생: {error.message}

); } ``` --- ## 🐛 개발 환경 vs 프로덕션 ### 개발 환경 (development) ```typescript // 에러 상세 정보 표시 {process.env.NODE_ENV === 'development' && (

에러 메시지: {error.message}

스택 트레이스: {error.stack}

)} ``` **특징:** - 에러 오버레이 표시 - 상세한 에러 정보 - Hot Reload 지원 ### 프로덕션 (production) ```typescript // 사용자 친화적 메시지만 표시

일시적인 오류가 발생했습니다.

``` **특징:** - 간결한 에러 메시지 - 보안 정보 숨김 - 에러 로깅 (Sentry 등) --- ## 📌 체크리스트 ### 404 페이지 - [ ] 전역 404 페이지 생성 (`app/[locale]/not-found.tsx`) - [ ] Protected 404 페이지 생성 (`app/[locale]/(protected)/not-found.tsx`) - [ ] 레이아웃 적용 확인 - [ ] 다국어 지원 (선택사항) - [ ] 버튼 링크 동작 테스트 ### 에러 페이지 - [ ] 'use client' 지시어 추가 확인 - [ ] Props 타입 정의 (`error`, `reset`) - [ ] 개발/프로덕션 환경 분기 - [ ] 에러 로깅 추가 (선택사항) - [ ] 복구 버튼 동작 테스트 ### 로딩 페이지 - [ ] 로딩 UI 디자인 일관성 - [ ] 레이아웃 내 표시 확인 - [ ] Suspense 경계 테스트 ### Catch-all 라우트 (메뉴 기반 라우팅) - [x] localStorage 메뉴 데이터 검증 로직 구현 - [x] 메뉴에 있는 경로 → EmptyPage 분기 - [x] 메뉴에 없는 경로 → not-found.tsx 분기 - [x] 재귀적 메뉴 트리 탐색 구현 - [ ] 디버그 로그 프로덕션 제거 - [ ] 성능 최적화 (메뉴 데이터 캐싱) --- ## 🔗 관련 문서 - [Empty Page Configuration](./[IMPL-2025-11-11]%20empty-page-configuration.md) - [Route Protection Architecture](./[IMPL-2025-11-07]%20route-protection-architecture.md) - [Authentication Implementation Guide](./[IMPL-2025-11-07]%20authentication-implementation-guide.md) --- ## 📚 참고 자료 - [Next.js 15 Error Handling](https://nextjs.org/docs/app/building-your-application/routing/error-handling) - [Next.js 15 Not Found](https://nextjs.org/docs/app/api-reference/file-conventions/not-found) - [Next.js 15 Loading UI](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming) --- **작성일:** 2025-11-11 **작성자:** Claude Code **마지막 수정:** 2025-11-12 (Catch-all 라우트 메뉴 기반 로직 추가)