@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
407 lines (361 loc) • 9.65 kB
text/typescript
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import type {
CacheLevel,
CacheStats,
ContentType,
CacheStatus,
CacheMetrics,
} from "./types";
// 캐시 매니저 인스턴스 (실제 구현에서는 Context에서 제공)
interface CacheManagerInterface {
get<T>(
key: string,
contentType: ContentType,
options?: {
levels?: CacheLevel[];
fallback?: () => Promise<T>;
}
): Promise<{ data: T | null; status: CacheStatus; level?: CacheLevel }>;
set<T>(
key: string,
data: T,
contentType: ContentType,
options?: {
ttl?: number;
levels?: CacheLevel[];
tags?: string[];
}
): Promise<boolean>;
invalidate(
pattern: string | string[],
options?: {
levels?: CacheLevel[];
strategy?: "key" | "tag" | "pattern";
}
): Promise<number>;
getStats(): CacheStats;
healthCheck(): Promise<{
healthy: boolean;
levels: Map<CacheLevel, boolean>;
issues: string[];
}>;
}
// 글로벌 캐시 매니저 (실제로는 Context Provider에서 제공)
declare const cacheManager: CacheManagerInterface;
/**
* 캐시된 데이터를 조회하고 관리하는 훅
*/
export function useCache<T>(
key: string,
contentType: ContentType,
options?: {
levels?: CacheLevel[];
fallback?: () => Promise<T>;
enabled?: boolean;
refetchInterval?: number;
staleTime?: number;
}
) {
const [data, setData] = useState<T | null>(null);
const [status, setStatus] = useState<CacheStatus>("miss");
const [level, setLevel] = useState<CacheLevel | undefined>(undefined);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const fetchTimeRef = useRef<Date | null>(null);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const fetchData = useCallback(async () => {
if (!options?.enabled) return;
setLoading(true);
setError(null);
try {
const result = await cacheManager.get(key, contentType, {
levels: options?.levels,
fallback: options?.fallback,
});
setData(result.data);
setStatus(result.status);
setLevel(result.level);
setLastUpdated(new Date());
fetchTimeRef.current = new Date();
} catch (err) {
setError(err instanceof Error ? err.message : "캐시 조회 오류");
setStatus("error");
} finally {
setLoading(false);
}
}, [key, contentType, options?.levels, options?.fallback, options?.enabled]);
// Stale time 체크
const isStale = useCallback(() => {
if (!fetchTimeRef.current || !options?.staleTime) return false;
return Date.now() - fetchTimeRef.current.getTime() > options.staleTime;
}, [options?.staleTime]);
// 데이터 무효화
const invalidate = useCallback(async () => {
await cacheManager.invalidate(key, { levels: options?.levels });
await fetchData();
}, [key, options?.levels, fetchData]);
// 데이터 설정
const setCache = useCallback(
async (
newData: T,
cacheOptions?: {
ttl?: number;
tags?: string[];
}
) => {
await cacheManager.set(key, newData, contentType, {
levels: options?.levels,
...cacheOptions,
});
setData(newData);
setLastUpdated(new Date());
fetchTimeRef.current = new Date();
},
[key, contentType, options?.levels]
);
// 초기 로드 및 인터벌 설정
useEffect(() => {
if (options?.enabled !== false) {
fetchData();
}
// 자동 갱신 설정
if (options?.refetchInterval) {
intervalRef.current = setInterval(() => {
if (!loading && (isStale() || !data)) {
fetchData();
}
}, options.refetchInterval);
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [
fetchData,
options?.refetchInterval,
options?.enabled,
loading,
data,
isStale,
]);
return {
data,
status,
level,
loading,
error,
lastUpdated,
isStale: isStale(),
refetch: fetchData,
invalidate,
setCache,
};
}
/**
* 캐시 통계를 조회하는 훅
*/
export function useCacheStats(refreshInterval = 5000) {
const [stats, setStats] = useState<CacheStats | null>(null);
const [loading, setLoading] = useState(true);
const fetchStats = useCallback(() => {
try {
const currentStats = cacheManager.getStats();
setStats(currentStats);
} catch (error) {
console.error("캐시 통계 조회 오류:", error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchStats();
const interval = setInterval(fetchStats, refreshInterval);
return () => clearInterval(interval);
}, [fetchStats, refreshInterval]);
return {
stats,
loading,
refresh: fetchStats,
};
}
/**
* 캐시 헬스 체크 훅
*/
export function useCacheHealth(checkInterval = 30000) {
const [health, setHealth] = useState<{
healthy: boolean;
levels: Map<CacheLevel, boolean>;
issues: string[];
} | null>(null);
const [checking, setChecking] = useState(false);
const checkHealth = useCallback(async () => {
setChecking(true);
try {
const healthResult = await cacheManager.healthCheck();
setHealth(healthResult);
} catch (error) {
console.error("캐시 헬스 체크 오류:", error);
setHealth({
healthy: false,
levels: new Map(),
issues: ["헬스 체크 실행 실패"],
});
} finally {
setChecking(false);
}
}, []);
useEffect(() => {
checkHealth();
const interval = setInterval(checkHealth, checkInterval);
return () => clearInterval(interval);
}, [checkHealth, checkInterval]);
return {
health,
checking,
checkHealth,
};
}
/**
* 여러 키를 동시에 캐시에서 조회하는 훅
*/
export function useMultiCache<T>(
keys: Array<{
key: string;
contentType: ContentType;
fallback?: () => Promise<T>;
}>,
options?: {
levels?: CacheLevel[];
enabled?: boolean;
}
) {
const [results, setResults] = useState<
Map<
string,
{
data: T | null;
status: CacheStatus;
level?: CacheLevel;
}
>
>(new Map());
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<Map<string, string>>(new Map());
const fetchAll = useCallback(async () => {
if (!options?.enabled || keys.length === 0) return;
setLoading(true);
setErrors(new Map());
const newResults = new Map();
const newErrors = new Map();
await Promise.allSettled(
keys.map(async ({ key, contentType, fallback }) => {
try {
const result = await cacheManager.get(key, contentType, {
levels: options?.levels,
fallback,
});
newResults.set(key, result);
} catch (error) {
newErrors.set(
key,
error instanceof Error ? error.message : "오류 발생"
);
}
})
);
setResults(newResults);
setErrors(newErrors);
setLoading(false);
}, [keys, options?.levels, options?.enabled]);
const invalidateAll = useCallback(async () => {
const keyList = keys.map(({ key }) => key);
await cacheManager.invalidate(keyList, { levels: options?.levels });
await fetchAll();
}, [keys, options?.levels, fetchAll]);
useEffect(() => {
if (options?.enabled !== false) {
fetchAll();
}
}, [fetchAll, options?.enabled]);
return {
results,
loading,
errors,
refetchAll: fetchAll,
invalidateAll,
};
}
/**
* 캐시 무효화를 위한 훅
*/
export function useCacheInvalidation() {
const [invalidating, setInvalidating] = useState(false);
const invalidatePattern = useCallback(
async (
pattern: string | string[],
options?: {
levels?: CacheLevel[];
strategy?: "key" | "tag" | "pattern";
}
) => {
setInvalidating(true);
try {
const count = await cacheManager.invalidate(pattern, options);
return count;
} catch (error) {
console.error("캐시 무효화 오류:", error);
throw error;
} finally {
setInvalidating(false);
}
},
[]
);
const invalidateByTag = useCallback(
async (tags: string | string[], levels?: CacheLevel[]) => {
return invalidatePattern(Array.isArray(tags) ? tags : [tags], {
levels,
strategy: "tag",
});
},
[invalidatePattern]
);
const invalidateByKey = useCallback(
async (keys: string | string[], levels?: CacheLevel[]) => {
return invalidatePattern(keys, {
levels,
strategy: "key",
});
},
[invalidatePattern]
);
return {
invalidating,
invalidatePattern,
invalidateByTag,
invalidateByKey,
};
}
/**
* 실시간 캐시 메트릭 훅
*/
export function useCacheMetrics(updateInterval = 1000) {
const [metrics, setMetrics] = useState<CacheMetrics | null>(null);
useEffect(() => {
const updateMetrics = () => {
try {
const stats = cacheManager.getStats();
setMetrics(stats.overall);
} catch (error) {
console.error("메트릭 업데이트 오류:", error);
}
};
updateMetrics();
const interval = setInterval(updateMetrics, updateInterval);
return () => clearInterval(interval);
}, [updateInterval]);
return metrics;
}