개요
App Router에서는 'error.tsx'를 통해 공통 에러 처리를 쉽게 구성할 수 있다.
'error.tsx'는 단순 에러 화면이 아니라 route segment 단위로 동작하는
Error Boundary 역할을 수행하기 때문에 에러 처리 범위를 비교적 명확하게 나눌 수 있는 구조다.
처음에는 segment마다 'error.tsx'를 배치하면
에러 처리 구조를 깔끔하게 설계할 수 있을 것이라 생각했다.
하지만 실제로 적용해 보니 부분적인 에러 처리를 위해 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 설계 자체가 라우트 아키텍처 설계와 연결된다는 점도 인상 깊었다.
'Web' 카테고리의 다른 글
| [Next.js] 스트리밍(Streaming)과 스켈레톤(Skeleton) - loading.txs, Suspense (0) | 2026.02.20 |
|---|---|
| [Next.js] App Router 데이터 fetching 구조와 에러 핸들링 (0) | 2026.02.16 |
| [React] useRef()로 DOM 참조하기 (0) | 2026.02.13 |