React์์ API ์์ฒญ์ ๋ฐฐ์น๋ก ์ฒ๋ฆฌํ๊ณ ์ธ๋ถ store์ ๋๊ธฐํํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์
๋๋ค. useSyncExternalStore๋ฅผ ์ฌ์ฉํ์ฌ React 18+์ ์๋ฒฝํ๊ฒ ํธํ๋ฉ๋๋ค.
- ๐ ๋ฐฐ์น ์ฒ๋ฆฌ: ์ฌ๋ฌ ๊ฐ๋ณ ์์ฒญ์ ํ๋์ ๋ฐฐ์น ์์ฒญ์ผ๋ก ํฉ์ณ์ ์ฒ๋ฆฌ
- ๐ Store ํตํฉ: Zustand, Redux ๋ฑ ๋ค์ํ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํธํ
- โก ์ฑ๋ฅ ์ต์ ํ: ์ค๋ณต ์์ฒญ ์ ๊ฑฐ ๋ฐ ์ง๋ฅ์ ์ธ ์บ์ฑ
- ๐ฏ TypeScript ์ง์: ์์ ํ ํ์ ์์ ์ฑ
- ๐ช React Hooks: ๊ฐํธํ React ํตํฉ
- ๐ ๏ธ ์ปค์คํฐ๋ง์ด์ง: ๋ฒํผ ์๊ฐ, ๋ฐฐ์น ํฌ๊ธฐ ๋ฑ ์ค์ ๊ฐ๋ฅ
- ๐พ ์ํ ์์์ฑ: localStorage/sessionStorage๋ฅผ ํตํ ์ํ ์ ์ฅ
npm install react-batcher
# ๋๋
yarn add react-batcher
# ๋๋
pnpm add react-batcher- ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
- Export ๋ชฉ๋ก
- ํ์ ์ ์
- BatchManager ํด๋์ค
- React Hooks
- Store ์ด๋ํฐ
- ์ ํธ๋ฆฌํฐ ํจ์
- ๊ณ ๊ธ ์ฌ์ฉ๋ฒ
import { BatchableItem } from 'react-batcher';
// ๋ชจ๋ ์์ดํ
์ ๋ฐ๋์ id ํ๋๋ฅผ ๊ฐ์ ธ์ผ ํฉ๋๋ค
interface User extends BatchableItem {
id: string;
name: string;
email: string;
avatar?: string;
}import { create } from 'zustand';
import { BatchManager, createSimpleZustandAdapter } from 'react-batcher';
// Zustand store ์์ฑ
const useUserStore = create<Record<string, User>>(() => ({}));
// Store ์ด๋ํฐ ์์ฑ
const userStoreAdapter = createSimpleZustandAdapter({
getState: useUserStore.getState,
setState: useUserStore.setState,
subscribe: useUserStore.subscribe,
});// BatchFetcher ํ์
: (ids: string[]) => Promise<T[]>
async function fetchBatchUsers(userIds: string[]): Promise<User[]> {
const response = await fetch('/api/batch-users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userIds }),
});
const data = await response.json();
return data.users;
}export const userBatchManager = new BatchManager<User>({
store: userStoreAdapter,
fetcher: fetchBatchUsers,
bufferConfig: {
delayMs: 100, // 100ms ๋๊ธฐ ํ ๋ฐฐ์น ์ฒ๋ฆฌ
maxBatchSize: 50, // ์ต๋ 50๊ฐ์ฉ ๋ฐฐ์น ์ฒ๋ฆฌ
},
debug: true,
onError: (error, failedIds) => {
console.error('Failed to fetch users:', error, failedIds);
},
});import { useBatchedItem } from "react-batcher";
function UserProfile({ userId }: { userId: string }) {
const user = useBatchedItem(userBatchManager, userId);
if (!user) {
return <div>Loading user...</div>;
}
return (
<div>
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}// ๋ฉ์ธ ํด๋์ค
export { BatchManager } from './BatchManager';
// ํ์
์ ์
export type {
BatchableItem,
Store,
BatchFetcher,
BufferConfig,
BatchManagerConfig,
BatchManagerState,
} from './types';
// React ํ
export {
useBatchedItem,
useBatchedItems,
useAllBatchedItems,
usePreloadItems,
useFlushBatch,
useBatchManagerState,
useBatchedState,
useBatchStats,
} from './hooks';
// Store ์ด๋ํฐ
export {
createZustandAdapter,
createSimpleZustandAdapter,
} from './adapters/zustand';
export { createReduxAdapter } from './adapters/redux';
export { createMemoryStore } from './adapters/memory';
// ์ ํธ๋ฆฌํฐ
export { createBuffer, type BufferInstance } from './buffer';
export {
createPersistence,
loadState,
saveState,
clearPersistedState,
type PersistenceConfig,
} from './persistence';๋ฐฐ์น ์ฒ๋ฆฌํ ๋ฐ์ดํฐ์ ๊ธฐ๋ณธ ์ธํฐํ์ด์ค์
๋๋ค. ๋ชจ๋ ์์ดํ
์ ๋ฐ๋์ ๊ณ ์ ํ id๋ฅผ ๊ฐ์ ธ์ผ ํฉ๋๋ค.
interface BatchableItem {
id: string;
}
// ์ฌ์ฉ ์์
interface User extends BatchableItem {
id: string;
name: string;
email: string;
}
interface Product extends BatchableItem {
id: string;
title: string;
price: number;
}์ธ๋ถ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ํตํฉ์ ์ํ ์ถ์ํ ์ธํฐํ์ด์ค์ ๋๋ค.
interface Store<T extends BatchableItem> {
// ํ์ฌ store์ ์ํ๋ฅผ ๊ฐ์ ธ์ค๋ ํจ์
getState: () => Record<string, T>;
// store ์ํ ๋ณ๊ฒฝ์ ๊ตฌ๋
ํ๋ ํจ์
subscribe: (listener: () => void) => () => void;
// store ์ํ๋ฅผ ์
๋ฐ์ดํธํ๋ ํจ์
setState: (newState: Record<string, T>) => void;
}API์์ ๋ฐฐ์น๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ํจ์ ํ์ ์ ๋๋ค.
type BatchFetcher<T extends BatchableItem> = (ids: string[]) => Promise<T[]>;
// ์ฌ์ฉ ์์
const fetchUsers: BatchFetcher<User> = async ids => {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ ids }),
});
return response.json();
};๋ฒํผ ์ค์ ์ต์ ์ ๋๋ค.
interface BufferConfig {
// ๋ฐฐ์น ์ฒ๋ฆฌ๋ฅผ ์ํ ๋๊ธฐ ์๊ฐ (๋ฐ๋ฆฌ์ด)
// @default 100
delayMs?: number;
// ํ ๋ฒ์ ์ฒ๋ฆฌํ ์ต๋ ์์ดํ
์
// @default undefined (์ ํ ์์)
maxBatchSize?: number;
}
// ์ฌ์ฉ ์์
const config: BufferConfig = {
delayMs: 50, // 50ms ๋๊ธฐ
maxBatchSize: 100, // ์ต๋ 100๊ฐ์ฉ ์ฒ๋ฆฌ
};์ฌ์๋ ์ค์ ์ต์ ์ ๋๋ค.
interface RetryConfig {
// ์ต๋ ์ฌ์๋ ํ์ @default 3
maxRetries?: number;
// ์ฌ์๋ ์ง์ฐ ์๊ฐ (๋ฐ๋ฆฌ์ด) @default 1000
retryDelay?: number;
// ๋ฐฑ์คํ ์ ๋ต @default 'exponential'
// - linear: ๊ณ ์ ์ง์ฐ
// - exponential: ์ง์์ ์ฆ๊ฐ
backoff?: 'linear' | 'exponential';
// ์ฌ์๋ ๊ฐ๋ฅํ ์๋ฌ์ธ์ง ํ๋จํ๋ ํจ์
shouldRetry?: (error: Error, attempt: number) => boolean;
}BatchManager ์์ฑ ์ ์ฌ์ฉํ๋ ์ค์ ์ต์ ์ ๋๋ค.
interface BatchManagerConfig<T extends BatchableItem> {
// [ํ์] ์ธ๋ถ store ์ธ์คํด์ค
store: Store<T>;
// [ํ์] ๋ฐฐ์น๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ํจ์
fetcher: BatchFetcher<T>;
// [์ ํ] ๋ฒํผ ์ค์
bufferConfig?: BufferConfig;
// [์ ํ] ์๋ฌ ๋ฐ์ ์ ํธ์ถ๋๋ ์ฝ๋ฐฑ
onError?: (error: Error, failedIds: string[]) => void;
// [์ ํ] ๋๋ฒ๊ทธ ๋ชจ๋ ํ์ฑํ
debug?: boolean;
// [์ ํ] ์ด๊ธฐ ์ํ
initialState?: Partial<BatchManagerState>;
// [์ ํ] ์ฌ์๋ ์ค์
retryConfig?: RetryConfig;
// [์ ํ] ์ํ ์ ์ฅ ํค (localStorage/sessionStorage์ ์ ์ฅ)
persistenceKey?: string;
}BatchManager์ ํ์ฌ ์ํ๋ฅผ ๋ํ๋ด๋ ์ธํฐํ์ด์ค์ ๋๋ค.
interface BatchManagerState {
// ํ์ฌ ์ํ
// - idle: ๋๊ธฐ ์ค
// - pending: ๋ฒํผ์ ์์ดํ
์ด ์์ด๋ ์ค
// - processing: API ์์ฒญ ์คํ ์ค
status: 'idle' | 'pending' | 'processing';
// ๋ฐฐ์น ์คํ ํ์
executionCount: number;
// ์ด ์ฒ๋ฆฌ๋ ์์ดํ
์
totalItemsProcessed: number;
// ๋ฒํผ๊ฐ ๋น์ด์๋์ง ์ฌ๋ถ
isEmpty: boolean;
// ๋๊ธฐ ์ค์ธ ์์ดํ
์ด ์๋์ง ์ฌ๋ถ
isPending: boolean;
// ํ์ฌ ๋ฒํผ ํฌ๊ธฐ
bufferSize: number;
// ๋๊ธฐ ์ค์ธ ์์ฒญ ๊ฐ์
pendingRequestCount: number;
// ๋ง์ง๋ง ๋ฐฐ์น ์คํ ์๊ฐ (timestamp)
lastExecutionTime: number | null;
// ๋ง์ง๋ง ์๋ฌ
lastError: Error | null;
}BatchManager๋ API ์์ฒญ์ ๋ฐฐ์น๋ก ๋ชจ์์ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ๊ณ , ์ธ๋ถ store์ ๋๊ธฐํํ๋ ํต์ฌ ํด๋์ค์ ๋๋ค.
const batchManager = new BatchManager<User>({
store: userStoreAdapter,
fetcher: fetchBatchUsers,
bufferConfig: {
delayMs: 100,
maxBatchSize: 50,
},
debug: true,
onError: (error, failedIds) => {
console.error('Fetch failed:', error, failedIds);
},
persistenceKey: 'user-batch-state', // localStorage์ ์ํ ์ ์ฅ
});์์ดํ ์ ์์ฒญํฉ๋๋ค. store์ ์์ผ๋ฉด ์ฆ์ ๋ฐํํ๊ณ , ์์ผ๋ฉด ๋ฐฐ์น ์์ฒญ์ ์ถ๊ฐ ํ null์ ๋ฐํํฉ๋๋ค.
const user = batchManager.requestItem('user-123');
if (user) {
console.log('User found:', user.name);
} else {
console.log('User is being fetched...');
}store์์ ์์ดํ ์ ๊ฐ์ ธ์ต๋๋ค. ์์ฒญ์ ํ์ง ์์ต๋๋ค.
// ์ด๋ฏธ ๋ก๋๋ ์์ดํ
๋ง ๊ฐ์ ธ์ค๊ธฐ (API ํธ์ถ ์์)
const user = batchManager.getItem('user-123');์ฌ๋ฌ ์์ดํ ์ ๋ฏธ๋ฆฌ ๋ก๋ํฉ๋๋ค. ํ๋ฉด์ ํ์ํ๊ธฐ ์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ์ ธ์ฌ ๋ ์ ์ฉํฉ๋๋ค.
// ํ์ด์ง ์ง์
์ ํ์ํ ์ฌ์ฉ์๋ค ๋ฏธ๋ฆฌ ๋ก๋
batchManager.preloadItems(['user-1', 'user-2', 'user-3']);๋๊ธฐ ์ค์ธ ๋ชจ๋ ๋ฐฐ์น ์์ฒญ์ ์ฆ์ ์ฒ๋ฆฌํฉ๋๋ค. ํ์์์์ ๊ธฐ๋ค๋ฆฌ์ง ์์ต๋๋ค.
// ์ฌ๋ฌ ์์ดํ
์์ฒญ ํ ์ฆ์ ์ฒ๋ฆฌ
batchManager.requestItem('user-1');
batchManager.requestItem('user-2');
batchManager.requestItem('user-3');
await batchManager.flush(); // 100ms ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ ์ฆ์ API ํธ์ถ
// ํ์ด์ง ๋ ๋๊ธฐ ์ ์ ๋๊ธฐ ์ค์ธ ์์ฒญ ๋ชจ๋ ์ฒ๋ฆฌ
window.addEventListener('beforeunload', () => {
batchManager.flush();
});store์ ๋ชจ๋ ์์ดํ ์ ๊ฐ์ ธ์ต๋๋ค.
const allUsers = batchManager.getAllItems();
Object.values(allUsers).forEach(user => {
console.log(user.name);
});ํน์ ์์ดํ ์ store์์ ์ ๊ฑฐํฉ๋๋ค.
// ์ฌ์ฉ์ ์ญ์ ํ store์์๋ ์ ๊ฑฐ
await deleteUser('user-123');
batchManager.removeItem('user-123');store์ ๋ชจ๋ ์์ดํ ์ ์ ๊ฑฐํฉ๋๋ค.
// ๋ก๊ทธ์์ ์ ๋ชจ๋ ์บ์ ํด๋ฆฌ์ด
function handleLogout() {
batchManager.clearAll();
}ํ์ฌ ๋๊ธฐ ์ค์ธ ์์ฒญ ๊ฐ์๋ฅผ ๋ฐํํฉ๋๋ค. (๋๋ฒ๊น ์ฉ)
console.log(`Pending requests: ${batchManager.getPendingCount()}`);ํ์ฌ ๋ฒํผ์ ๋๊ธฐ ์ค์ธ ์์ดํ ์๋ฅผ ๋ฐํํฉ๋๋ค.
console.log(`Buffer size: ${batchManager.getBufferSize()}`);๋ฒํผ๊ฐ ๋น์ด์๋์ง ํ์ธํฉ๋๋ค.
if (batchManager.isBufferEmpty()) {
console.log('No pending items');
}BatchManager์ ํ์ฌ ์ํ๋ฅผ ๋ฐํํฉ๋๋ค.
const state = batchManager.getState();
console.log(`Status: ${state.status}`);
console.log(`Execution count: ${state.executionCount}`);
console.log(`Total processed: ${state.totalItemsProcessed}`);ํต๊ณ ์ ๋ณด๋ฅผ ๋ฐํํฉ๋๋ค.
const stats = batchManager.getStats();
console.log(`Execution count: ${stats.executionCount}`);
console.log(`Total items processed: ${stats.totalItemsProcessed}`);
console.log(`Average items per batch: ${stats.averageItemsPerBatch}`);
console.log(`Last execution: ${new Date(stats.lastExecutionTime)}`);
console.log(`Current buffer size: ${stats.currentBufferSize}`);
console.log(`Pending requests: ${stats.pendingRequestCount}`);React์ useSyncExternalStore์ ํจ๊ป ์ฌ์ฉํ๊ธฐ ์ํ ๋ฉ์๋๋ค์
๋๋ค. ์ผ๋ฐ์ ์ผ๋ก ์ง์ ์ฌ์ฉํ์ง ์๊ณ ์ ๊ณต๋๋ hooks๋ฅผ ์ฌ์ฉํฉ๋๋ค.
// ๋ด๋ถ์ ์ผ๋ก hooks์์ ์ฌ์ฉ๋จ
const snapshot = useSyncExternalStore(
batchManager.subscribe,
batchManager.getSnapshot
);BatchManager ์ํ ๋ณ๊ฒฝ์ ๊ตฌ๋ ํ๊ธฐ ์ํ ๋ฉ์๋๋ค์ ๋๋ค.
// ์ํ ๋ณ๊ฒฝ ๊ตฌ๋
const unsubscribe = batchManager.subscribeState(() => {
console.log('State changed:', batchManager.getStateSnapshot());
});
// ๊ตฌ๋
ํด์
unsubscribe();๋จ์ผ ์์ดํ ์ ๊ฐ์ ธ์ค๋ ํ ์ ๋๋ค. ์์ดํ ์ด ์์ผ๋ฉด ์๋์ผ๋ก ๋ฐฐ์น ์์ฒญ์ ์ถ๊ฐ๋ฉ๋๋ค.
function useBatchedItem<T extends BatchableItem>(
manager: BatchManager<T>,
id: string
): T | null;
// ์ฌ์ฉ ์์
function UserCard({ userId }: { userId: string }) {
const user = useBatchedItem(userBatchManager, userId);
if (!user) {
return <Skeleton />;
}
return (
<div className="user-card">
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
</div>
);
}์ฌ๋ฌ ์์ดํ ์ ํ ๋ฒ์ ๊ฐ์ ธ์ค๋ ํ ์ ๋๋ค.
function useBatchedItems<T extends BatchableItem>(
manager: BatchManager<T>,
ids: string[]
): (T | null)[];
// ์ฌ์ฉ ์์
function UserList({ userIds }: { userIds: string[] }) {
const users = useBatchedItems(userBatchManager, userIds);
return (
<div className="user-list">
{users.map((user, index) => (
<div key={userIds[index]}>
{user ? user.name : 'Loading...'}
</div>
))}
</div>
);
}store์ ๋ชจ๋ ์์ดํ ์ ๊ฐ์ ธ์ค๋ ํ ์ ๋๋ค.
function useAllBatchedItems<T extends BatchableItem>(
manager: BatchManager<T>
): Record<string, T>;
// ์ฌ์ฉ ์์
function AllUsers() {
const allUsers = useAllBatchedItems(userBatchManager);
return (
<ul>
{Object.values(allUsers).map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}์์ดํ
๋ค์ ๋ฏธ๋ฆฌ ๋ก๋ํ๋ ํ
์
๋๋ค. ids๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์๋์ผ๋ก preload๋ฉ๋๋ค.
function usePreloadItems<T extends BatchableItem>(
manager: BatchManager<T>,
ids: string[]
): void;
// ์ฌ์ฉ ์์
function UserDashboard({ friendIds }: { friendIds: string[] }) {
// ์น๊ตฌ ๋ชฉ๋ก์ ๋ฏธ๋ฆฌ ๋ก๋
usePreloadItems(userBatchManager, friendIds);
return (
<div>
<h1>Dashboard</h1>
{/* ์ดํ UserCard์์ useBatchedItem ์ฌ์ฉ ์ ์ด๋ฏธ ๋ก๋๋จ */}
{friendIds.map(id => (
<UserCard key={id} userId={id} />
))}
</div>
);
}๋ฐฐ์น๋ฅผ ์๋์ผ๋ก flushํ๋ ํจ์๋ฅผ ๋ฐํํ๋ ํ ์ ๋๋ค.
function useFlushBatch<T extends BatchableItem>(
manager: BatchManager<T>
): () => Promise<void>;
// ์ฌ์ฉ ์์
function SubmitForm() {
const flushBatch = useFlushBatch(userBatchManager);
const handleSubmit = async () => {
// ํผ ์ ์ถ ์ ์ ๋๊ธฐ ์ค์ธ ๋ชจ๋ ์์ฒญ ์ฒ๋ฆฌ
await flushBatch();
submitForm();
};
return <button onClick={handleSubmit}>Submit</button>;
}BatchManager์ ์ ์ฒด ์ํ๋ฅผ ๊ตฌ๋ ํ๋ ํ ์ ๋๋ค.
function useBatchManagerState<T extends BatchableItem>(
manager: BatchManager<T>
): BatchManagerState;
// ์ฌ์ฉ ์์
function BatchStatus() {
const state = useBatchManagerState(userBatchManager);
return (
<div className="batch-status">
<p>Status: {state.status}</p>
<p>Pending: {state.isPending ? 'Yes' : 'No'}</p>
<p>Buffer Size: {state.bufferSize}</p>
<p>Executions: {state.executionCount}</p>
<p>Total Processed: {state.totalItemsProcessed}</p>
{state.lastError && (
<p className="error">Error: {state.lastError.message}</p>
)}
</div>
);
}BatchManager์ ํน์ ์ํ๋ง ์ ํ์ ์ผ๋ก ๊ตฌ๋ ํ๋ ํ ์ ๋๋ค. ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง์ ๋ฐฉ์งํฉ๋๋ค.
function useBatchedState<T extends BatchableItem, R>(
manager: BatchManager<T>,
selector: (state: BatchManagerState) => R
): R;
// ์ฌ์ฉ ์์
function LoadingIndicator() {
// status๋ง ๊ตฌ๋
ํ์ฌ ๋ค๋ฅธ ์ํ ๋ณ๊ฒฝ ์ ๋ฆฌ๋ ๋๋ง ๋ฐฉ์ง
const status = useBatchedState(
userBatchManager,
state => state.status
);
if (status === 'processing') {
return <Spinner />;
}
return null;
}
function PendingBadge() {
// isPending๋ง ๊ตฌ๋
const isPending = useBatchedState(
userBatchManager,
state => state.isPending
);
return isPending ? <Badge>Loading...</Badge> : null;
}
// ์ฌ๋ฌ ํ๋๋ฅผ ํ๋์ ๊ฐ์ฒด๋ก ์ ํ
function BatchInfo() {
const info = useBatchedState(userBatchManager, state => ({
count: state.executionCount,
total: state.totalItemsProcessed
}));
return <p>Batches: {info.count}, Items: {info.total}</p>;
}BatchManager์ ํต๊ณ ์ ๋ณด๋ฅผ ๊ตฌ๋ ํ๋ ํ ์ ๋๋ค.
function useBatchStats<T extends BatchableItem>(manager: BatchManager<T>): {
executionCount: number;
totalItemsProcessed: number;
averageItemsPerBatch: number;
lastExecutionTime: number | null;
};
// ์ฌ์ฉ ์์
function StatsDisplay() {
const stats = useBatchStats(userBatchManager);
return (
<div className="stats">
<p>Total batches: {stats.executionCount}</p>
<p>Total items: {stats.totalItemsProcessed}</p>
<p>Avg per batch: {stats.averageItemsPerBatch.toFixed(1)}</p>
{stats.lastExecutionTime && (
<p>Last run: {new Date(stats.lastExecutionTime).toLocaleString()}</p>
)}
</div>
);
}์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ด ์ฌ์ฉํ ์ ์๋ ๊ฐ๋จํ ์ธ๋ฉ๋ชจ๋ฆฌ store์ ๋๋ค.
function createMemoryStore<T extends BatchableItem>(): Store<T>;
// ์ฌ์ฉ ์์
import { createMemoryStore, BatchManager } from 'react-batcher';
const store = createMemoryStore<User>();
const batchManager = new BatchManager<User>({
store,
fetcher: fetchBatchUsers,
});๋จ์ํ Zustand store๋ฅผ ์ํ ์ด๋ํฐ์
๋๋ค. Store๊ฐ Record<string, T> ํํ์ผ ๋ ์ฌ์ฉํฉ๋๋ค.
function createSimpleZustandAdapter<T extends BatchableItem>(zustandStore: {
getState: () => Record<string, T>;
setState: (state: Record<string, T>) => void;
subscribe: (listener: () => void) => () => void;
}): Store<T>;
// ์ฌ์ฉ ์์
import { create } from 'zustand';
import { createSimpleZustandAdapter } from 'react-batcher';
// ๋จ์ ํํ์ Zustand store
const useUserStore = create<Record<string, User>>(() => ({}));
const userStoreAdapter = createSimpleZustandAdapter({
getState: useUserStore.getState,
setState: useUserStore.setState,
subscribe: useUserStore.subscribe,
});items ํ๋๋ก ๋ํ๋ Zustand store๋ฅผ ์ํ ์ด๋ํฐ์
๋๋ค.
function createZustandAdapter<T extends BatchableItem>(zustandStore: {
getState: () => { items: Record<string, T> };
setState: (partial: { items: Record<string, T> }) => void;
subscribe: (listener: () => void) => () => void;
}): Store<T>;
// ์ฌ์ฉ ์์
interface UserStore {
items: Record<string, User>;
// ๋ค๋ฅธ ์ํ๋ค...
filter: string;
}
const useUserStore = create<UserStore>(() => ({
items: {},
filter: '',
}));
const userStoreAdapter = createZustandAdapter({
getState: useUserStore.getState,
setState: useUserStore.setState,
subscribe: useUserStore.subscribe,
});Redux store๋ฅผ ์ํ ์ด๋ํฐ์ ๋๋ค.
function createReduxAdapter<T extends BatchableItem>(
reduxStore: {
getState: () => any;
dispatch: (action: any) => void;
subscribe: (listener: () => void) => () => void;
},
selector: (state: any) => Record<string, T>,
updateAction: (items: Record<string, T>) => any
): Store<T>;
// ์ฌ์ฉ ์์
import { createReduxAdapter } from 'react-batcher';
import { store } from './redux/store';
const userStoreAdapter = createReduxAdapter(
store,
// selector: Redux state์์ users ์ถ์ถ
state => state.users.items,
// action creator: ์
๋ฐ์ดํธ ์ก์
์์ฑ
users => ({ type: 'users/setAll', payload: users })
);
// Redux Toolkit ์ฌ์ฉ ์
import { setUsers } from './redux/usersSlice';
const userStoreAdapter = createReduxAdapter(
store,
state => state.users,
users => setUsers(users)
);์ฌ๋ฌ ํธ์ถ์ ๋ฐฐ์น๋ก ๋ชจ์์ ์ฒ๋ฆฌํ๋ ๋ฒํผ ํจ์๋ฅผ ์์ฑํฉ๋๋ค. BatchManager ๋ด๋ถ์์ ์ฌ์ฉ๋์ง๋ง, ์ง์ ์ฌ์ฉํ ์๋ ์์ต๋๋ค.
interface BufferInstance<T> {
add: (item: T) => void; // ๋ฒํผ์ ์์ดํ
์ถ๊ฐ
flush: () => Promise<void>; // ์ฆ์ ์ฒ๋ฆฌ
size: () => number; // ๋ฒํผ ํฌ๊ธฐ
isEmpty: () => boolean; // ๋น์ด์๋์ง ํ์ธ
}
function createBuffer<T, R = void>(options: {
ms: number; // ๋๊ธฐ ์๊ฐ
subscribedFn: (items: T[]) => Promise<R>; // ๋ฐฐ์น ์ฒ๋ฆฌ ํจ์
maxBatchSize?: number; // ์ต๋ ๋ฐฐ์น ํฌ๊ธฐ
}): BufferInstance<T>;
// ์ฌ์ฉ ์์: ๋ก๊ทธ ๋ฐฐ์น ์ ์ก
const logBuffer = createBuffer<LogEntry>({
ms: 5000, // 5์ด๋ง๋ค
maxBatchSize: 100, // ๋๋ 100๊ฐ ๋ชจ์ด๋ฉด
subscribedFn: async logs => {
await fetch('/api/logs', {
method: 'POST',
body: JSON.stringify(logs),
});
},
});
// ๋ก๊ทธ ์ถ๊ฐ
logBuffer.add({ level: 'info', message: 'User clicked button' });
logBuffer.add({ level: 'error', message: 'API failed' });
// ํ์ด์ง ์ข
๋ฃ ์ ์ฆ์ ์ ์ก
window.addEventListener('beforeunload', () => logBuffer.flush());์ํ๋ฅผ localStorage/sessionStorage์ ์ ์ฅํ๊ณ ๋ถ๋ฌ์ค๋ ์ ํธ๋ฆฌํฐ์ ๋๋ค.
interface PersistenceConfig {
key: string; // Storage key
storage?: 'localStorage' | 'sessionStorage'; // @default 'localStorage'
fields?: Array<keyof BatchManagerState>; // ์ ์ฅํ ํ๋๋ค
serialize?: (state: Partial<BatchManagerState>) => string;
deserialize?: (data: string) => Partial<BatchManagerState>;
}
// ์ํ ๋ถ๋ฌ์ค๊ธฐ
function loadState(
config: PersistenceConfig
): Partial<BatchManagerState> | null;
// ์ํ ์ ์ฅ
function saveState(state: BatchManagerState, config: PersistenceConfig): void;
// ์ ์ฅ๋ ์ํ ์ญ์
function clearPersistedState(config: PersistenceConfig): void;
// ํฌํผ ํจ์๋ค ์์ฑ
function createPersistence(config: PersistenceConfig): {
load: () => Partial<BatchManagerState> | null;
save: (state: BatchManagerState) => void;
clear: () => void;
};// ์ฌ์ฉ ์์: ์ง์ persistence ์ฌ์ฉ
import {
createPersistence,
loadState,
saveState,
clearPersistedState,
} from 'react-batcher';
// ๋ฐฉ๋ฒ 1: createPersistence ์ฌ์ฉ
const persistence = createPersistence({
key: 'my-batch-state',
storage: 'localStorage',
fields: ['executionCount', 'totalItemsProcessed'],
});
const savedState = persistence.load();
// ... ์์
ํ
persistence.save(currentState);
persistence.clear();
// ๋ฐฉ๋ฒ 2: ๊ฐ๋ณ ํจ์ ์ฌ์ฉ
const config = { key: 'my-batch-state' };
const state = loadState(config);
saveState(newState, config);
clearPersistedState(config);
// BatchManager์์ ์๋์ผ๋ก ์ฌ์ฉ
const batchManager = new BatchManager<User>({
store,
fetcher,
persistenceKey: 'user-batch-state', // ์๋์ผ๋ก ์ํ ์ ์ฅ/๋ณต์
});// users
const userBatchManager = new BatchManager<User>({
store: createMemoryStore<User>(),
fetcher: fetchBatchUsers,
});
// products
const productBatchManager = new BatchManager<Product>({
store: createMemoryStore<Product>(),
fetcher: fetchBatchProducts,
});
// ์ปดํฌ๋ํธ์์
function ProductWithSeller({ productId }: { productId: string }) {
const product = useBatchedItem(productBatchManager, productId);
const seller = useBatchedItem(userBatchManager, product?.sellerId ?? '');
if (!product) return <Loading />;
return (
<div>
<h2>{product.title}</h2>
<p>Sold by: {seller?.name ?? 'Loading...'}</p>
</div>
);
}const batchManager = new BatchManager<User>({
store,
fetcher,
onError: (error, failedIds) => {
// ์๋ฌ ๋ก๊น
console.error('Batch fetch failed:', error);
// ์ฌ์ฉ์์๊ฒ ์๋ฆผ
toast.error(`Failed to load ${failedIds.length} users`);
// ์๋ฌ ์ถ์ ์๋น์ค์ ์ ์ก
Sentry.captureException(error, {
extra: { failedIds },
});
},
});
// ์ปดํฌ๋ํธ์์ ์๋ฌ ์ํ ํ์
function UserWithError({ userId }: { userId: string }) {
const user = useBatchedItem(batchManager, userId);
const { lastError } = useBatchManagerState(batchManager);
if (lastError) {
return <ErrorMessage error={lastError} />;
}
if (!user) {
return <Skeleton />;
}
return <UserCard user={user} />;
}function ConditionalUser({ userId, shouldLoad }: {
userId: string;
shouldLoad: boolean;
}) {
// shouldLoad๊ฐ false๋ฉด ๋น ID๋ก ์์ฒญํ์ง ์์
const user = useBatchedItem(
userBatchManager,
shouldLoad ? userId : ''
);
if (!shouldLoad) {
return <p>Click to load user</p>;
}
return user ? <UserCard user={user} /> : <Loading />;
}// ํน์ ์ฌ์ฉ์ ์ ๋ณด ๊ฐฑ์
async function refreshUser(userId: string) {
// ๊ธฐ์กด ์บ์ ์ ๊ฑฐ
userBatchManager.removeItem(userId);
// ๋ค์ ์์ฒญ
userBatchManager.requestItem(userId);
await userBatchManager.flush();
}
// ์ ์ฒด ์บ์ ํด๋ฆฌ์ด
function clearAllCache() {
userBatchManager.clearAll();
}๊ธฐ์กด ์ฝ๋์์ ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ๋ง์ด๊ทธ๋ ์ด์ ํ๋ ๋ฐฉ๋ฒ:
const user = userStoreManager.requestUser(userId);
userStoreManager.preloadUsers(userIds);
const pendingCount = userStoreManager.getPendingCount();const user = useBatchedItem(userBatchManager, userId);
userBatchManager.preloadItems(userIds);
const pendingCount = userBatchManager.getPendingCount();MIT
์ด์๋ PR์ ํ์ํฉ๋๋ค!