[Next.js] Error Handling, 예측 가능한 것과 그렇지 않은 것

Woonil·2026년 1월 3일

Next.js

목록 보기
8/8

에러는 Expected errors와 Uncaught exceptions의 두 가지 분류로 나눌 수 있다. 이번 포스팅에서는 공식문서와 유튜브 Boaz 님의 영상을 참고하여 Next.js가 에러를 다루는 방법을 정리하고자 한다.

🤔개념

예측 가능한 에러 다루기

‘예측 가능한 에러(Expected error)’란 애플리케이션에서 발생할 수 있는 일반적인 동작의 에러이다. 예를 들어, 서버사이드에서 일어나는 폼 유효성 검증이나 실패한 요청이 있다. 이러한 에러는 명시적으로 처리하고 클라이언트에 반환해야 한다.

Server Functions(Actions)

서버 함수 내에서는 try...catch 문이 아닌 단순히 에러를 던짐으로써 반환한다. useActionState 를 사용하여 서버 함수 내의 반환된 예측 가능한 에러를 처리할 수 있다. 그러면 try...catch 는 어디에서 이루어지는가? useActionState 내에서 이러한 동작이 추상화되어 있으며 개발자는 단순히 이 훅에서 반환되는 값을 활용하여 폼 상태를 관리할 수 있다.

'use server'
 
export async function createPost(prevState: any, formData: FormData) {
  const title = formData.get('title')
  const content = formData.get('content')
 
  const res = await fetch('https://api.vercel.app/posts', {
    method: 'POST',
    body: { title, content },
  })
  const json = await res.json()
 
  // try...catch 문으로 에러를 처리하지 않고 바로 반환
  if (!res.ok) {
    return { message: 'Failed to create post' }
  }
}
'use client'
 
import { useActionState } from 'react'
import { createPost } from '@/app/actions'
 
const initialState = {
  message: '',
}
 
export function Form() {
  const [state, formAction, pending] = useActionState(createPost, initialState)
 
  return (
    <form action={formAction}>
      <label htmlFor="title">Title</label>
      <input type="text" id="title" name="title" required />
      <label htmlFor="content">Content</label>
      <textarea id="content" name="content" required />
      // createPost 수행 후 반환된 상태에 따라 UI를 표시
      {state?.message && <p aria-live="polite">{state.message}</p>}
      <button disabled={pending}>Create Post</button>
    </form>
  )
}

Server Components

서버 컴포넌트 내에서 데이터 페칭 수행 시, 조건부 에러 메시지를 렌더링하거나 리다이렉트 시킬 수 있다.

export default async function Page() {
  const res = await fetch(`https://...`)
  const data = await res.json()
 
  if (!res.ok) {
    return 'There was an error.'
  }
 
  return '...'
}

Not found

라우트 세그먼트 내에서 notFound 함수를 실행함으로써 미리 정의된 not-found.js 파일에서 404 UI를 띄울 수 있다.

// app/blog/[slug]/page.tsx
import { getPostBySlug } from '@/lib/posts'
 
export default async function Page({ params }: { params: { slug: string } }) {
  const { slug } = await params
  const post = getPostBySlug(slug)
 
  if (!post) {
    notFound()
  }
 
  return <div>{post.title}</div>
}
// app/blog/[slug]/not-found.tsx
export default function NotFound() {
  return <div>404 - Page Not Found</div>
}

예측 불가능한 에러 다루기

예측 불가능한 에러는 애플리케이션의 일반적인 흐름 중에 발생하지 않는 버그나 이슈를 말한다. 이는 결국 잡히지 않는 에러를 처리하는 것인데 자바스크립트에서는 보통 에러를 외부로 다시 던짐으로써 이를 외부에서 처리하는 방식을 취했다.

에러 바운더리로 감싸기

React에서는 자식 컴포넌트에서 잡히지 않은 에러를 상위에서 Errorboundary 클래스를 별도로 정의하여 사용했다면, Next.js는 동일한 동작을 error.js 로 추상화해놓았다.

  • error.js : 특정 라우트 세그먼트에서 발생하는 예외를 독립적으로 처리하여 사용자에게 더욱 안정적인 경험을 제공한다. 이는 애플리케이션의 견고성을 높이는 데 기여한다.
'use client' // 에러 바운더리는 클라이언트 컴포넌트여야 함
 
import { useEffect } from 'react'
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error)
  }, [error])
 
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button
        onClick={
          // Attempt to recover by trying to re-render the segment
          () => reset()
        }
      >
        Try again
      </button>
    </div>
  )
}

에러 바운더리는 렌더링 중 발생한 에러를 잡고 fallback UI를 제공함으로써 흰 화면이 보이며 애플리케이션 전체가 죽는 것을 방지한다.

여기서 유의해야 할 점이, 에러 바운더리는 이벤트 핸들러나 비동기 코드의 에러는 처리할 수 없고, 렌더링 중에 일어나는 동기적인 에러만 처리할 수 있다는 것이다. 비동기적인 에러의 경우 에러를 직접 잡아주거나 상태로써 관리해야 한다.

'use client'
 
import { useState } from 'react'
 
export function Button() {
  const [error, setError] = useState(null)
 
  const handleClick = () => {
    try {
      // do some work that might fail
      throw new Error('Exception')
    } catch (reason) {
      setError(reason)
    }
  }
 
  if (error) {
    /* render fallback UI */
  }
 
  return (
    <button type="button" onClick={handleClick}>
      Click me
    </button>
  )
}
profile
무엇이든 최선을 다하고자 노력합니다:)

0개의 댓글