Skip to content

API 에러 처리하기

정재희 edited this page Nov 7, 2023 · 1 revision

목적

  • 에러 처리를 사전 정의된 기준에 따라 처리하고자 합니다.
  • 일관성 있는 에러 처리를 하고자 합니다.
  • 서버와 클라이언트 및 렌더링과 핸들러에서의 에러 처리 방안을 제안하고자 합니다.

설명

1. FetchAPI의 변경사항

아래와 같이 responseHandler를 정의하여 기존 rest로 얻은 response에 감싸 일관된 에러를 throw합니다.

  private async responseHandler(response: Response): Promise<any> {
    if (!response.ok) {
      switch (response.status) {
        case 401:
          throw new UnauthorizedError(response)
        case 403:
          throw new ForbiddenError(response)
        case 404:
          throw new NotFoundError(response)
        case 500:
          throw new ServerError(response)
        default:
          throw new ApiError(response, 'An unexpected error occurred')
      }
    }
    return await response.json()
  }

예시

  public async delete(
    endpoint: string,
    nextInit: RequestInit = {},
    customHeaders: { [key: string]: string } = {},
  ): Promise<any> {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      method: 'DELETE',
      headers: { ...this.headers, ...customHeaders },
      ...nextInit,
    })
    return this.responseHandler(response)
  }

2. 커스텀 에러 클래스 생성

아래와 같이 네가지 에러 코드에 대한 에러를 사용합니다.

isInstanceOfApiError를 통해 ApiError인지 확인할 수 있습니다.

import ErrorMessages from '@/config/errorMessages'

export class ApiError extends Error {
  public statusCode: number
  public statusText: string
  public response: Response

  constructor(response: Response, message?: string) {
    super(message)
    this.statusCode = response.status
    this.statusText = response.statusText
    this.response = response
    this.name = 'ApiError'
  }
}

export function isInstanceOfApiError(error: unknown): error is ApiError {
  return error instanceof ApiError
}

export class NotFoundError extends ApiError {
  constructor(response: Response, message?: string) {
    super(response, message)
    this.name = 'NotFoundError'
    this.message = this.message ?? ErrorMessages.NotFound
  }
}

export class ForbiddenError extends ApiError {
  constructor(response: Response, message?: string) {
    super(response, message)
    this.name = 'ForbiddenError'
    this.message = message ?? ErrorMessages.Forbidden
  }
}

export class UnauthorizedError extends ApiError {
  constructor(response: Response, message?: string) {
    super(response, message)
    this.name = 'UnauthorizedError'
    this.message = this.message ?? ErrorMessages.Unauthorized
  }
}

export class ServerError extends ApiError {
  constructor(response: Response, message?: string) {
    super(response, message)
    this.name = 'ServerError'
    this.message = this.message ?? ErrorMessages.ServerError
  }
}

3. 렌더링에서 사용하기

렌더링에서는 에러를 throw 해주고, 이에 근접한 page에서 error.tsx 파일을 통해 에러를 처리합니다.

관련 내용은 에러 핸들링을 참고해주세요.

주의: error.tsx에서는 throw된 에러가 가려지고 일반 Error로 처리되기 때문에 error.message를 통해 에러를 구분할 수 있습니다.

예시

'use client'

// Error components must be Client Components
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useToast } from '@/components/ui/Toast/useToast'
import AppPath from '@/config/appPath'
import ErrorMessages from '@/config/errorMessages'

export default function ErrorPage({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  const router = useRouter()
  const { toast } = useToast()
  useEffect(() => {
    console.log(error.digest, error.message, error.name)
    if (error.message === ErrorMessages.Forbidden) {
      console.log('ForbiddenError')
      toast({
        title: 'Forbidden',
        description: 'You do not have permission to access this page.',
        duration: 2000,
      })
    }
    if (error.message === ErrorMessages.Unauthorized) {
      router.push(AppPath.login())
      toast({
        title: 'Unauthorized',
        description: 'Please login to access this page.',
        duration: 2000,
      })
    }
    if (error.message === ErrorMessages.NotFound) {
      toast({
        title: 'Not Found',
        description: 'Please login to access this page.',
        duration: 2000,
      })
    }
  }, [error, router, toast])

  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>
        <strong>Error:</strong> {error.message} ({error?.name})
      </p>
      <button
        onClick={
          // Attempt to recover by trying to re-render the segment
          () => reset()
        }
      >
        Try again
      </button>
    </div>
  )
}

4. 핸들러에서 사용하기

렌더링이 아닌 핸들러에서의 비동기 동작으로 호출되는 api call은 ErrorBoundary로 전달되지 않습니다.

때문에 handleApiError를 통해 얻은 값을 이용해 적절한 에러 처리를 할 수 있습니다.

  const onClickButton = async () => {
    try {
      await getTest()
    } catch (error) {
      const { shouldRedirect, message } = handleApiError(error)
      if (shouldRedirect) {
        router.push(shouldRedirect)
      } else {
        console.log(shouldRedirect, error)
        toast({
          title: 'Error',
          description: message,
          duration: 1000,
        })
      }
    }
  }