572 lines
14 KiB
Markdown
572 lines
14 KiB
Markdown
|
|
# 에러 및 특수 페이지 구성 가이드
|
|||
|
|
|
|||
|
|
## 📋 개요
|
|||
|
|
|
|||
|
|
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 (
|
|||
|
|
<div>404 - 페이지를 찾을 수 없습니다</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**트리거:**
|
|||
|
|
- 존재하지 않는 URL 접근
|
|||
|
|
- `notFound()` 함수 호출
|
|||
|
|
|
|||
|
|
#### Protected 404 (`app/[locale]/(protected)/not-found.tsx`)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ✅ 특징:
|
|||
|
|
// - DashboardLayout 자동 적용 (사이드바, 헤더)
|
|||
|
|
// - 인증된 사용자만 볼 수 있음
|
|||
|
|
// - 보호된 경로 내 404만 처리
|
|||
|
|
|
|||
|
|
export default function ProtectedNotFoundPage() {
|
|||
|
|
return (
|
|||
|
|
<div>보호된 경로에서 페이지를 찾을 수 없습니다</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. error.tsx (에러 바운더리)
|
|||
|
|
|
|||
|
|
#### 전역 에러 (`app/[locale]/error.tsx`)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
'use client'; // ✅ 필수!
|
|||
|
|
|
|||
|
|
export default function GlobalError({
|
|||
|
|
error,
|
|||
|
|
reset,
|
|||
|
|
}: {
|
|||
|
|
error: Error & { digest?: string };
|
|||
|
|
reset: () => void;
|
|||
|
|
}) {
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
<h2>오류 발생: {error.message}</h2>
|
|||
|
|
<button onClick={reset}>다시 시도</button>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**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 자동 적용됨
|
|||
|
|
<div>보호된 경로에서 오류 발생</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. loading.tsx (로딩 상태)
|
|||
|
|
|
|||
|
|
#### Protected 로딩 (`app/[locale]/(protected)/loading.tsx`)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ✅ 특징:
|
|||
|
|
// - 서버/클라이언트 모두 가능
|
|||
|
|
// - React Suspense 자동 적용
|
|||
|
|
// - DashboardLayout 유지
|
|||
|
|
|
|||
|
|
export default function ProtectedLoading() {
|
|||
|
|
return (
|
|||
|
|
<div>페이지를 불러오는 중...</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**동작 방식:**
|
|||
|
|
- `page.js`와 하위 요소를 자동으로 `<Suspense>` 경계로 감쌈
|
|||
|
|
- 페이지 전환 시 즉각적인 로딩 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<boolean | null>(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 <EmptyPage />;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 라우팅 결정 트리
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
사용자가 /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 <div>페이지</div>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// → 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 <div>{product.name}</div>;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. 에러 복구
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
'use client';
|
|||
|
|
|
|||
|
|
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
<h2>오류 발생: {error.message}</h2>
|
|||
|
|
<button onClick={() => reset()}>
|
|||
|
|
다시 시도 {/* ← 컴포넌트 재렌더링 */}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🐛 개발 환경 vs 프로덕션
|
|||
|
|
|
|||
|
|
### 개발 환경 (development)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 에러 상세 정보 표시
|
|||
|
|
{process.env.NODE_ENV === 'development' && (
|
|||
|
|
<div>
|
|||
|
|
<p>에러 메시지: {error.message}</p>
|
|||
|
|
<p>스택 트레이스: {error.stack}</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**특징:**
|
|||
|
|
- 에러 오버레이 표시
|
|||
|
|
- 상세한 에러 정보
|
|||
|
|
- Hot Reload 지원
|
|||
|
|
|
|||
|
|
### 프로덕션 (production)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 사용자 친화적 메시지만 표시
|
|||
|
|
<div>
|
|||
|
|
<p>일시적인 오류가 발생했습니다.</p>
|
|||
|
|
<button onClick={reset}>다시 시도</button>
|
|||
|
|
</div>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**특징:**
|
|||
|
|
- 간결한 에러 메시지
|
|||
|
|
- 보안 정보 숨김
|
|||
|
|
- 에러 로깅 (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 라우트 메뉴 기반 로직 추가)
|