Next.js Error Handling

질문Bot·2025년 10월 26일

Next.js

목록 보기
9/13
post-thumbnail

🤔 고민..

요즘 개발을 하면서 Next.js에서는 에러 처리를 어떤식으로 할까 라는 고민이 많습니다.

  • API 호출 중 요청 실패(4xx/5xx)
  • 네트워크/타임아웃
  • 인증 만료(401)
  • 의도치 않은 런타임 에러(Type Error, 로직 오류 등)

저도 이러한 부분들을 고민했고, Next.js App Router 기준에서 어떤식으로 처리하면 좋을지 한번 고민한 부분들을 정리한 글입니다.


💡 Next.js에 error.tsx는 언제 뜰까?

error.tsx는 Error Boundary(에러 바운더리) 입니다.

특정 컴포넌트에서 렌더링 과정 중 예상하지 못한 에러가 발생하면, 버블링되어 가장 가까운 error.tsx가 렌더됩니다.

추가 내용

또한, error.tsx는 에러 경계 (Error Boundary) 역할을 하며 Client Component여야 합니다.


🗂️ 케이스 별로 분리

1 error.tsx가 자동으로 나오는 케이스

  • 렌더링 중 던져진 에러
  • 훅/라이프사이클에서 동기적으로 던진 에러

(예: useEffect(() => { throw new Error('boom') }, [])처럼 즉시 throw면 잡힙니다)

2. 수동으로 잡아야하는 케이스

  • 이벤트 핸들러에서 난 에러(클릭 등)
  • 비동기 콜백 안에서 난 에러 (setTimeout, await 내부 throw 등)

왜 Error.tsx는 렌더링 에러만 잡는걸까요? 비동기도 잡으면 되는거 아니야?

이것을 설명하려면 먼저 Error.tsx가 누구인지 먼저 알 필요가 있습니다.

Error.tsx는 에러 바운더리 입니다.

에러 바운더리란?
렌더링 도중 발생하는 렌더링 에러를 잡아내는 것을 의미합니다.

그래서 에러 바운더리를 통해 저희가 원하는 범위만큼 컴포넌트를 감싸고, 나머지 UI에는 영향이 가지 않도록 하는것입니다.

이러한 에러 바운더리가 못하는것중 하나인 비동기 에러 캐치입니다. 왜냐면 그것은 렌더링 하는 과정에서 일어나는 오류가 아닙니다.

Error Boundaries – React


⭐️ 간단한 예시

렌더 중 에러 → error.tsx

// app/users/page.tsx (Server Component)
export default async function UsersPage() {
  const res = await fetch('https://api.example.com/users', { cache: 'no-store' });
  if (!res.ok) {
    // 렌더 경로에서 throw → 같은 세그먼트의 error.tsx가 받음
    throw new Error('Failed to load users');
  }
  const data = await res.json();
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

(Next.js는 세그먼트의 error.tsx를 자동 경계로 감쌉니다.)

이벤트/비동기 에러는?

onClick이나 await 내부에서 던진 에러는 경계가 바로 못 잡습니다.
그래서 이럴 때는 상태에 에러를 올려 다음 렌더에서 throw하도록 해야합니다.

function Escalate({ error }: { error: Error | null }) {
  if (error) throw error; // 렌더 경로에서 throw → error.tsx로
  return null;
}

function SomeClient() {
  const [fatal, setFatal] = useState<Error | null>(null);

  const onClick = async () => {
    try {
      await doAsync(); // 비동기
    } catch (e) {
      setFatal(e instanceof Error ? e : new Error('Unknown'));
    }
  };

  return (
    <>
      <Escalate error={fatal} />
      <button onClick={onClick}>실행</button>
    </>
  );
}

React Query와 error.tsx 같이 쓰기 (v5 기준)

저는 여기서 고생 좀 했습니다..

처음엔 onError에서 잡아서 경계로 “보내자”라고 생각했는데, 콜백에서 Promise.reject만 던진다고 경계가 받지 않습니다.
정답은 쿼리/뮤테이션에 throwOnError를 켜는 것입니다.

버전 정리

  • v5: throwOnError: true | (error) => boolean
  • v4: useErrorBoundary: true | (error) => boolean

React Query는 내부에서 에러를 잡고, 다음 렌더 사이클에서 재-throw해서 가장 가까운 에러 경계(error.tsx)가 받게 해줍니다. (참고 : Suspense | TanStack Query React Docs)

그래서 혹시나 onError 안에서 return Promise.reject(err)를 하셨다면 이제는 하지마세요.!!
의미 없습니다 ⇒ 콜백임

// useQuery 예시 (v5)
const { data } = useQuery({
  queryKey: ['users'],
  queryFn: () => api.get('/users'),
  // 예: 5xx만 경계로, 4xx는 로컬 UI에서 처리
  throwOnError: (err: any) => (err?.status ?? 0) >= 500,
  // 선택적으로 실무 감각 옵션
  staleTime: 1000 * 30,
  gcTime: 1000 * 60 * 5,
})
// useMutation 예시 (v5)
const mutation = useMutation({
  mutationFn: (vars) => api.post('/upload', vars),
  throwOnError: (err: any) => (err?.status ?? 0) >= 500,
})

에러 유형별 처리 가이드 (UX관점)

유형어디서 처리?사용자 피드백
404(예상 가능)notFound()not-found.tsx“없어요/삭제됨” 페이지
4xx(검증/권한)컴포넌트/폼/뷰 로직인라인 에러/토스트
5xx(서버 오류)경계로 던짐 (throwOnError) → error.tsx일관된 에러 화면 + 재시도
네트워크/타임아웃컴포넌트/전역 배너오프라인 배너 + 재시도(백오프)
401(인증 만료)인터셉터/미들웨어세션 만료 토스트 + 로그인 유도
이벤트/비동기 중 예외“승격”(상태→렌더에서 throw) 또는 React Query의 throwOnErrorerror.tsx 또는 토스트

API 에러 정규화

회사에서 지금 하는것중 하나는 API 정규화 작업을 진행하고있습니다.

// 공통 에러 타입
type AppError = {
  status: number | null;
  code: string;            // 서버 코드 또는 AXIOS_ERROR 등
  message: string;         // 사용자 메세지
  description?: string | null;
};

// 예: axios 인터셉터에서 AppError로 정규화 후 reject

이렇게 해두면 React Query의 throwOnError, 토스트, 인라인 메시지, 로깅이 모두 일관됩니다.


마무리

  • error.tsx렌더링 중의 “예상치 못한” 에러를 잡는 세그먼트 경계입니다. 비동기/이벤트 에러는 그대로는 안 잡히는 특징이 있습니다.
  • React Query v5는 throwOnError*로 쿼리/뮤테이션 에러를 경계로 전파**해줍니다. 5xx는 경계, 4xx는 로컬 UI로 가르는 패턴이 깔끔하다고 생각합니다.
  • 경계의 “다시 시도”는 useQueryErrorResetBoundary + reset()로 연결하면 더 나은 UX를 제공할수있습니다. tanstack.com
  • 404는 notFound(), 401은 인터셉터/미들웨어, 네트워크는 배너/재시도. 레이어를 나눠서 처리하면 팀 전체 UX가 안정적이라고 생각합니다.
profile
유용한 정보를 전달하는 사람이 되고자 노력합니다.

0개의 댓글