오류 처리
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;
}
}
이러한 오류 처리 패턴을 통해 안정적이고 사용자 친화적인 애플리케이션을 구축할 수 있습니다.