Files
sam-react-prod/claudedocs/archive/[IMPL-2025-11-11] error-pages-configuration.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

14 KiB
Raw Blame History

에러 및 특수 페이지 구성 가이드

📋 개요

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"
        }
      ]
    }
  ]
}

장점

  1. 동적 메뉴 관리: 백엔드에서 메뉴 구조 변경 시 프론트엔드 코드 수정 불필요
  2. 권한 기반 라우팅: 사용자별 메뉴가 다르면 접근 가능한 경로도 다름
  3. 명확한 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 라우트 메뉴 기반 로직 추가)