개요
Next.js App Router를 사용하면서 기존 Page Router 방식과 데이터 fetching 구조가 크게 달라졌다.
이전에는 getServerSideProps, getStaticProps를 통해 서버 데이터를 가져왔다면
App Router에서는 서버 컴포넌트가 직접 fetch를 수행한다.
이번 글에서는 다음 내용을 정리한다.
- Page Router와 App Router 데이터 페칭 차이
- 서버 컴포넌트 에러 처리 방식
- error.tsx 동작 구조 (Error Boundary)
- 실수하기 쉬운 error.tsx 배치 예시
내용
#1. Page Router에서의 데이터 fetching
기존 Page Router에서는 서버 데이터 fetching을 위해 아래 함수를 사용했다.
Page Router 데이터 fetching 예시
# fetch example
// 서버에서만 실행되는 코드 (SSR)
export async function getServerSideProps() {
const response = await fetch("https://example.com/api/books");
const books = await response.json();
return {
props: {
books,
},
};
}
// 서버에서만 실행되는 코드 (SSG)
export async function getStaticProps() {
const response = await fetch("https://example.com/api/books");
const books = await response.json();
return {
props: {
books,
},
revalidate: 60, // ISR (60초마다 재생성)
};
}
두 함수는 서버에서 실행되고
결과는 Page 컴포넌트의 Props로 전달된다.
Page Router fetching 결과 사용 예시
# page example
type Book = {
id: number;
title: string;
author: string;
};
type Props = {
books: Book[];
};
// 서버에서 한번 실행되고,
// 클라이언트에서 hydration 시에도 실행될 수 있음
export function Page({ books }: Props) {
return (
<div>
<h2>도서 목록</h2>
<ul>
{books.map((book) => (
<li key={book.id}>
{book.title} - {book.author}
</li>
))}
</ul>
</div>
);
}
이 구조는 데이터를 Page 레벨에서만 가져올 수 있기 때문에
하위 컴포넌트로 전달하는 Props가 많아져 'Props Drilling'이 발생하기 쉬웠다.
#2. App Router에서의 데이터 fetching
App Router에서는 서버 컴포넌트 개념을 도입해서 컴포넌트가 직접 데이터 fetch를 수행키로 설계됐다.
Page Router에서는 데이터를 Page 레벨에서만 가져올 수 있었던 구조적 제약이 있었으나,
서버 컴포넌트를 통해 상위에서 데이터를 내려주는 구조 없이 필요한 위치에서 데이터를 가져오는 방식이다.
export default async function Page() {
// 데이터 페칭 코드
const response = await fetch(...);
const allBooks: BookData[] = await response.json();
return (...)
}
그렇지만 fetching으로 수반되는 에러 핸들링도 서버 컴포넌트 단위로 고려하게 됐다.
#3. error.tsx는 무엇인가
서버 컴포넌트에서 에러를 처리하는 방법은 크게 2가지이다.
1. try / catch 기반 처리
2. 'error.tsx' 기반의 Error Boundary
여기서는 'error.tsx' 기반의 Error Boundary에 대해 자세하게 살펴본다.
'error.tsx'는 단순한 에러 화면 컴포넌트가 아니라
Next.js가 내부적으로 React Error Boundary로 감싸서 동작하게 만드는 fallback UI이다.
즉, 'error.tsx'는 Error Boundary 역할을 하는 컴포넌트로 정리할 수 있다.
해당 파일을 사용하면 특정 레이아웃이나 페이지에서 발생하는 에러를 공통 로직으로 처리할 수 있다.
Error Boundary를 구성하기 위해서는 다음의 세부내용을 따라야 한다.
[ 1. 에러 핸들러는 반드시 'error.tsx'명으로 파일을 생성해야 한다. ]
- Next.js는 파일 이름 기반으로 Error Boundary를 인식하므로 정해진 파일명을 사용해야 한다.
[ 2. 서버 컴포넌트의 에러를 처리하지만, 반드시 클라이언트 컴포넌트로 정의해야 한다. ]
- Error Boundary는 클라이언트에서 실행되도록 설계 됐고, 오류를 복구할 때 브라우저가 제공하는 기능을 사용하기 때문이다.
[ 3. 에러를 처리하려는 라우트 segment와 동일 레벨 또는 상위 레벨 디렉터리에 배치한다. ]
- 에러가 발생한 컴포넌트를 기준으로 가장 가까운 'error.tsx'를 찾아서 핸들링하기 때문이다.
- 동일 레벨은 좁은 범위, 상위 레벨은 더 넓은 범위를 감싸게 된다.
'error.tsx' 파일 구조 예시
1. 동일 레벨 구조 (page와 같은 위치에 error.tsx 존재)
app/
├─ layout.tsx // Global Layout
├─ error.tsx // Root Error Boundary
├─ (with-searchbar)/
│ ├─ layout.tsx
│ ├─ page.tsx // 여기서 에러 발생한다고 가정
│ └─ error.tsx // ← 가장 가까운 Error Boundary (우선 적용)
└─ admin-config/
├─ layout.tsx
└─ page.tsx
2. 상위 레벨 구조 (상위 segment의 error.tsx가 처리)
app/
├─ layout.tsx
├─ error.tsx // Root Error Boundary
├─ (with-searchbar)/
│ ├─ layout.tsx
│ └─ page.tsx // 에러 발생 -> root error.tsx가 처리
└─ (without-searchbar)/
├─ layout.tsx
└─ page.tsx
#4. 실수하기 쉬운 error.tsx 배치 예시
App Router에서 'error.tsx'는 단순 에러 화면이 아니라
Error Boundary 역할을 수행하기 때문에 위치에 따라 동작 범위가 달라진다.
실제로 사용하면서 헷갈렸던 구조를 정리해 봤다.
[ 1. Root에만 error.tsx를 두면 layout까지 사라진다. ]
처음에는 공통 에러 처리를 위해 root에만 error.tsx를 두는 구조를 작성했다.
구조 예시
// (with-searchbar)/layout.tsx 는 검색창 UI를 포함한 레이아웃을 렌더링한다고 가정한다.
// 그런데 (with-searchbar)/page.tsx 에서 에러가 발생했고,
// 해당 segment 내부에는 error.tsx가 존재하지 않는 상황이다.
// 이 경우 Next.js는 상위 경로에 위치한 error.tsx를 Error 컴포넌트로 사용한다.
app/
├─ layout.tsx
├─ error.tsx // Root Error Boundary
├─ (with-searchbar)/
│ ├─ layout.tsx // 검색창 포함 레이아웃
│ └─ page.tsx // 여기서 에러 발생
└─ (without-searchbar)/
├─ layout.tsx
└─ page.tsx
여기서 '(with-searchbar)/page.tsx'에서 에러가 발생하면
root 'error.tsx'가 subtree 전체를 대체한다.
따라서 searchbar layout도 사라지고, 공통 UI까지 함께 제거된다.
Error Boundary 범위를 너무 폭넓게 정의해서 발생한 문제였다.
[ 2. segment마다 error.tsx를 과하게 만드는 경우 ]
layout이 유지되게 하려고 아래와 같이 만들기도 했다.
구조 예시
app/
├─ layout.tsx
├─ (with-searchbar)/
│ ├─ layout.tsx
│ ├─ error.tsx
│ └─ page.tsx
└─ admin-config/
├─ error.tsx
└─ page.tsx
문제는 'error.tsx' 파일이 계속 늘어난다는 단점이 있었다.
공통 에러 처리를 하려고 시작했는데 결국 관리 포인트가 늘어나는 구조가 될 수도 있었다.
이 경우에는
- try / catch 구문
- 공통 fallback 컴포넌트
같은 방식과 함께 사용하는 게 더 자연스러웠다.
정리하면, error.tsx의 위치는 단순 파일 구조가 아니라
어디까지 UI를 유지하면서 에러를 처리할 것인가를 결정하는 기준이 된다.
정리
- 서버 컴포넌트에서는 직접 fetch를 사용할 수 있다.
- error.tsx는 App Router에서 Error Boundary 역할을 한다.
- error.tsx의 위치에 따라 layout 렌더링 범위가 달라진다.
- App Router로 넘어오면서 데이터 흐름이 Page 중심에서 컴포넌트 중심으로 이동한 느낌을 받았다.
App Router의 error.tsx는 단순 에러 화면이 아니라 Error Boundary 역할을 하는 구조다.
위치에 따라 layout까지 함께 대체될 수 있기 때문에 파일을 어디에 두느냐가 생각보다 중요하다.
공통 에러 처리를 위해 편리한 구조지만, segment마다 error.tsx를 과하게 늘리는 것보다는
필요한 범위에 맞게 설계하는 것이 더 중요해 보였다.
'Web' 카테고리의 다른 글
| [Next.js] 스트리밍(Streaming)과 스켈레톤(Skeleton) - loading.txs, Suspense (0) | 2026.02.20 |
|---|---|
| [Next.js] App Router에서 error.tsx를 쓰다가 try/catch로 돌아온 이유 (0) | 2026.02.16 |
| [React] useRef()로 DOM 참조하기 (0) | 2026.02.13 |