본문 바로가기

Web

[Next.js] App Router에서 error.tsx를 쓰다가 try/catch로 돌아온 이유

반응형

개요

App Router에서는 'error.tsx'를 통해 공통 에러 처리를 쉽게 구성할 수 있다.
'error.tsx'는 단순 에러 화면이 아니라 route segment 단위로 동작하는

Error Boundary 역할을 수행하기 때문에 에러 처리 범위를 비교적 명확하게 나눌 수 있는 구조다.

 

처음에는 segment마다 'error.tsx'를 배치하면
에러 처리 구조를 깔끔하게 설계할 수 있을 것이라 생각했다.

(Next.js 공식 문서에서도 권장하는 방향)

 

하지만 실제로 적용해 보니 부분적인 에러 처리를 위해 route 구조를 계속 세분화하게 되었고,
결국 관리 포인트가 늘어나는 상황도 경험하게 됐다.

 

App Router에서 'error.tsx'를 어떻게 사용하는 게 적절한지 고민하면서
실제로 느꼈던 몇 가지 상황을 정리해 보려고 한다.


내용

#1. 특정 컴포넌트만 에러 fallback이 필요할 때

예를 들어 메인 페이지에서 추천 도서 영역만 fetch를 한다고 가정해 보자.

추천 도서 컴포넌트 예시
async function RecoBooks() {
  const response = await fetch("/api/books/random");

  if (!response.ok) {
    throw new Error("추천 도서 조회 실패");
  }

  const books = await response.json();
  return <BookList books={books} />;
}

 

 

이때 API가 실패했다고 해서

페이지 전체가 'error.tsx'로 대체되는 것은 UX 측면에서 과할 수 있다.

 

물론 segment를 더 세분화해서 error.tsx를 추가할 수도 있다.

추천 도서 영역 error.tsx
(with-searchbar)/
 └─ (reco-books)/
     ├─ page.tsx
     └─ error.tsx

 

 

하지만 단순히 추천 영역 하나를 위해 route 구조를 나누는 것이 부담으로 느껴졌다.

이런 경우에는 try / catch 구문으로 부분 처리하는 게 더 자연스럽다.

에러 부분 처리 예시
// 전체 Error Boundary가 아니라
// 컴포넌트 단위 fallback이 필요한 상황

async function RecoBooks() {
  try {
    const response = await fetch("/api/books/random");

    if (!response.ok) {
      throw new Error();
    }

    const books = await response.json();
    return <BookList books={books} />;
  } catch {
    return <div>추천 도서를 불러오지 못했습니다.</div>;
  }
}

 


#2. layout까지 같이 사라지면 안 되는 경우

'error.tsx'는 해당 segment subtree 전체를 대체하므로

Searchbar, Header, GNB와 같은 공통 UI까지 사라질 수 있다.

 

물론 layout을 유지하고 싶다면
layout 하위에 새로운 segment를 만들어 error.tsx를 추가하면 된다.

 

하지만 이 방식 역시 에러 처리를 위해 route 구조를 분리하는 느낌이 들었다.

 

게다가 마이페이지에서 일부 데이터만 실패했을 뿐인데
layout까지 사라지는 경험은 사용자 입장에서 “페이지가 죽은 느낌”을 줄 수 있으므로

'error.tsx' 보다 try / catch 구문이 더 적절하다 생각한다.


#3. 실시간 API처럼 실패 가능성이 높은 요청

실시간 API처럼 일시적인 실패가 자주 발생하는 경우에도 비슷한 고민이 있었다.

예를 들어 실시간 추천 도서 API를 호출한다고 가정해 보자.

실시간 추천 도서 요청 API
// app/(with-searchbar)/page.tsx

async function RecoBooks() {
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_API_URL}/api/reco-books`,
    { cache: "no-store" }
  );

  if (!response.ok) {
    throw new Error("추천 도서 로딩 실패");
  }

  const books = await response.json();

  return <BookList books={books} />;
}

export default async function Page() {
  return (
    <>
      <h2>메인 페이지</h2>
      <RecoBooks />
    </>
  );
}

 

 

여기서 API가 실패한다면 error를 던져서 'error.tsx'가 동작한다.

문제는 이 fallback을 분리하기 위해 또 다른 segment를 만들게 되므로 이 또한 부담이었다.

 

실시간 위젯 하나 때문에 segment와 error.tsx 파일이 계속 늘어나는 구조는

장기적으로 관리 비용이 커질 수 있다고 느꼈다.

 

이런 경우에는 Error Boundary를 추가하기보다 컴포넌트 단위 fallback이 더 현실적으로 느껴졌다.


#4. 언제 error.tsx를 쓰는 게 좋은가?

반대로 페이지 자체를 더 이상 보여줄 수 없는 상황에서는 'error.tsx'가 적절했다.

  • 인증 실패
  • 페이지 자체 렌더링 불가
  • 필수 데이터 fetch 실패

예를 들어 인증 기반 마이페이지라면

마이페이지 예시
// app/profile/page.tsx

async function getUser() {
  const response = await fetch("https://api.example.com/me");

  if (!response.ok) {
    throw new Error("인증 실패");
  }

  return response.json();
}

export default async function Page() {
  const user = await getUser();

  return <div>{user.name}님의 마이페이지</div>;
}

// 인증 실패하면
throw Error → error.tsx 실행

 

 

이 경우에는 Error Boundary가 자연스럽게 동작한다.

error.tsx 예시
"use client";

import { useRouter } from "next/navigation";

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  const router = useRouter();

  return (
    <div>
      <h2>페이지를 불러올 수 없습니다.</h2>
      <button onClick={() => reset()}>
        다시 시도
      </button>

      <button onClick={() => router.push("/")}>
        홈으로 이동
      </button>
    </div>
  );
}

정리

App Router에서 error.tsx는 강력한 Error Boundary 도구지만,
부분적인 에러 처리를 위해 segment를 계속 세분화하는 구조는
오히려 관리 포인트를 늘릴 수 있다고 느꼈다.

 

개인적으로는 치명적인 에러는 'error.tsx'로 처리하고, 부분 실패는 try / catch 구문으로 처리하는

정도로 기준을 잡고 사용하는 편이 더 자연스러웠다.

 

App Router에서는 Error Boundary 설계 자체가 라우트 아키텍처 설계와 연결된다는 점도 인상 깊었다.

반응형