01 SOFTWARE
SDK

Error Handling

에러 처리 가이드

Error Handling

SDK는 사용자 친화적인 에러 메시지와 타입 안전한 에러 처리를 제공합니다.

에러 타입

SDKError

모든 SDK 에러의 기본 클래스입니다.

class SDKError extends Error {
  code: string              // 에러 코드
  statusCode?: number       // HTTP 상태 코드 (해당되는 경우)
  userMessage: string       // 사용자 친화적 메시지
  suggestion: string        // 해결 제안

  getUserMessage(): string  // 사용자 친화적 메시지 반환
}

NetworkError

네트워크 연결 실패 시 발생합니다.

class NetworkError extends SDKError {
  code: 'NETWORK_ERROR'
  userMessage: '네트워크 연결을 확인해주세요.'
  suggestion: '인터넷 연결을 확인하거나 잠시 후 다시 시도해주세요.'
}

ApiError

API 요청 실패 시 발생합니다.

class ApiError extends SDKError {
  code: 'API_ERROR'
  statusCode: number
  userMessage: string       // 상태 코드에 따른 메시지
  suggestion: string        // 상태 코드에 따른 제안
}

HTTP 상태 코드별 메시지

상태 코드메시지제안
400잘못된 요청입니다.입력한 정보를 다시 확인해주세요.
401인증이 필요합니다.로그인 후 다시 시도해주세요.
403접근 권한이 없습니다.권한이 필요한 작업입니다.
404요청한 데이터를 찾을 수 없습니다.URL이나 ID를 확인해주세요.
429너무 많은 요청을 보냈습니다.잠시 후 다시 시도해주세요.
500서버 오류가 발생했습니다.잠시 후 다시 시도해주세요.
503서비스를 일시적으로 사용할 수 없습니다.잠시 후 다시 시도해주세요.

ValidationError

데이터 검증 실패 시 발생합니다.

class ValidationError extends SDKError {
  code: 'VALIDATION_ERROR'
  userMessage: '입력한 정보를 다시 확인해주세요.'
  suggestion: '필수 필드를 모두 입력했는지 확인해주세요.'
}

TimeoutError

요청 시간 초과 시 발생합니다.

class TimeoutError extends SDKError {
  code: 'TIMEOUT_ERROR'
  userMessage: '요청 시간이 초과되었습니다.'
  suggestion: '잠시 후 다시 시도해주세요.'
}

타입 가드

isNetworkError()

import { isNetworkError } from '@01.software/sdk'

try {
  await client.from('products').find()
} catch (error) {
  if (isNetworkError(error)) {
    console.error('네트워크 에러:', error.getUserMessage())
    console.error('제안:', error.suggestion)
  }
}

isApiError()

import { isApiError } from '@01.software/sdk'

try {
  await client.from('products').findById('invalid_id')
} catch (error) {
  if (isApiError(error)) {
    console.error('API 에러:', error.statusCode)
    console.error('메시지:', error.getUserMessage())

    if (error.statusCode === 404) {
      // 404 처리
    }
  }
}

isValidationError()

import { isValidationError } from '@01.software/sdk'

try {
  await client.from('products').create({
    // 잘못된 데이터
  })
} catch (error) {
  if (isValidationError(error)) {
    console.error('검증 에러:', error.getUserMessage())
    // 폼 에러 표시
  }
}

isTimeoutError()

import { isTimeoutError } from '@01.software/sdk'

try {
  await client.from('products').find()
} catch (error) {
  if (isTimeoutError(error)) {
    console.error('타임아웃:', error.getUserMessage())
    // 재시도 버튼 표시
  }
}

응답 타입 가드

isSuccessResponse()

import { isSuccessResponse } from '@01.software/sdk'

const response = await client.from('products').find()

if (isSuccessResponse(response)) {
  // TypeScript가 response.data의 타입을 자동 추론
  console.log(response.data.docs)
  console.log(response.data.pagination)
}

isErrorResponse()

import { isErrorResponse } from '@01.software/sdk'

const response = await client.from('products').find()

if (isErrorResponse(response)) {
  // TypeScript가 response.error의 타입을 자동 추론
  console.error(response.error.code)
  console.error(response.error.message)
  console.error(response.error.getUserMessage())
}

에러 처리 패턴

Try-Catch 패턴

import {
  isNetworkError,
  isApiError,
  isValidationError,
  isTimeoutError,
} from '@01.software/sdk'

async function fetchProducts() {
  try {
    const { data } = await client.from('products').find()
    return data?.docs || []
  } catch (error) {
    if (isNetworkError(error)) {
      // 네트워크 에러 처리
      toast.error('네트워크 연결을 확인해주세요')
      return []
    }

    if (isApiError(error)) {
      // API 에러 처리
      if (error.statusCode === 404) {
        toast.error('데이터를 찾을 수 없습니다')
      } else if (error.statusCode >= 500) {
        toast.error('서버 오류가 발생했습니다')
      } else {
        toast.error(error.getUserMessage())
      }
      return []
    }

    if (isValidationError(error)) {
      // 검증 에러 처리
      toast.error(error.getUserMessage())
      return []
    }

    if (isTimeoutError(error)) {
      // 타임아웃 에러 처리
      toast.error('요청 시간이 초과되었습니다')
      return []
    }

    // 기타 에러
    console.error('Unknown error:', error)
    toast.error('오류가 발생했습니다')
    return []
  }
}

응답 체크 패턴

import { isSuccessResponse, isErrorResponse } from '@01.software/sdk'

async function createProduct(data: ProductInput) {
  const response = await client.from('products').create(data)

  if (isSuccessResponse(response)) {
    toast.success('상품이 생성되었습니다')
    router.push(`/products/${response.data.id}`)
    return response.data
  }

  if (isErrorResponse(response)) {
    toast.error(response.error.getUserMessage())
    console.error('Error details:', response.error)
    return null
  }
}

React 컴포넌트에서의 에러 처리

React Query 에러 처리

'use client'

import { client } from '@/lib/client'
import { isApiError } from '@01.software/sdk'

export default function ProductsPage() {
  const { data, error, isLoading } = client.query.useCollection('products', {
    limit: 10,
  })

  if (isLoading) return <div>Loading...</div>

  if (error) {
    if (isApiError(error) && error.statusCode === 404) {
      return <div>상품을 찾을 수 없습니다</div>
    }

    return (
      <div className="error">
        <h2>오류가 발생했습니다</h2>
        <p>{error.getUserMessage()}</p>
        <p className="text-sm text-gray-600">{error.suggestion}</p>
      </div>
    )
  }

  return (
    <div>
      {data?.docs.map(product => (
        <div key={product.id}>{product.name}</div>
      ))}
    </div>
  )
}

Error Boundary

'use client'

import { Component, ReactNode } from 'react'
import { SDKError } from '@01.software/sdk'

interface Props {
  children: ReactNode
  fallback?: (error: Error) => ReactNode
}

interface State {
  hasError: boolean
  error?: Error
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error) {
    console.error('Error caught by boundary:', error)

    if (error instanceof SDKError) {
      console.error('SDK Error:', {
        code: error.code,
        message: error.getUserMessage(),
        suggestion: error.suggestion,
      })
    }
  }

  render() {
    if (this.state.hasError) {
      if (this.props.fallback) {
        return this.props.fallback(this.state.error!)
      }

      const error = this.state.error

      return (
        <div className="error-boundary">
          <h2>오류가 발생했습니다</h2>
          {error instanceof SDKError && (
            <>
              <p>{error.getUserMessage()}</p>
              <p className="text-sm">{error.suggestion}</p>
            </>
          )}
          <button onClick={() => this.setState({ hasError: false })}>
            다시 시도
          </button>
        </div>
      )
    }

    return this.props.children
  }
}

// 사용
export default function App() {
  return (
    <ErrorBoundary>
      <ProductsPage />
    </ErrorBoundary>
  )
}

Toast 알림

import { toast } from 'sonner'
import { isNetworkError, isApiError } from '@01.software/sdk'

async function handleAction() {
  try {
    await client.from('products').create(data)
    toast.success('상품이 생성되었습니다')
  } catch (error) {
    if (isNetworkError(error)) {
      toast.error('네트워크 연결을 확인해주세요', {
        description: error.suggestion,
      })
    } else if (isApiError(error)) {
      toast.error(error.getUserMessage(), {
        description: error.suggestion,
      })
    } else {
      toast.error('오류가 발생했습니다')
    }
  }
}

전역 에러 처리

React Query 전역 에러 핸들러

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { toast } from 'sonner'
import { isApiError, isNetworkError } from '@01.software/sdk'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      onError: (error) => {
        console.error('Query error:', error)

        if (isNetworkError(error)) {
          toast.error('네트워크 연결을 확인해주세요')
        } else if (isApiError(error)) {
          toast.error(error.getUserMessage())
        }
      },
    },
    mutations: {
      onError: (error) => {
        console.error('Mutation error:', error)

        if (isApiError(error)) {
          toast.error(error.getUserMessage())
        } else {
          toast.error('작업 중 오류가 발생했습니다')
        }
      },
    },
  },
})

export default function App({ children }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

커스텀 에러 로거

import { createBrowserClient } from '@01.software/sdk'
import * as Sentry from '@sentry/nextjs'

const client = createBrowserClient({
  clientKey: process.env.NEXT_PUBLIC_SOFTWARE_CLIENT_KEY!,
  errorLogger: {
    log: (error) => {
      // Sentry로 에러 전송
      Sentry.captureException(error, {
        tags: {
          source: 'sdk',
          code: error.code,
        },
        extra: {
          userMessage: error.getUserMessage(),
          suggestion: error.suggestion,
        },
      })

      // 콘솔 로깅 (개발 환경)
      if (process.env.NODE_ENV === 'development') {
        console.error('SDK Error:', {
          code: error.code,
          message: error.message,
          userMessage: error.getUserMessage(),
          suggestion: error.suggestion,
        })
      }
    },
  },
})

재시도 로직

SDK는 자동으로 재시도를 수행하지만, 커스터마이징할 수 있습니다.

const client = createBrowserClient({
  clientKey: process.env.NEXT_PUBLIC_SOFTWARE_CLIENT_KEY!,
  retry: {
    maxRetries: 3,
    retryableStatuses: [408, 429, 500, 502, 503, 504],
    retryDelay: (attempt) => {
      // Exponential backoff: 1초 → 2초 → 4초 (최대 10초)
      return Math.min(1000 * 2 ** attempt, 10000)
    },
  },
  debug: {
    logErrors: true, // 에러 로깅
  },
})

네트워크 에러와 특정 HTTP 상태 코드는 자동으로 재시도됩니다.

디버깅

디버그 모드

const client = createBrowserClient({
  clientKey: process.env.NEXT_PUBLIC_SOFTWARE_CLIENT_KEY!,
  debug: {
    logRequests: true,
    logResponses: true,
    logErrors: true,
  },
})

에러 상세 정보

try {
  await client.from('products').find()
} catch (error) {
  if (error instanceof SDKError) {
    console.log({
      code: error.code,
      statusCode: error.statusCode,
      message: error.message,
      userMessage: error.getUserMessage(),
      suggestion: error.suggestion,
      stack: error.stack,
    })
  }
}

다음 단계

On this page