Dev Thinking
21완료

에러 핸들링 – error.tsx, notFound, 404 UX

2025-09-28
5분 읽기

공식문서 기반의 Next.js 입문기

들어가며

4-2편에서는 인보이스 생성/수정/삭제를 서버 액션으로 처리했습니다. 폼 데이터를 서버에서 검증하고 저장한 뒤 캐시를 무효화하는 흐름을 완성했지만, 실제 앱에서는 실패 시점의 처리가 더 중요합니다. 존재하지 않는 ID를 입력하거나 서버 오류가 발생하는 상황에서 사용자를 어떻게 안내할까요.

Next.js에서는 error.tsxnotFound()로 에러를 경계별로 처리합니다. 단순한 에러 표시가 아니라, 사용자에게 다음 행동을 안내하는 UX를 만드는 방법입니다.

에러 핸들링 – 경계별 처리 원칙

Next.js App Router에서는 에러를 URL 기반 경계로 처리합니다. CSR에서는 컴포넌트에 갇힌 에러 상태지만, 여기서는 라우팅 문제로 접근하여 공유 가능한 에러 경험을 만듭니다.

에러가 발생하면 사용자는 혼란스러워집니다. 적절한 경계를 두면 "다시 시도"나 "목록으로 돌아가기" 같은 다음 행동을 안내할 수 있습니다.

핵심은 실패 유형별 경계 선택입니다:

  • notFound(): 리소스가 존재하지 않는 경우
  • error.tsx: 예상치 못한 서버/클라이언트 오류
  • redirect(): 의도적인 페이지 이동

에러 경계의 실행 방식

에러 경계는 파일 시스템 라우팅과 연결됩니다. 서버 컴포넌트나 서버 액션에서 notFound()를 호출하면 가장 가까운 not-found.tsx가 실행되고, 예상치 못한 에러는 error.tsx에서 포착됩니다.

에러 경계는 폴더별로 계층적으로 적용됩니다. 루트 레벨에 두면 전체 앱을 커버하고, 특정 라우트에 두면 그 하위만 보호합니다.

기능 구현 및 비교

인보이스 상세 화면을 기준으로 CSR과 Next.js의 에러 처리 방식을 비교해보겠습니다.

리액트 + React Query 구성 – 클라이언트 데이터 페칭

CSR에서는 React Query로 데이터 페칭을 처리합니다. 자동 캐싱과 에러 처리를 지원하지만, 여전히 클라이언트에 모든 로직이 집중됩니다:

// src/hooks/useInvoice.js
import { useQuery } from "@tanstack/react-query";
 
async function fetchInvoice(id) {
  const res = await fetch(`/api/invoices/${id}`);
  if (!res.ok) {
    if (res.status === 404) throw new Error("존재하지 않는 인보이스입니다");
    throw new Error("서버 오류");
  }
  return res.json();
}
 
export function useInvoice(id) {
  return useQuery({
    queryKey: ["invoice", id],
    queryFn: () => fetchInvoice(id),
    retry: (failureCount, error) => {
      // 404 에러는 재시도하지 않음
      if (error.message.includes("존재하지 않는")) return false;
      return failureCount < 3;
    },
  });
}

React Query가 자동으로 로딩/에러 상태를 관리하고 캐싱합니다. 컴포넌트에서는 에러 상태를 직접 표시하지만, 여전히 새로고침 시 에러 상태가 사라집니다.

Next.js 구성 – 에러 경계로 처리

Next.js에서는 서버 컴포넌트에서 에러를 던지고 경계 파일이 포착합니다:

app/invoices/[id]/
├── page.tsx
├── error.tsx
└── not-found.tsx
// app/invoices/[id]/page.tsx
import { notFound } from "next/navigation";
 
export default async function InvoiceDetailPage({ params }: { params: { id: string } }) {
  const invoice = await getInvoice(params.id);
  if (!invoice) notFound();
  return <InvoiceDetail invoice={invoice} />;
}
// app/invoices/[id]/error.tsx
"use client";
 
export default function InvoiceError({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <div>
      <h2>인보이스를 불러올 수 없습니다</h2>
      <p>{error.message}</p>
      <button onClick={reset}>다시 시도</button>
    </div>
  );
}

리액트 vs Next.js 비교표

구분리액트 + React Query (CSR)Next.js (에러 경계)
실행 환경 기본값React Query 에러 처리 → UI 표시에러 발생 → 서버/경계 포착 → 적절한 UI 이동
데이터 접근 모델React Query 캐싱 + 자동 에러 처리서버에서 조회 실패 → 경계 파일 자동 실행
번들 관점에러 처리 로직이 클라이언트 번들에 포함에러 UI만 클라이언트 번들, 검증은 서버에서 실행
컴포넌트 분리 의미React Query 훅 + UI 컴포넌트서버 액션(데이터 조회) + 에러 경계(UI) 분리
설계의 제약공유성/SEO 제한적, 클라이언트 상태 의존URL 기반 에러 경험 공유 가능, 자동 경계 적용

에러 유형별 선택 가이드

에러 유형주요 원인추천 경계사용자 경험 목표
404 Not Found잘못된 ID, 삭제된 데이터notFound()목록으로 돌아가기 유도
서버 오류DB 연결, 외부 API 실패error.tsx재시도 + 대안 액션 제공
검증 오류폼 데이터 문제서버 액션인라인 메시지 + 수정 유도
네트워크 오류연결 끊김, 타임아웃error.tsx오프라인 대응 + 재시도

에러 핸들링 적용의 트레이드오프

장점

  • 사용자 경험 개선: 적절한 에러 페이지로 사용자 이탈을 방지하고, 복구 옵션을 제공하여 앱의 신뢰성을 높입니다. 사용자가 에러 발생 시 다음 행동을 명확히 안내받을 수 있어 좋습니다.
  • 디버깅 용이성: 파일 기반 에러 경계로 특정 라우트의 에러를 격리하여 문제 파악과 해결이 쉬워집니다. 개발자가 에러 원인을 빠르게 찾아 수정할 수 있습니다.
  • SEO 고려: 404 페이지에 대한 적절한 상태 코드 반환으로 검색 엔진이 올바른 색인을 수행할 수 있습니다. 검색 엔진이 404를 만나면 해당 페이지를 색인에서 제외하므로 오히려 긍정적입니다.

단점

  • 에러 분류 복잡성: 404와 서버 오류를 혼동하기 쉬워 사용자에게 적절한 가이드를 제공하기 어려워집니다. 어떤 에러가 어느 경계에서 처리되어야 하는지 명확한 기준이 필요합니다.
  • 경계 설계 어려움: 너무 세밀하게 나누면 유지보수가 복잡해지고, 너무 크게 나누면 사용자 맞춤 에러 처리가 어려워집니다. 적절한 경계 범위를 결정하는 것이 까다롭습니다.

균형 맞추기 팁

notFound()는 404 전용으로, error.tsx는 서버 오류 전용으로 사용하세요. 각 에러 페이지에 reset 버튼이나 돌아가기 링크를 제공하세요.

예상 질문

Q. error.tsx와 Error Boundary의 차이는? error.tsx는 Next.js App Router의 파일 기반 에러 경계입니다. React Error Boundary와 달리 서버/클라이언트 컴포넌트를 모두 포착하고 라우팅과 연동됩니다.

Q. notFound()를 호출하면 SEO에 영향이 있나요? 404 상태 코드를 반환하므로 존재하지 않는 리소스에 대한 적절한 처리입니다. 검색 엔진이 404를 만나면 색인에서 제외하므로 오히려 긍정적입니다.

Q. error.tsx에서 서버 액션 결과를 어떻게 표시하나요? error.tsx는 예상치 못한 에러를 자동 포착합니다. 서버 액션의 성공/실패 상태를 직접 제어하려면 useActionState를 사용하세요.

Q. 그냥 try/catch로 에러를 처리하면 안 되나요? 가능하지만 사용자 경험에 한계가 있습니다. try/catch는 컴포넌트 내부에서만 작동하므로 URL 기반 공유나 일관된 경험이 어렵습니다.

요약

4-2편의 서버 액션 흐름을 기반으로, 실패 시점에 사용자 복구를 안내하는 에러 경계 패턴을 다루었습니다. Next.js에서는 notFound()/error.tsx로 경계별 에러 처리가 가능합니다.

CSR에서는 React Query로 에러를 자동 관리하지만, Next.js에서는 URL 기반 에러 경계를 통해 공유 가능한 에러 경험을 만듭니다. 404는 notFound()로, 서버 오류는 error.tsx로 처리하는 전략입니다.

  • 사용자 경험 개선: 에러 시 명확한 복구 옵션 제공
  • 개발 효율: 에러 처리를 경계 파일로 분리
  • SEO/공유성: URL 기반 에러 경험 공유 가능

참조