FE/NextJS

NextJS 공식문서 파헤치기 (Error Handling)

-Daniel- 2024. 12. 10. 22:43

0️⃣ INTRO

  • 에러는 예상된 에러와 예상치 못한 예외 두가지로 나눌 수 있다.
    • 예상된 에러를 반환 값으로 모델링: 서버 액션에서 예상된 에러를 try/catch 문 보다는 useActionState를 사용하여 에러를 관리하고 클라이언트에 반환하는것이 좋다.
    • 예상치 못한 에러는 에러 경계로 처리: error.tsx, global-error.tsx 파일을 사용해 에러 경계를 구현하고 에러를 처리한다.
❓ useActionState ❓

- React v19 에 정식 도입된 훅으로서 폼 작업의 결과에 따라 상태를 업데이트할 수 있다.
- 기존의 폼 액션 함수와 초기 state를 전달받고, 폼에서 사용할 새로운 액션을 반환한다.
- 최신 폼 state와 액션이 여전히 진행(Pending) 중인지 여부도 반환한다.
import { useActionState } from "react";

// 함수가 실행될때 첫 번째 인수로 폼의 이전 state를 전달한다.
async function increment(previousState, formData) {
  return previousState + 1;
}

function StatefulForm({}) {
  const [state, formAction] = useActionState(increment, 0);
  return (
    <form>
      {state}
      <button formAction={formAction}>Increment</button>
    </form>
  )
}

1️⃣ Handling Expected Errors

  • 예상된 에러는 서버 측 폼 검증이나 실패한 요청등 정상적인 애플리케이션 운영 중에 발생할 수 있는 에러이다.
  • 이러한 에러는 명시적으로 처리하고 클라이언트에 반환해야 한다.

서버 액션을 통한 예상된 에러 핸들링

  • useActionState 훅(이전에는 useFormState)을 사용하여 서버 액션의 상태를 관리하고 에러를 처리한다.
  • try/catch 블록을 피하고, 에러를 throw 하는 대신 반환 값으로 모델링해야 한다.
  • 아래 예제는 서버 액션을 정의한 후 useActionState 훅을 통해 반환된 state를 사용하여 에러를 컨트롤 하는 예제이다.
// app/actions.js
'use server'

import { redirect } from 'next/navigation'

export async function createUser(prevState, formData) {
  const res = await fetch('https://...')
  const json = await res.json()

  if (!res.ok) {
    return { message: 'Please enter a valid email' }
  }

  redirect('/dashboard')
}

// app/ui/signup.jsx
'use client'

import { useActionState } from 'react'
import { createUser } from '@/app/actions'

const initialState = {
  message: '',
}

export function Signup() {
  const [state, formAction] = useActionState(createUser, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="email">Email</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      // 클라이언트 컴포넌트에서 반환된 상태를 사용하여 토스트 메시지 표시
      <p aria-live="polite">{state?.message}</p>
      <button>Sign up</button>
    </form>
  )
}

서버 컴포넌트에서 예상되는 에러 핸들링

  • 서버 컴포넌트 내에서 데이터를 가져올 때는 응답을 사용하여 조건부로 에러 메시지를 렌더링하거나 redirect할 수 있다.
export default async function Page() {
  const res = await fetch(`https://...`)
  const data = await res.json()

  if (!res.ok) {
    return 'There was an error.'
  }

  return '...'
}

2️⃣ Uncaught Exceptions

  • 정상적인 애플리케이션 흐름 중에 발생해서는 안되는 버그나 문제를 나타내는 예기치 않은 에러
  • 이러한 에러는 throw 하여 에러 경계에서 처리해야 한다.
🚨 상황에 따른 에러처리 🚨

- 일반적인 경우: 루트 레이아웃 하위에서 발생한 예기치 않은 에러는 `error.js`로 처리한다.
- 선택적인 경우: 중첩된 `error.js` 파일(ex: `app/dashboard/error.js`)을 사용하여 세분화된 예기치 않은 에러를 처리한다.
- 드문 경우: 루트 레이아웃에서 발생한 예기치 않은 에러는 `global-error.js`로 처리한다.

Error Boundaries 사용

  • Nextjs는 예상치 못한 예외를 처리하기 위해 Error Boundaries를 사용한다.
  • 자식 컴포넌트에서 발생한 에러를 캐치하고 충돌한 컴포넌트 트리 대신 fallback UI를 표시한다
  • 경로 세그먼트 내에 error.jsx | error.tsx 파일을 추가하고 React 컴포넌트를 내보내어 에러 경계를 생성할 수 있다.
  • 에러 바운더리는 클라이언트 컴포넌트로 선언되어야 한다.
'use client' // Error boundaries must be Client Components

import { useEffect } from 'react'

export default function Error({ error, reset }) {
  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>
  )
}

중첩된 라우트에서 에러 핸들링

  • 에러는 가장 가까운 부모 에러 경계로 전파된다.
  • 경로 계층 구조의 다양한 수준에서 error.jsx | error.tsx 파일을 배치하여 세분화된 에러 처리가 가능하다.

계층에 따른 ErrorBoundary의 위치 구조

전역 에러 핸들링

  • 루트 레이아웃에서 에러를 처리해야 할 때는 app/global-error.js 를 사용해야 한다.
  • 루트 앱 디렉토리에 위치해야한다.
  • 전역 에러 UI는 활성화될 때 루트 레이아웃이나 템플릿을 대체하므로 <html>, <body> 태그를 정의해야 한다.
'use client' // Error boundaries must be Client Components

export default function GlobalError({ error, reset }) {
  return (
    // global-error must include html and body tags
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  )
}

📚 REFERENCE