매일매일
프론트엔드에 관련한 질문이에요.

Next.js App Router에서 error, loading, not-found 파일은 어떻게 동작하나요?

2026-04-15Next.jsApp Routererror.tsxloading.tsxnot-found.tsxErrorBoundary

핵심 요약 (Summary)

Next.js App Router는 라우트 세그먼트 단위로 error.tsx, loading.tsx, not-found.tsx 파일을 배치해 에러, 로딩, 404 상태를 선언적으로 처리합니다. 이 파일들은 Next.js가 자동으로 React의 ErrorBoundary와 Suspense로 감싸주므로, 수동으로 상태를 관리할 필요가 없습니다.


왜 이런 문제가 발생하나요? (Why)

Pages Router에서는 에러, 로딩, 404 상태를 컴포넌트 내부에서 직접 분기 처리하거나, _error.tsx 같은 전역 파일로만 대응할 수 있었습니다. 세그먼트별로 다른 UI를 보여주기 어려웠고, 코드가 분산되어 유지보수가 복잡했습니다.

App Router는 이 문제를 특수 파일(Convention File)로 해결합니다.

이 파일들은 라우트 폴더마다 독립적으로 배치할 수 있어, 페이지별로 다른 에러/로딩 UI를 선언적으로 구성할 수 있습니다.


예시 코드 — 잘못된 사용 (Bad Example)

hljs
// app/posts/[id]/page.tsx
// 에러, 로딩, 404 상태를 컴포넌트 내부에서 직접 처리하는 경우

'use client';

import { useEffect, useState } from 'react';

export default function PostPage({ params }: { params: { id: string } }) {
  const [post, setPost] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [notFound, setNotFound] = useState(false);

  useEffect(() => {
    fetch(`/api/posts/${params.id}`)
      .then((res) => {
        if (res.status === 404) { setNotFound(true); return; }
        if (!res.ok) { setError(true); return; }
        return res.json();
      })
      .then((data) => { if (data) setPost(data); })
      .catch(() => setError(true))
      .finally(() => setLoading(false));
  }, [params.id]);

  if (loading) return <p>불러오는 중...</p>;
  if (notFound) return <p>게시글을 찾을 수 없습니다.</p>;
  if (error) return <p>오류가 발생했습니다.</p>;

  return <h1>{post.title}</h1>;
}

서버 컴포넌트의 이점을 포기하고 클라이언트에서 데이터를 직접 가져오고 있습니다. 상태 분기가 컴포넌트 내부에 뒤섞여 코드가 복잡해지고, 에러/로딩 UI를 재사용하기 어렵습니다.


올바른 사용법 (Good Example)

hljs
// app/posts/[id]/loading.tsx
// 데이터를 가져오는 동안 자동으로 표시됩니다.

export default function Loading() {
  return (
    <div>
      <div style={{ background: '#E0E0E0', height: 32, borderRadius: 4, marginBottom: 16 }} />
      <div style={{ background: '#E0E0E0', height: 16, borderRadius: 4, marginBottom: 8 }} />
      <div style={{ background: '#E0E0E0', height: 16, borderRadius: 4, width: '60%' }} />
    </div>
  );
}
hljs
// app/posts/[id]/error.tsx
// 런타임 에러 발생 시 자동으로 표시됩니다. 반드시 'use client'가 필요합니다.

'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div>
      <p>게시글을 불러오는 중 오류가 발생했습니다.</p>
      <p>{error.message}</p>
      <button onClick={reset}>다시 시도</button>
    </div>
  );
}
hljs
// app/posts/[id]/not-found.tsx
// notFound()가 호출되거나 존재하지 않는 경로 접근 시 표시됩니다.

export default function NotFound() {
  return (
    <div>
      <h2>게시글을 찾을 수 없습니다.</h2>
      <p>삭제되었거나 존재하지 않는 게시글입니다.</p>
    </div>
  );
}
hljs
// app/posts/[id]/page.tsx
// 서버 컴포넌트에서 notFound()를 호출하면 not-found.tsx가 렌더링됩니다.

import { notFound } from 'next/navigation';

type Props = {
  params: Promise<{ id: string }>;
};

export default async function PostPage({ params }: Props) {
  const { id } = await params;
  const res = await fetch(`https://api.example.com/posts/${id}`);

  if (res.status === 404) notFound(); // not-found.tsx 렌더링
  if (!res.ok) throw new Error('게시글 조회 실패'); // error.tsx 렌더링

  const post = await res.json();

  return <h1>{post.title}</h1>;
}

각 특수 파일이 역할을 나눠가지므로 page.tsx는 데이터 페칭과 렌더링에만 집중할 수 있습니다. 폴더 구조만 보아도 어떤 세그먼트에서 어떤 상태를 처리하는지 파악할 수 있습니다.


정리 (Conclusion)

파일역할컴포넌트 종류트리거 조건
loading.tsx로딩 중 폴백 UI서버 컴포넌트상위 Suspense 해소 전
error.tsx에러 폴백 UI클라이언트 컴포넌트 필수런타임 에러 발생 시
not-found.tsx404 UI서버 컴포넌트notFound() 호출 또는 매칭 실패

세 파일 모두 라우트 폴더 단위로 독립 배치가 가능합니다. 전체 앱에 하나의 에러/로딩 UI를 쓰고 싶다면 app/ 루트에, 특정 섹션만 다르게 하고 싶다면 해당 폴더에 추가하면 됩니다.


추가 학습 자료 공유합니다.


[FOOTER]

Code
로고: 매일매일
Copyright © 2026 매일매일. All rights reserved.

Contact: kangmu238@gmail.com
Socials: Github

목록으로 돌아가기