/** * 네트워크 상태 관리 및 오류 처리를 위한 유틸리티 */ // 네트워크 상태 저장 키 const NETWORK_STATUS_KEY = 'network_status'; // 네트워크 재시도 설정 const MAX_RETRY_COUNT = 3; const INITIAL_RETRY_DELAY = 1000; // 1초 // 네트워크 재연결 시도 설정 const RECONNECT_INTERVAL = 5000; // 5초마다 재연결 시도 const MAX_RECONNECT_ATTEMPTS = 10; // 최대 재연결 시도 횟수 let reconnectAttempts = 0; let reconnectTimer: ReturnType | null = null; // 네트워크 모니터링 상태 let isMonitoring = false; let networkCheckInterval: ReturnType | null = null; // 네트워크 상태 타입 export type NetworkStatus = 'online' | 'offline' | 'reconnecting'; // 동기화 작업 큐 아이템 타입 export interface SyncQueueItem { id: string; type: 'upload' | 'download' | 'delete'; entityType: 'transaction' | 'budget' | 'categoryBudget'; data: Record; timestamp: number; retryCount: number; } // 네트워크 이벤트 핸들러 인터페이스 interface NetworkEventHandlers { handleOnline: () => void; handleOffline: () => void; } // 확장된 Window 인터페이스 interface ExtendedWindow extends Window { __networkHandlers?: NetworkEventHandlers; } // 메모리에 네트워크 상태 저장 let networkStatus: NetworkStatus | null = null; /** * 현재 네트워크 상태 확인 (개선 버전) */ export const checkNetworkStatus = async (): Promise => { try { // 더 빠른 타임아웃 설정 (2초) const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 2000); // 여러 엔드포인트로 확인하여 신뢰성 향상 const endpoints = [ 'https://www.google.com', 'https://www.cloudflare.com', 'https://www.apple.com' ]; // 첫 번째 성공 응답 시 온라인으로 판단 const isOnline = await Promise.any( endpoints.map(url => fetch(url, { method: 'HEAD', signal: controller.signal }) .then(res => res.ok) ) ); clearTimeout(timeoutId); setNetworkStatus(isOnline ? 'online' : 'offline'); return isOnline; } catch (error) { console.error('[네트워크] 연결 확인 실패:', error); setNetworkStatus('offline'); return false; } }; /** * 네트워크 상태 저장 (메모리 기반) */ export const setNetworkStatus = (status: NetworkStatus): void => { // 메모리에 상태 저장 networkStatus = status; // 상태 변경 이벤트 발생 window.dispatchEvent(new CustomEvent('networkStatusChange', { detail: status })); console.log(`[네트워크] 상태 변경: ${status}`); }; /** * 네트워크 상태 가져오기 (메모리 기반) */ export const getNetworkStatus = (): NetworkStatus => { return networkStatus || 'online'; }; /** * 네트워크 상태 모니터링 시작 */ export const startNetworkMonitoring = (): void => { if (isMonitoring) { return; } isMonitoring = true; // 초기 네트워크 상태 확인 checkNetworkStatus().then(isOnline => { setNetworkStatus(isOnline ? 'online' : 'offline'); if (!isOnline) { // 오프라인 상태면 재연결 시도 시작 reconnectTimer = setTimeout(attemptReconnect, RECONNECT_INTERVAL); } }); // 브라우저 온라인/오프라인 이벤트 리스너 const handleOnline = () => { console.log('[네트워크] 브라우저 온라인 이벤트 감지'); setNetworkStatus('online'); reconnectAttempts = 0; // 대기 중인 동기화 작업 처리 processPendingSyncQueue().catch(err => { console.error('[네트워크] 온라인 전환 후 동기화 큐 처리 중 오류:', err); }); }; const handleOffline = () => { console.log('[네트워크] 브라우저 오프라인 이벤트 감지'); setNetworkStatus('offline'); // 재연결 시도 시작 reconnectTimer = setTimeout(attemptReconnect, RECONNECT_INTERVAL); }; // 이벤트 리스너 등록 및 참조 저장 window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); // 전역 변수에 이벤트 핸들러 참조 저장 (window as ExtendedWindow).__networkHandlers = { handleOnline, handleOffline }; // 정기적인 네트워크 상태 확인 (30초마다) networkCheckInterval = setInterval(async () => { try { const isOnline = await checkNetworkStatus(); const currentStatus = getNetworkStatus(); if (isOnline && currentStatus !== 'online') { // 오프라인에서 온라인으로 변경 setNetworkStatus('online'); reconnectAttempts = 0; // 대기 중인 동기화 작업 처리 processPendingSyncQueue().catch(err => { console.error('[네트워크] 정기 확인 후 동기화 큐 처리 중 오류:', err); }); } else if (!isOnline && currentStatus === 'online') { // 온라인에서 오프라인으로 변경 setNetworkStatus('offline'); // 재연결 시도 시작 if (!reconnectTimer) { reconnectTimer = setTimeout(attemptReconnect, RECONNECT_INTERVAL); } } } catch (error) { console.error('[네트워크] 정기 네트워크 상태 확인 중 오류:', error); } }, 30000); console.log('[네트워크] 네트워크 모니터링 시작'); }; /** * 네트워크 상태 모니터링 중지 */ export const stopNetworkMonitoring = (): void => { if (!isMonitoring) { return; } isMonitoring = false; // 브라우저 이벤트 리스너 제거 const handlers = (window as ExtendedWindow).__networkHandlers; if (handlers) { window.removeEventListener('online', handlers.handleOnline); window.removeEventListener('offline', handlers.handleOffline); delete (window as ExtendedWindow).__networkHandlers; } // 인터벌 및 타이머 정리 if (networkCheckInterval) { clearInterval(networkCheckInterval); networkCheckInterval = null; } if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } reconnectAttempts = 0; console.log('[네트워크] 네트워크 모니터링 중지'); }; /** * 네트워크 재연결 시도 함수 */ export const attemptReconnect = async (): Promise => { if (getNetworkStatus() === 'online') { // 이미 온라인 상태면 재연결 필요 없음 reconnectAttempts = 0; if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } return true; } // 재연결 시도 중 상태로 변경 setNetworkStatus('reconnecting'); try { const isOnline = await checkNetworkStatus(); if (isOnline) { // 재연결 성공 setNetworkStatus('online'); reconnectAttempts = 0; console.log('[네트워크] 재연결 성공'); // 대기 중인 동기화 작업 처리 processPendingSyncQueue().catch(err => { console.error('[네트워크] 재연결 후 동기화 큐 처리 중 오류:', err); }); return true; } else { // 재연결 실패 reconnectAttempts++; console.log(`[네트워크] 재연결 실패 (시도 ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`); if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { // 최대 재연결 시도 횟수 초과 setNetworkStatus('offline'); reconnectAttempts = 0; console.log('[네트워크] 최대 재연결 시도 횟수 초과, 오프라인 모드로 전환'); return false; } // 다음 재연결 시도 예약 reconnectTimer = setTimeout(attemptReconnect, RECONNECT_INTERVAL); return false; } } catch (error) { console.error('[네트워크] 재연결 시도 중 오류:', error); setNetworkStatus('offline'); reconnectAttempts = 0; return false; } }; /** * 온라인 상태 처리 */ const handleOnline = async () => { console.log('[네트워크] 온라인 상태 감지'); setNetworkStatus('reconnecting'); // 실제 연결 확인 (브라우저 이벤트는 때때로 부정확할 수 있음) const isReallyOnline = await checkNetworkStatus(); if (isReallyOnline) { console.log('[네트워크] 온라인 상태 확인됨, 보류 중인 작업 처리 시작'); processPendingSyncQueue(); } }; /** * 오프라인 상태 처리 */ const handleOffline = () => { console.log('[네트워크] 오프라인 상태 감지'); setNetworkStatus('offline'); }; // 동기화 작업 큐 관리 const SYNC_QUEUE_KEY = 'sync_queue'; /** * 동기화 작업을 큐에 추가 */ export const addToSyncQueue = (operation: { type: 'upload' | 'download' | 'delete'; entityType: 'transaction' | 'budget' | 'categoryBudget'; data: Record; timestamp?: number; retries?: number; }): void => { // 타임스탬프 및 재시도 횟수 추가 const enhancedOperation = { ...operation, timestamp: operation.timestamp || Date.now(), retries: operation.retries || 0 }; // 로컬 스토리지에서 기존 큐 가져오기 const queueString = localStorage.getItem(SYNC_QUEUE_KEY) || '[]'; let queue: typeof enhancedOperation[] = []; try { queue = JSON.parse(queueString); } catch (error) { console.error('[네트워크] 동기화 큐 파싱 오류:', error); queue = []; } // 큐에 작업 추가 queue.push(enhancedOperation); // 큐 저장 localStorage.setItem(SYNC_QUEUE_KEY, JSON.stringify(queue)); console.log(`[네트워크] 동기화 큐에 작업 추가: ${operation.type} ${operation.entityType}`); }; /** * 동기화 큐 가져오기 */ export const getSyncQueue = (): SyncQueueItem[] => { try { const queueStr = localStorage.getItem(SYNC_QUEUE_KEY); return queueStr ? JSON.parse(queueStr) : []; } catch (error) { console.error('[네트워크] 동기화 큐 조회 실패:', error); return []; } }; /** * 동기화 큐에서 항목 제거 */ export const removeFromSyncQueue = (itemId: string): void => { try { const queue = getSyncQueue(); const updatedQueue = queue.filter(item => item.id !== itemId); localStorage.setItem(SYNC_QUEUE_KEY, JSON.stringify(updatedQueue)); console.log(`[네트워크] 동기화 큐에서 작업 제거: ${itemId}`); } catch (error) { console.error('[네트워크] 동기화 큐 항목 제거 실패:', error); } }; /** * 동기화 큐 항목 업데이트 */ export const updateSyncQueueItem = (itemId: string, updates: Partial): void => { try { const queue = getSyncQueue(); const updatedQueue = queue.map(item => item.id === itemId ? { ...item, ...updates } : item ); localStorage.setItem(SYNC_QUEUE_KEY, JSON.stringify(updatedQueue)); } catch (error) { console.error('[네트워크] 동기화 큐 항목 업데이트 실패:', error); } }; /** * 보류 중인 동기화 작업 처리 */ export const processPendingSyncQueue = async (): Promise => { if (getNetworkStatus() !== 'online') { console.log('[네트워크] 오프라인 상태에서 동기화 큐 처리 불가'); return; } const queue = getSyncQueue(); if (queue.length === 0) { console.log('[네트워크] 처리할 동기화 작업 없음'); return; } console.log(`[네트워크] ${queue.length}개의 보류 중인 동기화 작업 처리 시작`); // 큐의 각 항목을 순차적으로 처리 for (const item of queue) { try { // 여기서 실제 동기화 작업 수행 (구현 필요) // 예: await processSyncItem(item); // 성공 시 큐에서 제거 removeFromSyncQueue(item.id); console.log(`[네트워크] 동기화 작업 성공: ${item.id}`); } catch (error) { console.error(`[네트워크] 동기화 작업 실패: ${item.id}`, error); // 재시도 횟수 증가 const newRetryCount = item.retryCount + 1; if (newRetryCount <= MAX_RETRY_COUNT) { // 지수 백오프 알고리즘 적용 (재시도 간격을 점점 늘림) const retryDelay = INITIAL_RETRY_DELAY * Math.pow(2, newRetryCount - 1); console.log(`[네트워크] ${retryDelay}ms 후 재시도 예정 (${newRetryCount}/${MAX_RETRY_COUNT})`); // 재시도 횟수 업데이트 updateSyncQueueItem(item.id, { retryCount: newRetryCount }); } else { // 최대 재시도 횟수 초과 시 실패로 처리하고 큐에서 제거 console.error(`[네트워크] 최대 재시도 횟수 초과, 작업 실패: ${item.id}`); removeFromSyncQueue(item.id); // 사용자에게 알림 (구현 필요) // 예: notifyUser(`동기화 작업 실패: ${item.entityType}`); } } } }; /** * 네트워크 요청 래퍼 함수 (재시도 로직 포함) */ export const withRetry = async ( fn: () => Promise, options: { maxRetries?: number; retryDelay?: number; onRetry?: (attempt: number, error: Error | unknown) => void; shouldRetry?: (error: Error | unknown) => boolean; } = {} ): Promise => { const { maxRetries = 3, retryDelay = 1000, onRetry = (attempt, error) => console.log(`[네트워크] 재시도 중 (${attempt}/${maxRetries}):`, error), shouldRetry = (error) => true // 기본적으로 모든 오류에 대해 재시도 } = options; let lastError: Error | unknown; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { // 네트워크 상태 확인 if (attempt > 0 && getNetworkStatus() !== 'online') { throw new Error('오프라인 상태입니다.'); } return await fn(); } catch (error) { lastError = error; // 마지막 시도였거나 재시도하지 않아야 하는 오류인 경우 if (attempt === maxRetries || !shouldRetry(error)) { throw error; } // 재시도 콜백 호출 onRetry(attempt + 1, error); // 재시도 전 지연 await new Promise(resolve => setTimeout(resolve, retryDelay * (attempt + 1))); } } // 이 코드는 실행되지 않아야 하지만, TypeScript 컴파일러를 위해 추가 throw lastError; }; /** * 네트워크 요청 타임아웃 래퍼 함수 */ export const withTimeout = ( promise: Promise, timeoutMs: number = 10000 ): Promise => { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`요청 시간 초과 (${timeoutMs}ms)`)); }, timeoutMs); promise .then(result => { clearTimeout(timeoutId); resolve(result); }) .catch(error => { clearTimeout(timeoutId); reject(error); }); }); }; /** * 네트워크 상태 변경 이벤트 리스너 등록 */ export const onNetworkStatusChange = (callback: (status: NetworkStatus) => void): () => void => { const handler = (event: Event) => { const customEvent = event as CustomEvent; callback(customEvent.detail); }; window.addEventListener('networkStatusChange', handler); // 구독 해제 함수 반환 return () => { window.removeEventListener('networkStatusChange', handler); }; };