Next.js에서의 에러 핸들링 방법과 로딩 전략에 대해서 간단히 알아보도록 합시다.
에러 핸들링
Next.js의 에러는 예상 가능한 에러와 예상치 못한 에러, 두 가지로 구분됩니다.
1. 예상 가능한 에러 처리
네트워크 오류나 유효성 검사 실패와 같이 예상 가능한 에러는
useActionState
훅으로 처리합니다.// app/actions.ts 'use server' export async function createUser(prevState: any, formData: FormData) { const res = await fetch('https://...') if (!res.ok) { return { message: '유효한 이메일을 입력해주세요' } } redirect('/dashboard') }
클라이언트 컴포넌트 사용 예시는 다음과 같습니다.
'use client' export function Signup() { const [state, formAction, pending] = useActionState(createUser, { message: '' }) return ( <form action={formAction}> <input type="text" name="email" required /> {state?.message && <p>{state.message}</p>} <button disabled={pending}>가입하기</button> </form> ) }
핵심 포인트는
try/catch
를 사용하지 말고 에러를 반환값으로 처리하는 것입니다.2. 예상치 못한 에러 처리
Next.js의 App Router는
error.js
파일을 통해 예상치 못한 에러를 처리합니다.아래 예시와 같이 ErrorBoundary를 직접 끌어쓰지 말고, next에서 제공해주는 방식을 사용해야 합니다.
'use client'; import { ErrorBoundary } from "next/dist/client/components/error-boundary"; import ErrorFallback from './ErrorFallback'; export default function Page({ children }: PropsWithChildren) { return ( <ErrorBoundary fallback={ErrorFallback}> {children} </ErrorBoundary> ); }
이는 공식적으로 지원되는 공개 API가 아니기 때문에, Next.js 버전이 업데이트되면 내부 경로나 구현이 변경될 수 있어 안정적이지 않습니다.
대신 Next.js에서 권장하는
error.js
파일을 사용합니다.// app/dashboard/error.tsx 'use client' export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( <div className="error-container"> <h2>문제가 발생했습니다</h2> <p>에러 코드: {error.digest}</p> <button onClick={reset}>다시 시도하기</button> </div> ) }
에러 처리 위치는 다음과 같습니다.
- 일반적 에러:
error.js
파일로 라우트 세그먼트 내 에러를 처리합니다.
- 루트 레이아웃 에러:
global-error.js
파일로 루트 레이아웃 에러를 처리합니다(드물게 사용).
- 리소스 없음:
notFound()
함수로 404 페이지를 표시합니다.
// 리소스가 없을 때 404 처리 예시 import { notFound } from 'next/navigation' export default async function Page({ params }) { const product = await fetchProduct(params.id) if (!product) { notFound()// 자동으로 not-found.js를 렌더링합니다 } return <ProductDetails product={product} /> }
로딩 전략
1. loading.js 활용하기
Next.js는
loading.js
파일을 통해 로딩 상태를 선언적으로 처리합니다.// app/dashboard/loading.tsx export default function Loading() { return ( <div className="dashboard-skeleton"> <div className="header-skeleton"></div> <div className="cards-container"> {[1, 2, 3].map(i => ( <div key={i} className="card-skeleton"> <div className="title-skeleton"></div> <div className="content-skeleton"></div> </div> ))} </div> </div> ) }
한 폴더에
page.tsx
와 loading.tsx
를 함께 배치하면 자동으로 적용됩니다.2. Suspense로 세밀하게 제어하기
특정 컴포넌트의 로딩 상태만 관리하려면 Suspense를 직접 사용합니다.
import { Suspense } from 'react' export default function Dashboard() { return ( <div className="dashboard"> {/* 정적 콘텐츠는 즉시 표시됩니다 */} <DashboardHeader /> <div className="content"> {/* 컴포넌트들이 독립적으로 로드됩니다 */} <Suspense fallback={<StatsSkeleton />}> <Stats /> {/* 데이터 로딩에 시간이 걸리는 컴포넌트 */} </Suspense> <Suspense fallback={<ChartSkeleton />}> <RevenueChart /> </Suspense> </div> </div> ) }
3. 스트리밍은 이렇게 작동합니다
기존 SSR에서는 모든 콘텐츠가 준비될 때까지 사용자가 기다려야 했습니다. 스트리밍은 이 과정을 다음과 같이 개선합니다.
- 정적 부분(레이아웃, 헤더 등)을 즉시 표시합니다.
- 로딩 UI(스켈레톤, 스피너 등)를 렌더링합니다.
- 데이터가 준비되는 대로 콘텐츠를 점진적으로 전송합니다.
- React의 선택적 하이드레이션으로 상호작용 우선순위를 지정합니다.
사용자는 전체 페이지가 로드되기를 기다리지 않고 일부 UI와 상호작용할 수 있어 체감 성능이 향상됩니다.
핵심 요약
- 에러 처리: 예상 가능한 에러는
useActionState
로, 예상치 못한 에러는error.js
로 처리합니다.
- 로딩 처리: 페이지 수준에서는
loading.js
를, 컴포넌트 수준에서는Suspense
를 활용합니다.
- 리소스 없음:
notFound()
함수를 활용합니다.