- BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영 - 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결) - 항목 수정 기능 추가 (useTemplateManagement) - 실시간 동기화 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
14 KiB
14 KiB
에러 및 특수 페이지 구성 가이드
📋 개요
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)
// ✅ 특징:
// - 서버 컴포넌트 (async/await 가능)
// - 'use client' 불필요
// - 레이아웃 없음 (전체 화면)
// - metadata 지원 가능
export default function NotFoundPage() {
return (
<div>404 - 페이지를 찾을 수 없습니다</div>
);
}
트리거:
- 존재하지 않는 URL 접근
notFound()함수 호출
Protected 404 (app/[locale]/(protected)/not-found.tsx)
// ✅ 특징:
// - DashboardLayout 자동 적용 (사이드바, 헤더)
// - 인증된 사용자만 볼 수 있음
// - 보호된 경로 내 404만 처리
export default function ProtectedNotFoundPage() {
return (
<div>보호된 경로에서 페이지를 찾을 수 없습니다</div>
);
}
2. error.tsx (에러 바운더리)
전역 에러 (app/[locale]/error.tsx)
'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)
'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)
// ✅ 특징:
// - 서버/클라이언트 모두 가능
// - 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 권장) |
에러 예시:
// ❌ 잘못된 코드 - 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 파일은 메뉴 기반 동적 라우팅을 구현합니다.
동작 로직
'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: 메뉴에 있는 경로 (구현 안됨)
# 사용자 메뉴에 /base/product/lists가 있는 경우
http://localhost:3000/ko/base/product/lists
→ ✅ EmptyPage 표시 (사이드바, 헤더 유지)
케이스 2: 메뉴에 없는 엉뚱한 경로
# 사용자 메뉴에 /fake-page가 없는 경우
http://localhost:3000/ko/fake-page
→ ❌ not-found.tsx 표시 (사이드바, 헤더 유지)
케이스 3: 실제 구현된 페이지
# dashboard/page.tsx가 실제로 존재
http://localhost:3000/ko/dashboard
→ ✅ Dashboard 컴포넌트 표시
메뉴 데이터 구조
// 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"
}
]
}
]
}
장점
- 동적 메뉴 관리: 백엔드에서 메뉴 구조 변경 시 프론트엔드 코드 수정 불필요
- 권한 기반 라우팅: 사용자별 메뉴가 다르면 접근 가능한 경로도 다름
- 명확한 UX:
- 메뉴에 있는 페이지 (미구현) → "준비 중" 메시지
- 메뉴에 없는 페이지 → "404 Not Found"
디버깅
개발 모드에서는 콘솔에 디버그 로그가 출력됩니다:
console.log('🔍 요청된 경로:', requestedPath);
console.log('📋 메뉴 데이터:', menus);
console.log(' - 비교 중:', item.path, 'vs', path);
console.log('📌 경로 존재 여부:', pathExists);
💡 실전 사용 예시
1. 404 테스트
// 존재하지 않는 경로 접근
/non-existent-page
→ app/[locale]/not-found.tsx 표시
// 보호된 경로에서 404
/dashboard/unknown-page
→ app/[locale]/(protected)/not-found.tsx 표시 (레이아웃 포함)
2. 에러 발생 시뮬레이션
// page.tsx
export default function TestPage() {
// 의도적으로 에러 발생
throw new Error('테스트 에러');
return <div>페이지</div>;
}
// → error.tsx가 에러 포착
3. 프로그래매틱 404
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. 에러 복구
'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)
// 에러 상세 정보 표시
{process.env.NODE_ENV === 'development' && (
<div>
<p>에러 메시지: {error.message}</p>
<p>스택 트레이스: {error.stack}</p>
</div>
)}
특징:
- 에러 오버레이 표시
- 상세한 에러 정보
- Hot Reload 지원
프로덕션 (production)
// 사용자 친화적 메시지만 표시
<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 라우트 (메뉴 기반 라우팅)
- localStorage 메뉴 데이터 검증 로직 구현
- 메뉴에 있는 경로 → EmptyPage 분기
- 메뉴에 없는 경로 → not-found.tsx 분기
- 재귀적 메뉴 트리 탐색 구현
- 디버그 로그 프로덕션 제거
- 성능 최적화 (메뉴 데이터 캐싱)
🔗 관련 문서
📚 참고 자료
작성일: 2025-11-11 작성자: Claude Code 마지막 수정: 2025-11-12 (Catch-all 라우트 메뉴 기반 로직 추가)