diff --git a/src/App.tsx b/src/App.tsx index 9c60889..c6f6e53 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,7 +22,7 @@ import Settings from './pages/Settings'; import { BudgetProvider } from './contexts/BudgetContext'; import PrivateRoute from './components/auth/PrivateRoute'; // 전역 오류 핸들러 -const handleError = (error: any) => { +const handleError = (error: Error | unknown) => { console.error('앱 오류 발생:', error); }; @@ -77,7 +77,7 @@ function App() { // 웹뷰 콘텐츠가 완전히 로드되었을 때만 스플래시 화면을 숨김 const onAppReady = async () => { try { - // 1초 후에 스플래시 화면을 숨김 (콘텐츠가 완전히 로드될 시간 확보) + // 스플래시 화면을 더 빠르게 숨김 (데이터 로딩과 별도로 진행) setTimeout(async () => { try { await SplashScreen.hide(); @@ -85,7 +85,7 @@ function App() { } catch (err) { console.error('스플래시 화면 숨김 오류:', err); } - }, 1000); + }, 500); // 500ms로 줄임 } catch (err) { console.error('앱 준비 오류:', err); } @@ -96,9 +96,13 @@ function App() { // 추가 보호장치: 페이지 로드 시 다시 실행 const handleLoad = () => { + // 즉시 스플래시 화면을 숨김 시도 + SplashScreen.hide().catch(() => {}); + + // 백업 시도 setTimeout(() => { SplashScreen.hide().catch(() => {}); - }, 1000); + }, 300); }; window.addEventListener('load', handleLoad); diff --git a/src/components/security/DataResetSection.tsx b/src/components/security/DataResetSection.tsx index a2ec32a..58e2600 100644 --- a/src/components/security/DataResetSection.tsx +++ b/src/components/security/DataResetSection.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { useAuth } from '@/contexts/auth/AuthProvider'; +import { useAuth } from '@/contexts/auth'; import { useDataReset } from '@/hooks/useDataReset'; import DataResetDialog from './DataResetDialog'; import { isSyncEnabled } from '@/utils/sync/syncSettings'; diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 4c481a9..dcaa83c 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { AuthProvider, useAuth } from './auth/AuthProvider'; +import { AuthProvider } from './auth/AuthProvider'; +import { useAuth } from './auth/useAuth'; export { AuthProvider, useAuth }; diff --git a/src/contexts/auth/AuthProvider.tsx b/src/contexts/auth/AuthProvider.tsx index d281ddc..7f8cc0b 100644 --- a/src/contexts/auth/AuthProvider.tsx +++ b/src/contexts/auth/AuthProvider.tsx @@ -1,13 +1,12 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { supabase } from '@/lib/supabase'; import { Session, User } from '@supabase/supabase-js'; import { toast } from '@/hooks/useToast.wrapper'; import { AuthContextType } from './types'; import * as authActions from './authActions'; import { clearAllToasts } from '@/hooks/toast/toastManager'; - -const AuthContext = createContext(undefined); +import { AuthContext } from './useAuth'; export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [session, setSession] = useState(null); @@ -15,46 +14,75 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const [loading, setLoading] = useState(true); useEffect(() => { - // 현재 세션 체크 + // 현재 세션 체크 - 최적화된 버전 const getSession = async () => { try { + console.log('세션 로딩 시작'); + + // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 + await new Promise(resolve => queueMicrotask(() => resolve())); + const { data, error } = await supabase.auth.getSession(); if (error) { console.error('세션 로딩 중 오류:', error); } else if (data.session) { - setSession(data.session); - setUser(data.session.user); + // 상태 업데이트를 마이크로태스크로 지연 + queueMicrotask(() => { + setSession(data.session); + setUser(data.session.user); + console.log('세션 로딩 완료'); + }); + } else { + console.log('활성 세션 없음'); } } catch (error) { console.error('세션 확인 중 예외 발생:', error); } finally { - setLoading(false); + // 로딩 상태 업데이트를 마이크로태스크로 지연 + queueMicrotask(() => { + setLoading(false); + }); } }; - getSession(); + // 초기 세션 로딩 - 약간 지연시켜 UI 렌더링 우선시 + setTimeout(() => { + getSession(); + }, 100); - // auth 상태 변경 리스너 + // auth 상태 변경 리스너 - 최적화된 버전 const { data: { subscription } } = supabase.auth.onAuthStateChange( async (event, session) => { console.log('Supabase auth 이벤트:', event); + // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 + await new Promise(resolve => queueMicrotask(() => resolve())); + if (session) { - setSession(session); - setUser(session.user); + // 상태 업데이트를 마이크로태스크로 지연 + queueMicrotask(() => { + setSession(session); + setUser(session.user); + }); } else if (event === 'SIGNED_OUT') { - setSession(null); - setUser(null); - - // 로그아웃 시 열려있는 모든 토스트 제거 - clearAllToasts(); - - // 로그아웃 이벤트 발생시켜 SyncSettings 등에서 감지하도록 함 - window.dispatchEvent(new Event('auth-state-changed')); + // 상태 업데이트를 마이크로태스크로 지연 + queueMicrotask(() => { + setSession(null); + setUser(null); + + // 로그아웃 시 열려있는 모든 토스트 제거 + clearAllToasts(); + + // 로그아웃 이벤트 발생시켜 SyncSettings 등에서 감지하도록 함 + window.dispatchEvent(new Event('auth-state-changed')); + }); } - setLoading(false); + // 로딩 상태 업데이트를 마이크로태스크로 지연 + queueMicrotask(() => { + setLoading(false); + }); } ); @@ -78,10 +106,4 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children return {children}; }; -export const useAuth = () => { - const context = useContext(AuthContext); - if (context === undefined) { - throw new Error('useAuth는 AuthProvider 내부에서 사용해야 합니다'); - } - return context; -}; +// useAuth 후크는 useAuth.ts 파일로 이동했습니다 diff --git a/src/contexts/auth/index.ts b/src/contexts/auth/index.ts index a682455..e91dffa 100644 --- a/src/contexts/auth/index.ts +++ b/src/contexts/auth/index.ts @@ -1,3 +1,4 @@ -export { AuthProvider, useAuth } from './AuthProvider'; +export { AuthProvider } from './AuthProvider'; +export { useAuth } from './useAuth'; export type { AuthContextType } from './types'; diff --git a/src/contexts/auth/useAuth.ts b/src/contexts/auth/useAuth.ts new file mode 100644 index 0000000..8d568c5 --- /dev/null +++ b/src/contexts/auth/useAuth.ts @@ -0,0 +1,17 @@ +import { useContext, createContext } from 'react'; +import { AuthContextType } from './types'; + +// AuthContext 생성 +export const AuthContext = createContext(undefined); + +/** + * 인증 컨텍스트에 접근하기 위한 커스텀 훅 + * AuthProvider 내부에서만 사용해야 함 + */ +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth는 AuthProvider 내부에서 사용해야 합니다'); + } + return context; +}; diff --git a/src/contexts/budget/BudgetContext.tsx b/src/contexts/budget/BudgetContext.tsx index 724fecb..aca1ca3 100644 --- a/src/contexts/budget/BudgetContext.tsx +++ b/src/contexts/budget/BudgetContext.tsx @@ -1,29 +1,8 @@ -import React, { createContext, useContext } from 'react'; +import React from 'react'; import { useBudgetState } from './useBudgetState'; -import { BudgetData, BudgetPeriod, Transaction } from './types'; - -// 컨텍스트 인터페이스 정의 -interface BudgetContextType { - transactions: Transaction[]; - selectedTab: BudgetPeriod; - setSelectedTab: (tab: BudgetPeriod) => void; - budgetData: BudgetData; - categoryBudgets: Record; - getCategorySpending: () => Array<{ - title: string; - current: number; - total: number; - }>; - addTransaction: (transaction: Transaction) => void; - updateTransaction: (transaction: Transaction) => void; - deleteTransaction: (id: string) => void; - handleBudgetGoalUpdate: (type: BudgetPeriod, amount: number, newCategoryBudgets?: Record) => void; - resetBudgetData?: () => void; // 선택적 필드로 추가 -} - -// 컨텍스트 생성 -const BudgetContext = createContext(undefined); +import { BudgetContext, BudgetContextType } from './useBudget'; +import { BudgetPeriod } from './types'; // 컨텍스트 프로바이더 컴포넌트 export const BudgetProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -36,13 +15,7 @@ export const BudgetProvider: React.FC<{ children: React.ReactNode }> = ({ childr ); }; -// 훅을 통한 컨텍스트 접근 -export const useBudget = (): BudgetContextType => { - const context = useContext(BudgetContext); - if (context === undefined) { - throw new Error('useBudget must be used within a BudgetProvider'); - } - return context; -}; +// useBudget 훅은 useBudget.ts 파일로 이동했습니다 +export { useBudget, BudgetContextType } from './useBudget'; export type { BudgetPeriod } from './types'; diff --git a/src/contexts/budget/hooks/useBudgetDataState.ts b/src/contexts/budget/hooks/useBudgetDataState.ts index 17c0861..36280eb 100644 --- a/src/contexts/budget/hooks/useBudgetDataState.ts +++ b/src/contexts/budget/hooks/useBudgetDataState.ts @@ -12,79 +12,60 @@ import { calculateSpentAmounts } from '../budgetUtils'; +import { Transaction } from '../types'; + // 예산 데이터 상태 관리 훅 -export const useBudgetDataState = (transactions: any[]) => { +export const useBudgetDataState = (transactions: Transaction[]) => { const [budgetData, setBudgetData] = useState(loadBudgetDataFromStorage()); const [selectedTab, setSelectedTab] = useState("daily"); const [isInitialized, setIsInitialized] = useState(false); - // 초기 로드 및 이벤트 리스너 설정 + // 초기 로드 및 이벤트 리스너 설정 - 최적화된 버전 useEffect(() => { - const loadBudget = () => { + // 예산 데이터 로드 함수 - 비동기 처리로 변경 + const loadBudget = async () => { try { console.log('예산 데이터 로드 시도 중...'); + + // 비동기 작업을 마이크로태스크로 지연 + await new Promise(resolve => queueMicrotask(() => resolve())); + const loadedData = loadBudgetDataFromStorage(); - console.log('예산 데이터 로드됨:', loadedData); // 새로 로드한 데이터와 현재 데이터가 다를 때만 업데이트 if (JSON.stringify(loadedData) !== JSON.stringify(budgetData)) { - console.log('예산 데이터 변경 감지됨, 상태 업데이트'); - setBudgetData(loadedData); + // 상태 업데이트를 마이크로태스크로 지연 + queueMicrotask(() => { + setBudgetData(loadedData); + console.log('예산 데이터 업데이트 완료'); + }); } - // 최근 데이터 로드 시간 기록 - localStorage.setItem('lastBudgetDataLoadTime', new Date().toISOString()); - + // 초기화 상태 업데이트 if (!isInitialized) { - setIsInitialized(true); + queueMicrotask(() => { + setIsInitialized(true); + }); } } catch (error) { console.error('예산 데이터 로드 중 오류:', error); } }; - // 초기 로드 - loadBudget(); - - // 이벤트 리스너 설정 - const handleBudgetUpdate = (e?: StorageEvent) => { - console.log('예산 데이터 업데이트 이벤트 감지:', e?.key); - if (!e || e.key === 'budgetData' || e.key === null) { - loadBudget(); - } - }; - - // 이벤트 발생 시 데이터 새로고침 - window.addEventListener('budgetDataUpdated', () => handleBudgetUpdate()); - window.addEventListener('storage', handleBudgetUpdate); - window.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible') { - console.log('페이지 보임: 예산 데이터 새로고침'); - loadBudget(); - } - }); - window.addEventListener('focus', () => { - console.log('창 포커스: 예산 데이터 새로고침'); + // 초기 로드 - 지연 시간 추가 + setTimeout(() => { loadBudget(); - }); + }, 100); // 지연된 초기 로드 + + // 필수 이벤트만 등록 + const handleBudgetUpdate = () => loadBudget(); - // 주기적 데이터 검사 (1초마다) - 다른 컴포넌트에서 변경된 사항 감지 - const intervalId = setInterval(() => { - const lastSaveTime = localStorage.getItem('lastBudgetSaveTime'); - const lastLoadTime = localStorage.getItem('lastBudgetDataLoadTime'); - - if (lastSaveTime && lastLoadTime && new Date(lastSaveTime) > new Date(lastLoadTime)) { - console.log('새로운 저장 감지됨, 데이터 다시 로드...'); - loadBudget(); - } - }, 1000); + // 필수 이벤트만 등록 + const budgetUpdateHandler = () => handleBudgetUpdate(); + window.addEventListener('budgetDataUpdated', budgetUpdateHandler); return () => { - window.removeEventListener('budgetDataUpdated', () => handleBudgetUpdate()); - window.removeEventListener('storage', handleBudgetUpdate); - window.removeEventListener('visibilitychange', () => {}); - window.removeEventListener('focus', () => loadBudget()); - clearInterval(intervalId); + window.removeEventListener('budgetDataUpdated', budgetUpdateHandler); }; }, [isInitialized, budgetData]); diff --git a/src/contexts/budget/hooks/useTransactionState.ts b/src/contexts/budget/hooks/useTransactionState.ts index c116382..b62dc95 100644 --- a/src/contexts/budget/hooks/useTransactionState.ts +++ b/src/contexts/budget/hooks/useTransactionState.ts @@ -16,39 +16,46 @@ export const useTransactionState = () => { // 초기 트랜잭션 로드 및 이벤트 리스너 설정 useEffect(() => { - const loadTransactions = () => { - console.log('트랜잭션 로드 시도 중...'); - const storedTransactions = loadTransactionsFromStorage(); - console.log('트랜잭션 로드됨:', storedTransactions.length, '개'); - setTransactions(storedTransactions); + // 트랜잭션 로드 함수 - 비동기 처리로 변경 + const loadTransactions = async () => { + try { + console.log('트랜잭션 로드 시도 중...'); + + // 비동기 작업을 마이크로태스크로 지연 + await new Promise(resolve => queueMicrotask(() => resolve())); + + const storedTransactions = loadTransactionsFromStorage(); + console.log('트랜잭션 로드됨:', storedTransactions.length, '개'); + + // 상태 업데이트를 마이크로태스크로 지연 + queueMicrotask(() => { + setTransactions(storedTransactions); + }); + } catch (error) { + console.error('트랜잭션 로드 오류:', error); + } }; - // 초기 로드 - loadTransactions(); + // 초기 로드 - 지연 시간 추가 + setTimeout(() => { + loadTransactions(); + }, 100); // 지연된 초기 로드 - // 이벤트 리스너 추가 + // 이벤트 리스너 추가 - 최소한으로 유지 const handleTransactionUpdate = (e?: StorageEvent) => { - console.log('트랜잭션 업데이트 이벤트 감지:', e?.key); if (!e || e.key === 'transactions' || e.key === null) { loadTransactions(); } }; - window.addEventListener('transactionUpdated', () => handleTransactionUpdate()); - window.addEventListener('transactionDeleted', () => handleTransactionUpdate()); - window.addEventListener('transactionAdded', () => handleTransactionUpdate()); + // 필수 이벤트만 등록 + const transactionUpdateHandler = () => handleTransactionUpdate(); + window.addEventListener('transactionUpdated', transactionUpdateHandler); window.addEventListener('storage', handleTransactionUpdate); - window.addEventListener('focus', () => { - console.log('창 포커스: 트랜잭션 새로고침'); - loadTransactions(); - }); return () => { - window.removeEventListener('transactionUpdated', () => handleTransactionUpdate()); - window.removeEventListener('transactionDeleted', () => handleTransactionUpdate()); - window.removeEventListener('transactionAdded', () => handleTransactionUpdate()); + window.removeEventListener('transactionUpdated', transactionUpdateHandler); window.removeEventListener('storage', handleTransactionUpdate); - window.removeEventListener('focus', () => loadTransactions()); }; }, []); @@ -74,7 +81,7 @@ export const useTransactionState = () => { }); }, []); - // 트랜잭션 삭제 함수 - 안정성 개선 + // 트랜잭션 삭제 함수 - 성능 최적화 및 안정성 개선 const deleteTransaction = useCallback((transactionId: string) => { // 이미 삭제 중이면 중복 삭제 방지 if (isDeleting) { @@ -82,57 +89,59 @@ export const useTransactionState = () => { return; } - console.log('트랜잭션 삭제 시작:', transactionId); - // 중복 삭제 방지 if (lastDeletedId === transactionId) { console.log('중복 삭제 요청 무시:', transactionId); return; } - setIsDeleting(true); - setLastDeletedId(transactionId); - - try { - setTransactions(prev => { - // 기존 트랜잭션 목록 백업 (문제 발생 시 복원용) - const originalTransactions = [...prev]; - - // 삭제할 항목 필터링 - const updated = prev.filter(transaction => transaction.id !== transactionId); - - // 항목이 실제로 삭제되었는지 확인 - if (updated.length === originalTransactions.length) { - console.log('삭제할 트랜잭션을 찾을 수 없음:', transactionId); - setIsDeleting(false); - return originalTransactions; + // 삭제 상태 설정 - 마이크로태스크로 지연 + queueMicrotask(() => { + setIsDeleting(true); + setLastDeletedId(transactionId); + + // 삭제 작업을 마이크로태스크로 진행하여 UI 차단 방지 + queueMicrotask(() => { + try { + setTransactions(prev => { + // 삭제할 항목 필터링 - 성능 최적화 + const updated = prev.filter(transaction => transaction.id !== transactionId); + + // 항목이 실제로 삭제되었는지 확인 + if (updated.length === prev.length) { + console.log('삭제할 트랜잭션을 찾을 수 없음:', transactionId); + return prev; // 변경 없음 + } + + // 저장소 업데이트를 마이크로태스크로 진행 + queueMicrotask(() => { + saveTransactionsToStorage(updated); + + // 토스트 메시지 표시 + toast({ + title: "지출이 삭제되었습니다", + description: "지출 항목이 성공적으로 삭제되었습니다.", + }); + }); + + return updated; + }); + } catch (error) { + console.error('트랜잭션 삭제 중 오류 발생:', error); + toast({ + title: "삭제 실패", + description: "지출 항목 삭제 중 오류가 발생했습니다.", + variant: "destructive" + }); + } finally { + // 삭제 상태 초기화 (500ms 후) - 시간 단축 + setTimeout(() => { + setIsDeleting(false); + setLastDeletedId(null); + }, 500); } - - // 저장소에 업데이트된 목록 저장 - saveTransactionsToStorage(updated); - - // 토스트 메시지 표시 - toast({ - title: "지출이 삭제되었습니다", - description: "지출 항목이 성공적으로 삭제되었습니다.", - }); - - return updated; }); - } catch (error) { - console.error('트랜잭션 삭제 중 오류 발생:', error); - toast({ - title: "삭제 실패", - description: "지출 항목 삭제 중 오류가 발생했습니다.", - variant: "destructive" - }); - } finally { - // 삭제 상태 초기화 (1초 후) - setTimeout(() => { - setIsDeleting(false); - setLastDeletedId(null); - }, 1000); - } + }); }, [lastDeletedId, isDeleting]); // 트랜잭션 초기화 함수 diff --git a/src/contexts/budget/useBudget.ts b/src/contexts/budget/useBudget.ts new file mode 100644 index 0000000..77affb4 --- /dev/null +++ b/src/contexts/budget/useBudget.ts @@ -0,0 +1,36 @@ +import { useContext, createContext } from 'react'; +import { BudgetData, BudgetPeriod, Transaction } from './types'; + +// 컨텍스트 인터페이스 정의 +export interface BudgetContextType { + transactions: Transaction[]; + selectedTab: BudgetPeriod; + setSelectedTab: (tab: BudgetPeriod) => void; + budgetData: BudgetData; + categoryBudgets: Record; + getCategorySpending: () => Array<{ + title: string; + current: number; + total: number; + }>; + addTransaction: (transaction: Transaction) => void; + updateTransaction: (transaction: Transaction) => void; + deleteTransaction: (id: string) => void; + handleBudgetGoalUpdate: (type: BudgetPeriod, amount: number, newCategoryBudgets?: Record) => void; + resetBudgetData?: () => void; // 선택적 필드로 추가 +} + +// 컨텍스트 생성 +export const BudgetContext = createContext(undefined); + +/** + * 예산 컨텍스트에 접근하기 위한 커스텀 훅 + * BudgetProvider 내부에서만 사용해야 함 + */ +export const useBudget = (): BudgetContextType => { + const context = useContext(BudgetContext); + if (context === undefined) { + throw new Error('useBudget는 BudgetProvider 내부에서 사용해야 합니다'); + } + return context; +}; diff --git a/src/hooks/useDataInitialization.ts b/src/hooks/useDataInitialization.ts index 5864e3d..a354a1a 100644 --- a/src/hooks/useDataInitialization.ts +++ b/src/hooks/useDataInitialization.ts @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { resetAllData } from '@/contexts/budget/storage'; import { resetAllStorageData } from '@/utils/storageUtils'; import { clearCloudData } from '@/utils/syncUtils'; -import { useAuth } from '@/contexts/auth/AuthProvider'; +import { useAuth } from '@/contexts/auth'; export const useDataInitialization = (resetBudgetData?: () => void) => { const [isInitialized, setIsInitialized] = useState(false); diff --git a/src/hooks/useDataReset.ts b/src/hooks/useDataReset.ts index 2fc60df..e2219c6 100644 --- a/src/hooks/useDataReset.ts +++ b/src/hooks/useDataReset.ts @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { useToast } from '@/hooks/useToast.wrapper'; import { resetAllStorageData } from '@/utils/storageUtils'; import { clearCloudData } from '@/utils/sync/clearCloudData'; -import { useAuth } from '@/contexts/auth/AuthProvider'; +import { useAuth } from '@/contexts/auth'; import { isSyncEnabled, setSyncEnabled } from '@/utils/sync/syncSettings'; export interface DataResetResult {