Neople LogoNeople SDK JS

오류 처리

Neople SDK JS에서 발생할 수 있는 오류와 효과적인 처리 방법

오류 처리

SDK에서 발생할 수 있는 다양한 오류 상황과 이를 효과적으로 처리하는 방법을 알아보세요.

오류 타입

SDK에서 발생할 수 있는 주요 오류 타입들입니다.

HTTP 상태 코드 오류

401 Unauthorized - 인증 실패

import { NeopleDFClient } from 'neople-sdk-js';

try {
  const client = new NeopleDFClient('invalid-api-key');
  const result = await client.searchCharacter('테스트');
} catch (error) {
  if (error.status === 401) {
    console.error('API 키가 유효하지 않습니다. API 키를 확인해주세요.');
    // 사용자에게 API 키 재입력 요청 또는 로그인 페이지로 리다이렉트
  }
}

404 Not Found - 리소스를 찾을 수 없음

try {
  const character = await dfClient.getCharacter('cain', 'invalid-character-id');
} catch (error) {
  if (error.status === 404) {
    console.error('캐릭터를 찾을 수 없습니다.');
    // 사용자에게 캐릭터가 존재하지 않음을 알림
  }
}

429 Too Many Requests - 요청 한도 초과

try {
  const result = await dfClient.searchCharacter('테스트');
} catch (error) {
  if (error.status === 429) {
    console.error('요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.');
    // 지수 백오프로 재시도 구현
    await new Promise(resolve => setTimeout(resolve, 5000));
    // 재시도 로직
  }
}

500 Internal Server Error - 서버 오류

try {
  const result = await dfClient.searchCharacter('테스트');
} catch (error) {
  if (error.status >= 500) {
    console.error('서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
    // 사용자에게 일시적 오류임을 알리고 재시도 유도
  }
}

네트워크 오류

연결 실패

try {
  const result = await dfClient.searchCharacter('테스트');
} catch (error) {
  if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
    console.error('네트워크 연결을 확인해주세요.');
    // 오프라인 모드로 전환 또는 사용자에게 연결 상태 확인 요청
  }
}

타임아웃

const client = new NeopleDFClient(apiKey, {
  timeout: 10000, // 10초 타임아웃
  onTimeout: () => {
    console.log('요청이 시간 초과되었습니다.');
  },
});

try {
  const result = await client.searchCharacter('테스트');
} catch (error) {
  if (error.name === 'TimeoutError') {
    console.error('요청 시간이 초과되었습니다. 네트워크 상태를 확인해주세요.');
  }
}

종합적인 오류 처리

기본 오류 처리 패턴

import { NeopleDFClient } from 'neople-sdk-js';

async function handleApiCall<T>(
  apiCall: () => Promise<T>,
  fallbackValue?: T
): Promise<T | null> {
  try {
    return await apiCall();
  } catch (error) {
    // HTTP 오류 처리
    if (error.status) {
      switch (error.status) {
        case 401:
          console.error('인증 실패: API 키를 확인해주세요');
          // 인증 관련 처리 (예: 로그인 페이지로 리다이렉트)
          break;

        case 403:
          console.error('접근 권한이 없습니다');
          break;

        case 404:
          console.error('요청한 리소스를 찾을 수 없습니다');
          break;

        case 429:
          console.error('요청 한도 초과: 잠시 후 다시 시도해주세요');
          // 재시도 로직 구현
          break;

        case 500:
        case 502:
        case 503:
        case 504:
          console.error('서버 오류: 잠시 후 다시 시도해주세요');
          break;

        default:
          console.error(
            `예상치 못한 HTTP 오류: ${error.status} ${error.statusText}`
          );
      }
    }
    // 네트워크 오류 처리
    else if (error.code) {
      switch (error.code) {
        case 'ECONNREFUSED':
        case 'ENOTFOUND':
        case 'ECONNRESET':
          console.error('네트워크 연결 오류: 인터넷 연결을 확인해주세요');
          break;

        case 'ETIMEDOUT':
          console.error('요청 시간 초과: 네트워크가 느릴 수 있습니다');
          break;

        default:
          console.error(`네트워크 오류: ${error.code}`);
      }
    }
    // 기타 오류
    else {
      console.error('예상치 못한 오류:', error.message);
    }

    return fallbackValue || null;
  }
}

// 사용 예제
const character = await handleApiCall(
  () => dfClient.getCharacter('cain', 'character-id'),
  null // 오류 시 반환할 기본값
);

재시도 로직 구현

async function withRetry<T>(
  apiCall: () => Promise<T>,
  maxRetries: number = 3,
  baseDelay: number = 1000
): Promise<T> {
  let lastError: Error;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await apiCall();
    } catch (error) {
      lastError = error;

      // 재시도하지 않을 오류들
      if (
        error.status === 401 ||
        error.status === 403 ||
        error.status === 404
      ) {
        throw error;
      }

      // 마지막 시도였다면 오류 발생
      if (attempt === maxRetries) {
        break;
      }

      // 지수 백오프로 대기
      const delay = baseDelay * Math.pow(2, attempt - 1);
      console.log(`재시도 ${attempt}/${maxRetries} - ${delay}ms 후 재시도`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw lastError;
}

// 사용 예제
try {
  const result = await withRetry(
    () => dfClient.searchCharacter('테스트'),
    3, // 최대 3번 재시도
    1000 // 1초 기본 대기시간
  );
  console.log(result);
} catch (error) {
  console.error('모든 재시도 실패:', error.message);
}

전역 오류 핸들러

import { NeopleDFClient, NeopleCyphersClient } from 'neople-sdk-js';

// 전역 오류 처리 함수
function createGlobalErrorHandler() {
  return (error: Error) => {
    // 로깅 서비스에 오류 전송
    console.error('API 오류 발생:', {
      message: error.message,
      status: error.status,
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent,
    });

    // 오류 추적 서비스에 전송 (예: Sentry)
    // Sentry.captureException(error);

    // 사용자에게 친화적인 오류 메시지 표시
    showUserFriendlyError(error);
  };
}

function showUserFriendlyError(error: Error) {
  let message = '알 수 없는 오류가 발생했습니다.';

  if (error.status === 401) {
    message = 'API 키가 유효하지 않습니다.';
  } else if (error.status === 404) {
    message = '요청한 데이터를 찾을 수 없습니다.';
  } else if (error.status === 429) {
    message = '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.';
  } else if (error.status >= 500) {
    message = '서버에 일시적인 문제가 발생했습니다.';
  } else if (error.code && error.code.startsWith('E')) {
    message = '네트워크 연결을 확인해주세요.';
  }

  // UI에 오류 메시지 표시 (예: 토스트, 모달 등)
  // toast.error(message);
  console.error(message);
}

// 클라이언트 설정 시 전역 오류 핸들러 적용
const dfClient = new NeopleDFClient(apiKey, {
  onError: createGlobalErrorHandler(),
});

const cyphersClient = new NeopleCyphersClient(apiKey, {
  onError: createGlobalErrorHandler(),
});

React에서의 오류 처리

커스텀 훅

import { useState, useCallback } from 'react';
import { NeopleDFClient } from 'neople-sdk-js';

interface ApiState<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

function useApi<T>() {
  const [state, setState] = useState<ApiState<T>>({
    data: null,
    loading: false,
    error: null
  });

  const execute = useCallback(async (apiCall: () => Promise<T>) => {
    setState({ data: null, loading: true, error: null });

    try {
      const data = await apiCall();
      setState({ data, loading: false, error: null });
      return data;
    } catch (error) {
      let errorMessage = '오류가 발생했습니다.';

      if (error.status === 401) {
        errorMessage = 'API 키가 유효하지 않습니다.';
      } else if (error.status === 404) {
        errorMessage = '데이터를 찾을 수 없습니다.';
      } else if (error.status === 429) {
        errorMessage = '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.';
      } else if (error.status >= 500) {
        errorMessage = '서버 오류가 발생했습니다.';
      }

      setState({ data: null, loading: false, error: errorMessage });
      throw error;
    }
  }, []);

  return { ...state, execute };
}

// 사용 예제
function CharacterSearch() {
  const { data, loading, error, execute } = useApi();
  const [characterName, setCharacterName] = useState('');

  const handleSearch = async () => {
    if (!characterName.trim()) return;

    try {
      await execute(() => dfClient.searchCharacter(characterName));
    } catch (error) {
      // 오류는 이미 state에 저장됨
    }
  };

  return (
    <div>
      <input
        value={characterName}
        onChange={(e) => setCharacterName(e.target.value)}
        placeholder="캐릭터 이름 입력"
      />
      <button onClick={handleSearch} disabled={loading}>
        {loading ? '검색 중...' : '검색'}
      </button>

      {error && <div className="error">{error}</div>}
      {data && <div>검색 결과: {data.rows.length}</div>}
    </div>
  );
}

Error Boundary

import React, { Component, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

class ApiErrorBoundary 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, errorInfo: any) {
    console.error('API Error Boundary caught an error:', error, errorInfo);

    // 오류 추적 서비스에 전송
    // Sentry.captureException(error, { extra: errorInfo });
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="error-boundary">
          <h2>오류가 발생했습니다</h2>
          <p>페이지를 새로고침하거나 잠시 후 다시 시도해주세요.</p>
          <button onClick={() => this.setState({ hasError: false })}>
            다시 시도
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// 사용 예제
function App() {
  return (
    <ApiErrorBoundary>
      <CharacterSearch />
    </ApiErrorBoundary>
  );
}

개발 환경에서의 디버깅

상세 로깅

const client = new NeopleDFClient(apiKey, {
  debug: process.env.NODE_ENV === 'development',
  onRequest: (url, options) => {
    if (process.env.NODE_ENV === 'development') {
      console.log('🚀 API Request:', {
        url,
        method: options.method || 'GET',
        headers: options.headers,
        timestamp: new Date().toISOString(),
      });
    }
  },
  onResponse: response => {
    if (process.env.NODE_ENV === 'development') {
      console.log('✅ API Response:', {
        status: response.status,
        statusText: response.statusText,
        headers: response.headers,
        timestamp: new Date().toISOString(),
      });
    }
  },
  onError: error => {
    if (process.env.NODE_ENV === 'development') {
      console.error('❌ API Error:', {
        message: error.message,
        status: error.status,
        response: error.response,
        stack: error.stack,
        timestamp: new Date().toISOString(),
      });
    } else {
      // 프로덕션에서는 간단한 로깅
      console.error('API Error:', error.message);
    }
  },
});

모의 오류 생성 (테스트용)

// 테스트 환경에서 의도적으로 오류 발생
if (process.env.NODE_ENV === 'test') {
  const mockClient = {
    searchCharacter: async (name: string) => {
      // 특정 이름으로 테스트 오류 발생
      if (name === 'test-401') {
        throw { status: 401, message: 'Unauthorized' };
      }
      if (name === 'test-429') {
        throw { status: 429, message: 'Too Many Requests' };
      }
      if (name === 'test-network') {
        throw { code: 'ECONNREFUSED', message: 'Connection refused' };
      }

      // 정상 응답
      return { rows: [{ characterName: name }] };
    },
  };
}

모니터링 및 알림

오류 추적

interface ErrorMetrics {
  errorCount: number;
  errorsByType: Record<string, number>;
  lastError?: {
    message: string;
    timestamp: Date;
    status?: number;
  };
}

class ErrorTracker {
  private metrics: ErrorMetrics = {
    errorCount: 0,
    errorsByType: {},
  };

  trackError(error: Error) {
    this.metrics.errorCount++;

    const errorType = error.status
      ? `HTTP_${error.status}`
      : error.code || 'UNKNOWN';
    this.metrics.errorsByType[errorType] =
      (this.metrics.errorsByType[errorType] || 0) + 1;

    this.metrics.lastError = {
      message: error.message,
      timestamp: new Date(),
      status: error.status,
    };

    // 임계값 초과 시 알림
    if (this.metrics.errorCount > 10) {
      this.sendAlert();
    }
  }

  private sendAlert() {
    console.warn('오류 발생 횟수가 임계값을 초과했습니다:', this.metrics);
    // 실제 알림 서비스 연동 (예: Slack, 이메일 등)
  }

  getMetrics(): ErrorMetrics {
    return { ...this.metrics };
  }

  reset() {
    this.metrics = { errorCount: 0, errorsByType: {} };
  }
}

const errorTracker = new ErrorTracker();

// 클라이언트에 적용
const client = new NeopleDFClient(apiKey, {
  onError: error => {
    errorTracker.trackError(error);
  },
});

모범 사례

1. 사용자 친화적인 오류 메시지

function getErrorMessage(error: Error): string {
  const messages = {
    401: '로그인이 필요합니다.',
    403: '이 기능을 사용할 권한이 없습니다.',
    404: '요청한 정보를 찾을 수 없습니다.',
    429: '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.',
    500: '서버에 문제가 발생했습니다. 잠시 후 다시 시도해주세요.',
    503: '서비스가 일시적으로 사용할 수 없습니다.',
  };

  return messages[error.status] || '예상치 못한 오류가 발생했습니다.';
}

2. 점진적 성능 저하

async function getCharacterWithFallback(serverId: string, characterId: string) {
  try {
    // 1차 시도: 전체 캐릭터 정보
    return await dfClient.getCharacter(serverId, characterId);
  } catch (error) {
    if (error.status === 500) {
      try {
        // 2차 시도: 기본 캐릭터 정보만
        const searchResult = await dfClient.searchCharacter(characterId);
        return searchResult.rows.find(char => char.characterId === characterId);
      } catch (fallbackError) {
        // 3차 시도: 캐시된 데이터 또는 기본값
        return (
          getCachedCharacterData(characterId) || {
            characterId,
            characterName: '알 수 없음',
            level: 0,
          }
        );
      }
    }
    throw error;
  }
}

3. 오류 상태 관리

// 전역 상태 관리 (예: Redux, Zustand)
interface AppState {
  errors: {
    network: boolean;
    auth: boolean;
    server: boolean;
  };
  retryCount: number;
}

function errorReducer(state: AppState, action: any) {
  switch (action.type) {
    case 'SET_NETWORK_ERROR':
      return { ...state, errors: { ...state.errors, network: true } };
    case 'SET_AUTH_ERROR':
      return { ...state, errors: { ...state.errors, auth: true } };
    case 'CLEAR_ERRORS':
      return {
        ...state,
        errors: { network: false, auth: false, server: false },
      };
    case 'INCREMENT_RETRY':
      return { ...state, retryCount: state.retryCount + 1 };
    default:
      return state;
  }
}

이러한 오류 처리 패턴을 통해 안정적이고 사용자 친화적인 애플리케이션을 구축할 수 있습니다.