# Next.js 15 App Router - Error Handling 가이드 ## 개요 Next.js 15 App Router는 4가지 특수 파일을 통해 에러 처리와 로딩 상태를 관리합니다: - `error.tsx` - 에러 바운더리 (전역, locale별, protected 그룹별) - `not-found.tsx` - 404 페이지 (전역, locale별, protected 그룹별) - `global-error.tsx` - 루트 레벨 에러 (전역만) - `loading.tsx` - 로딩 상태 (전역, locale별, protected 그룹별) --- ## 1. error.tsx (에러 바운더리) ### 역할 렌더링 중 발생한 예상치 못한 런타임 에러를 포착하여 폴백 UI를 표시합니다. ### 파일 위치 및 우선순위 ``` src/app/ ├── global-error.tsx # 🔴 최상위 (루트 layout 에러만 처리) ├── error.tsx # 🟡 전역 에러 ├── [locale]/ │ ├── error.tsx # 🟢 locale별 에러 (우선순위 높음) │ ├── (protected)/ │ │ └── error.tsx # 🔵 protected 그룹 에러 (최우선) │ └── dashboard/ │ └── error.tsx # 🟣 특정 라우트 에러 (가장 구체적) ``` **우선순위:** 가장 가까운 부모 에러 바운더리가 에러를 포착합니다. `dashboard/error.tsx` > `(protected)/error.tsx` > `[locale]/error.tsx` > `error.tsx` ### 필수 요구사항 ```typescript // ✅ 반드시 'use client' 지시어 필요 'use client' import { useEffect } from 'react' export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { useEffect(() => { // 에러 로깅 서비스에 전송 console.error(error) }, [error]) return (

문제가 발생했습니다!

) } ``` ### Props 및 타입 정의 ```typescript interface ErrorProps { // Error 객체 (서버 컴포넌트에서 전달) error: Error & { digest?: string // 자동 생성된 에러 해시 (서버 로그 매칭용) } // 에러 바운더리 재렌더링 시도 함수 reset: () => void } ``` ### 주요 특징 1. **'use client' 필수**: 에러 바운더리는 클라이언트 컴포넌트여야 합니다. 2. **에러 전파**: 자식 컴포넌트의 에러를 포착하며, 처리되지 않으면 상위 에러 바운더리로 전파됩니다. 3. **프로덕션 에러 보안**: 프로덕션에서는 민감한 정보가 제거된 일반 메시지만 전달됩니다. 4. **digest 프로퍼티**: 서버 로그와 매칭할 수 있는 고유 식별자를 제공합니다. 5. **reset() 함수**: 에러 바운더리의 콘텐츠를 재렌더링 시도합니다. ### 제한사항 - ❌ 이벤트 핸들러 내부의 에러는 포착하지 않습니다. - ❌ 루트 `layout.tsx`나 `template.tsx`의 에러는 포착하지 않습니다 (→ `global-error.tsx` 사용). ### 실전 예시 (TypeScript + i18n) ```typescript 'use client' import { useEffect } from 'react' import { useTranslations } from 'next-intl' export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { const t = useTranslations('error') useEffect(() => { // 에러 모니터링 서비스에 전송 (Sentry, LogRocket 등) console.error('Error digest:', error.digest, error) }, [error]) return (

{t('title')}

{t('description')}

{process.env.NODE_ENV === 'development' && (
{error.message}
)}
) } ``` --- ## 2. not-found.tsx (404 페이지) ### 역할 `notFound()` 함수가 호출되거나 일치하지 않는 URL에 대해 사용자 정의 404 UI를 렌더링합니다. ### 파일 위치 및 우선순위 ``` src/app/ ├── not-found.tsx # 🟡 전역 404 ├── [locale]/ │ ├── not-found.tsx # 🟢 locale별 404 (우선순위 높음) │ ├── (protected)/ │ │ └── not-found.tsx # 🔵 protected 그룹 404 (최우선) │ └── dashboard/ │ └── not-found.tsx # 🟣 특정 라우트 404 (가장 구체적) ``` **우선순위:** 가장 가까운 부모 세그먼트의 `not-found.tsx`가 사용됩니다. ### 필수 요구사항 ```typescript // ✅ 'use client' 지시어 불필요 (서버 컴포넌트 가능) // ✅ Props 없음 import Link from 'next/link' export default function NotFound() { return (

페이지를 찾을 수 없습니다

요청하신 리소스를 찾을 수 없습니다.

홈으로 돌아가기
) } ``` ### Props 및 타입 정의 ```typescript // not-found.tsx는 props를 받지 않습니다 export default function NotFound() { // ... } ``` ### notFound() 함수 사용법 ```typescript // app/[locale]/user/[id]/page.tsx import { notFound } from 'next/navigation' interface User { id: string name: string } async function getUser(id: string): Promise { const res = await fetch(`https://api.example.com/users/${id}`) if (!res.ok) return null return res.json() } export default async function UserPage({ params }: { params: { id: string } }) { const user = await getUser(params.id) if (!user) { notFound() // ← 가장 가까운 not-found.tsx 렌더링 } return
사용자: {user.name}
} ``` ### HTTP 상태 코드 - **Streamed 응답**: `200` (스트리밍 중에는 헤더를 변경할 수 없음) - **Non-streamed 응답**: `404` ### 주요 특징 1. **서버 컴포넌트 기본**: async/await로 데이터 페칭 가능 2. **Metadata 지원**: SEO를 위한 metadata 객체 내보내기 가능 (전역 버전만) 3. **자동 Robot 헤더**: ``가 자동 삽입됨 4. **Props 없음**: 어떤 props도 받지 않습니다 ### 실전 예시 (TypeScript + i18n + Metadata) ```typescript // app/[locale]/not-found.tsx import Link from 'next/link' import { useTranslations } from 'next-intl' import { getTranslations } from 'next-intl/server' export async function generateMetadata({ params }: { params: { locale: string } }) { const t = await getTranslations({ locale: params.locale, namespace: 'not-found' }) return { title: t('meta_title'), description: t('meta_description'), } } export default function NotFound() { const t = useTranslations('not-found') return (

404

{t('title')}

{t('description')}

{t('back_home')}
) } ``` --- ## 3. global-error.tsx (루트 레벨 에러) ### 역할 루트 `layout.tsx`나 `template.tsx`에서 발생한 에러를 처리합니다. ### 파일 위치 ``` src/app/ └── global-error.tsx # ⚠️ 반드시 루트 app 디렉토리에만 위치 ``` **주의**: `global-error.tsx`는 **루트 app 디렉토리에만** 위치하며, locale이나 그룹 라우트에는 배치하지 않습니다. ### 필수 요구사항 ```typescript // ✅ 반드시 'use client' 지시어 필요 // ✅ 반드시 자체 , 태그 정의 필요 'use client' export default function GlobalError({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return (

전역 에러가 발생했습니다!

) } ``` ### Props 및 타입 정의 ```typescript interface GlobalErrorProps { error: Error & { digest?: string } reset: () => void } ``` ### 주요 특징 1. **루트 layout 대체**: 활성화되면 루트 layout을 완전히 대체합니다. 2. **자체 HTML 구조 필요**: ``과 `` 태그를 직접 정의해야 합니다. 3. **드물게 사용됨**: 일반적으로 중첩된 `error.tsx`로 충분합니다. 4. **프로덕션 전용**: 개발 환경에서는 에러 오버레이가 표시됩니다. ### 실전 예시 (TypeScript) ```typescript 'use client' import { useEffect } from 'react' export default function GlobalError({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { useEffect(() => { // 크리티컬 에러 모니터링 (Sentry, Datadog 등) console.error('Global error:', error.digest, error) }, [error]) return (

시스템 에러

애플리케이션에 치명적인 오류가 발생했습니다.

{process.env.NODE_ENV === 'development' && (
{error.message}
)}
) } ``` --- ## 4. loading.tsx (로딩 상태) ### 역할 React Suspense를 활용하여 콘텐츠가 로드되는 동안 즉각적인 로딩 UI를 표시합니다. ### 파일 위치 및 우선순위 ``` src/app/ ├── loading.tsx # 🟡 전역 로딩 ├── [locale]/ │ ├── loading.tsx # 🟢 locale별 로딩 (우선순위 높음) │ ├── (protected)/ │ │ └── loading.tsx # 🔵 protected 그룹 로딩 (최우선) │ └── dashboard/ │ └── loading.tsx # 🟣 특정 라우트 로딩 (가장 구체적) ``` **우선순위:** 각 세그먼트의 `loading.tsx`가 해당 `page.tsx`와 자식들을 감쌉니다. ### 필수 요구사항 ```typescript // ✅ 'use client' 지시어 선택사항 (서버/클라이언트 모두 가능) // ✅ Props 없음 export default function Loading() { return
로딩 중...
} ``` ### Props 및 타입 정의 ```typescript // loading.tsx는 어떤 params도 받지 않습니다 export default function Loading() { // ... } ``` ### 동작 방식 ```typescript // Next.js가 자동으로 생성하는 구조: }> ``` ### 주요 특징 1. **즉각적 로딩 상태**: 서버에서 즉시 전송되는 폴백 UI 2. **자동 Suspense 경계**: `page.js`와 자식들을 자동으로 ``로 감쌉니다 3. **네비게이션 중단 가능**: 사용자가 로딩 중에도 다른 곳으로 이동 가능 4. **공유 레이아웃 유지**: 레이아웃은 상호작용 가능 상태 유지 5. **서버/클라이언트 모두 가능**: 기본은 서버 컴포넌트, `'use client'`로 클라이언트 가능 ### 제약사항 - 일부 브라우저는 1024바이트를 초과할 때까지 스트리밍 응답을 버퍼링합니다. - Static export에서는 작동하지 않습니다 (Node.js 서버 또는 Docker 필요). ### 실전 예시 (Skeleton UI) ```typescript // app/[locale]/(protected)/dashboard/loading.tsx export default function DashboardLoading() { return (
{/* Header Skeleton */}
{/* Content Skeletons */}
{[...Array(6)].map((_, i) => (
))}
{/* Footer Skeleton */}
) } ``` ### 고급 패턴: 클라이언트 로딩 (Spinner) ```typescript 'use client' import { useEffect, useState } from 'react' export default function ClientLoading() { const [dots, setDots] = useState('.') useEffect(() => { const interval = setInterval(() => { setDots(prev => prev.length >= 3 ? '.' : prev + '.') }, 500) return () => clearInterval(interval) }, []) return (

로딩 중{dots}

) } ``` --- ## 파일 위치 및 우선순위 종합 ### 프로젝트 구조 예시 ``` src/app/ ├── global-error.tsx # 루트 layout/template 에러만 ├── error.tsx # 전역 에러 폴백 ├── not-found.tsx # 전역 404 ├── loading.tsx # 전역 로딩 │ ├── [locale]/ # locale 세그먼트 │ ├── error.tsx # locale별 에러 (우선순위 ↑) │ ├── not-found.tsx # locale별 404 (우선순위 ↑) │ ├── loading.tsx # locale별 로딩 (우선순위 ↑) │ │ │ ├── (protected)/ # 보호된 라우트 그룹 │ │ ├── error.tsx # protected 에러 (우선순위 ↑↑) │ │ ├── not-found.tsx # protected 404 (우선순위 ↑↑) │ │ ├── loading.tsx # protected 로딩 (우선순위 ↑↑) │ │ │ │ │ └── dashboard/ │ │ ├── error.tsx # dashboard 에러 (최우선 ✅) │ │ ├── not-found.tsx # dashboard 404 (최우선 ✅) │ │ ├── loading.tsx # dashboard 로딩 (최우선 ✅) │ │ └── page.tsx │ │ │ ├── login/ │ │ ├── loading.tsx # login 로딩 │ │ └── page.tsx │ │ │ └── signup/ │ ├── loading.tsx # signup 로딩 │ └── page.tsx ``` ### 우선순위 규칙 **에러 처리 우선순위 (error.tsx, not-found.tsx):** ``` 가장 구체적 (특정 라우트) ↓ dashboard/error.tsx ↓ (protected)/error.tsx ↓ [locale]/error.tsx ↓ error.tsx (전역) ↓ global-error.tsx (루트 layout 전용) ``` **로딩 상태 우선순위 (loading.tsx):** ``` 가장 구체적 (특정 라우트) ↓ dashboard/loading.tsx ↓ (protected)/loading.tsx ↓ [locale]/loading.tsx ↓ loading.tsx (전역) ``` --- ## 'use client' 지시어 필요 여부 요약 | 파일 | 'use client' 필수 여부 | 이유 | |------|------------------------|------| | `error.tsx` | ✅ **필수** | React Error Boundary는 클라이언트 전용 | | `global-error.tsx` | ✅ **필수** | Error Boundary + 상태 관리 필요 | | `not-found.tsx` | ❌ **선택** | 서버 컴포넌트 가능 (metadata 지원) | | `loading.tsx` | ❌ **선택** | 서버 컴포넌트 가능 (정적 UI 권장) | --- ## Next.js 15 App Router 특수 파일 규칙 종합 ### 파일 컨벤션 우선순위 ``` 1. layout.tsx # 레이아웃 (필수, 공유) 2. template.tsx # 템플릿 (재마운트) 3. error.tsx # 에러 바운더리 4. loading.tsx # 로딩 UI 5. not-found.tsx # 404 UI 6. page.tsx # 페이지 콘텐츠 ``` ### 라우트 세그먼트 파일 구조 ```typescript // 단일 라우트 세그먼트의 완전한 구조 app/dashboard/ ├── layout.tsx # 공유 레이아웃 ├── template.tsx # 재마운트 템플릿 (선택) ├── error.tsx # 에러 처리 ├── loading.tsx # 로딩 상태 ├── not-found.tsx # 404 페이지 └── page.tsx # 실제 페이지 콘텐츠 ``` ### 중첩 라우트 에러 전파 ``` 사용자 → dashboard/settings → 에러 발생 ↓ settings/error.tsx 있음? → 예: 여기서 처리 ↓ 아니오 dashboard/error.tsx 있음? → 예: 여기서 처리 ↓ 아니오 [locale]/error.tsx 있음? → 예: 여기서 처리 ↓ 아니오 error.tsx (전역) → 여기서 처리 ↓ global-error.tsx (루트 layout 에러만) ``` --- ## 다국어(i18n) 지원 시 주의사항 ### next-intl 라이브러리 사용 시 **Server Component (not-found.tsx, loading.tsx):** ```typescript import { getTranslations } from 'next-intl/server' export default async function NotFound() { const t = await getTranslations('not-found') return
{t('title')}
} ``` **Client Component (error.tsx, global-error.tsx):** ```typescript 'use client' import { useTranslations } from 'next-intl' export default function Error() { const t = useTranslations('error') return
{t('title')}
} ``` ### i18n 메시지 구조 예시 ```json // messages/ko.json { "error": { "title": "문제가 발생했습니다", "description": "잠시 후 다시 시도해주세요", "retry": "다시 시도" }, "not-found": { "title": "페이지를 찾을 수 없습니다", "description": "요청하신 페이지가 존재하지 않습니다", "back_home": "홈으로 돌아가기", "meta_title": "404 - 페이지를 찾을 수 없음", "meta_description": "요청하신 페이지를 찾을 수 없습니다" } } ``` --- ## 실전 구현 체크리스트 ### 전역 에러 처리 (필수) - [ ] `/app/global-error.tsx` 생성 (루트 layout 에러 처리) - [ ] `/app/error.tsx` 생성 (전역 폴백) - [ ] `/app/not-found.tsx` 생성 (전역 404) ### Locale별 에러 처리 (권장) - [ ] `/app/[locale]/error.tsx` 생성 (다국어 에러) - [ ] `/app/[locale]/not-found.tsx` 생성 (다국어 404) - [ ] `/app/[locale]/loading.tsx` 생성 (다국어 로딩) ### Protected 그룹 에러 처리 (권장) - [ ] `/app/[locale]/(protected)/error.tsx` 생성 - [ ] `/app/[locale]/(protected)/not-found.tsx` 생성 - [ ] `/app/[locale]/(protected)/loading.tsx` 생성 ### 특정 라우트 에러 처리 (선택) - [ ] `/app/[locale]/(protected)/dashboard/error.tsx` - [ ] `/app/[locale]/(protected)/dashboard/loading.tsx` - [ ] 필요시 다른 라우트에도 동일하게 적용 ### 다국어 메시지 설정 - [ ] `messages/ko.json`에 에러/404 메시지 추가 - [ ] `messages/en.json`에 에러/404 메시지 추가 - [ ] `messages/ja.json`에 에러/404 메시지 추가 ### 테스트 시나리오 - [ ] 존재하지 않는 URL 접근 시 404 페이지 표시 확인 - [ ] 에러 발생 시 가장 가까운 에러 바운더리 동작 확인 - [ ] 로딩 상태 UI 표시 확인 - [ ] 다국어 전환 시 에러/404 메시지 정상 표시 확인 - [ ] reset() 함수 동작 확인 (에러 복구) --- ## 참고 자료 - [Next.js 15 공식 문서 - Error Handling](https://nextjs.org/docs/app/building-your-application/routing/error-handling) - [Next.js API Reference - error.js](https://nextjs.org/docs/app/api-reference/file-conventions/error) - [Next.js API Reference - not-found.js](https://nextjs.org/docs/app/api-reference/file-conventions/not-found) - [Next.js API Reference - loading.js](https://nextjs.org/docs/app/api-reference/file-conventions/loading) - [React Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) - [React Suspense](https://react.dev/reference/react/Suspense) --- ## 마무리 이 가이드를 바탕으로 Next.js 15 App Router 프로젝트에 체계적인 에러 처리와 로딩 상태 관리를 구현할 수 있습니다. 파일 위치와 우선순위를 정확히 이해하고, 각 파일의 역할과 요구사항을 준수하여 사용자 경험을 개선하세요.