Skip to content

Repository files navigation

React Batcher

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

๋ชฉ์ฐจ

  1. ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•
  2. Export ๋ชฉ๋ก
  3. ํƒ€์ž… ์ •์˜
  4. BatchManager ํด๋ž˜์Šค
  5. React Hooks
  6. Store ์–ด๋Œ‘ํ„ฐ
  7. ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
  8. ๊ณ ๊ธ‰ ์‚ฌ์šฉ๋ฒ•

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

1. ํƒ€์ž… ์ •์˜

import { BatchableItem } from 'react-batcher';

// ๋ชจ๋“  ์•„์ดํ…œ์€ ๋ฐ˜๋“œ์‹œ id ํ•„๋“œ๋ฅผ ๊ฐ€์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค
interface User extends BatchableItem {
  id: string;
  name: string;
  email: string;
  avatar?: string;
}

2. Store ์„ค์ • (Zustand ์˜ˆ์ œ)

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,
});

3. API ํ•จ์ˆ˜ ์ •์˜

// 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;
}

4. BatchManager ์ƒ์„ฑ

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);
  },
});

5. React ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ

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 ๋ชฉ๋ก

// ๋ฉ”์ธ ํด๋ž˜์Šค
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';

ํƒ€์ž… ์ •์˜

BatchableItem

๋ฐฐ์น˜ ์ฒ˜๋ฆฌํ•  ๋ฐ์ดํ„ฐ์˜ ๊ธฐ๋ณธ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  ์•„์ดํ…œ์€ ๋ฐ˜๋“œ์‹œ ๊ณ ์œ ํ•œ 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;
}

Store

์™ธ๋ถ€ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€์˜ ํ†ตํ•ฉ์„ ์œ„ํ•œ ์ถ”์ƒํ™” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค.

interface Store<T extends BatchableItem> {
  // ํ˜„์žฌ store์˜ ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜
  getState: () => Record<string, T>;

  // store ์ƒํƒœ ๋ณ€๊ฒฝ์„ ๊ตฌ๋…ํ•˜๋Š” ํ•จ์ˆ˜
  subscribe: (listener: () => void) => () => void;

  // store ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ํ•จ์ˆ˜
  setState: (newState: Record<string, T>) => void;
}

BatchFetcher

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();
};

BufferConfig

๋ฒ„ํผ ์„ค์ • ์˜ต์…˜์ž…๋‹ˆ๋‹ค.

interface BufferConfig {
  // ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๋Œ€๊ธฐ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ)
  // @default 100
  delayMs?: number;

  // ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•  ์ตœ๋Œ€ ์•„์ดํ…œ ์ˆ˜
  // @default undefined (์ œํ•œ ์—†์Œ)
  maxBatchSize?: number;
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
const config: BufferConfig = {
  delayMs: 50, // 50ms ๋Œ€๊ธฐ
  maxBatchSize: 100, // ์ตœ๋Œ€ 100๊ฐœ์”ฉ ์ฒ˜๋ฆฌ
};

RetryConfig

์žฌ์‹œ๋„ ์„ค์ • ์˜ต์…˜์ž…๋‹ˆ๋‹ค.

interface RetryConfig {
  // ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ @default 3
  maxRetries?: number;

  // ์žฌ์‹œ๋„ ์ง€์—ฐ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) @default 1000
  retryDelay?: number;

  // ๋ฐฑ์˜คํ”„ ์ „๋žต @default 'exponential'
  // - linear: ๊ณ ์ • ์ง€์—ฐ
  // - exponential: ์ง€์ˆ˜์  ์ฆ๊ฐ€
  backoff?: 'linear' | 'exponential';

  // ์žฌ์‹œ๋„ ๊ฐ€๋Šฅํ•œ ์—๋Ÿฌ์ธ์ง€ ํŒ๋‹จํ•˜๋Š” ํ•จ์ˆ˜
  shouldRetry?: (error: Error, attempt: number) => boolean;
}

BatchManagerConfig

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;
}

BatchManagerState

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 ํด๋ž˜์Šค

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์— ์ƒํƒœ ์ €์žฅ
});

๋ฉ”์„œ๋“œ

requestItem(id: string): T | null

์•„์ดํ…œ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. store์— ์žˆ์œผ๋ฉด ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•˜๊ณ , ์—†์œผ๋ฉด ๋ฐฐ์น˜ ์š”์ฒญ์— ์ถ”๊ฐ€ ํ›„ null์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

const user = batchManager.requestItem('user-123');
if (user) {
  console.log('User found:', user.name);
} else {
  console.log('User is being fetched...');
}

getItem(id: string): T | null

store์—์„œ ์•„์ดํ…œ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ์š”์ฒญ์€ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

// ์ด๋ฏธ ๋กœ๋“œ๋œ ์•„์ดํ…œ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ (API ํ˜ธ์ถœ ์—†์Œ)
const user = batchManager.getItem('user-123');

preloadItems(ids: string[]): void

์—ฌ๋Ÿฌ ์•„์ดํ…œ์„ ๋ฏธ๋ฆฌ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. ํ™”๋ฉด์— ํ‘œ์‹œํ•˜๊ธฐ ์ „์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์˜ฌ ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

// ํŽ˜์ด์ง€ ์ง„์ž… ์‹œ ํ•„์š”ํ•œ ์‚ฌ์šฉ์ž๋“ค ๋ฏธ๋ฆฌ ๋กœ๋“œ
batchManager.preloadItems(['user-1', 'user-2', 'user-3']);

flush(): Promise

๋Œ€๊ธฐ ์ค‘์ธ ๋ชจ๋“  ๋ฐฐ์น˜ ์š”์ฒญ์„ ์ฆ‰์‹œ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž„์•„์›ƒ์„ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

// ์—ฌ๋Ÿฌ ์•„์ดํ…œ ์š”์ฒญ ํ›„ ์ฆ‰์‹œ ์ฒ˜๋ฆฌ
batchManager.requestItem('user-1');
batchManager.requestItem('user-2');
batchManager.requestItem('user-3');
await batchManager.flush(); // 100ms ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ์ฆ‰์‹œ API ํ˜ธ์ถœ

// ํŽ˜์ด์ง€ ๋– ๋‚˜๊ธฐ ์ „์— ๋Œ€๊ธฐ ์ค‘์ธ ์š”์ฒญ ๋ชจ๋‘ ์ฒ˜๋ฆฌ
window.addEventListener('beforeunload', () => {
  batchManager.flush();
});

getAllItems(): Record<string, T>

store์˜ ๋ชจ๋“  ์•„์ดํ…œ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

const allUsers = batchManager.getAllItems();
Object.values(allUsers).forEach(user => {
  console.log(user.name);
});

removeItem(id: string): void

ํŠน์ • ์•„์ดํ…œ์„ store์—์„œ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

// ์‚ฌ์šฉ์ž ์‚ญ์ œ ํ›„ store์—์„œ๋„ ์ œ๊ฑฐ
await deleteUser('user-123');
batchManager.removeItem('user-123');

clearAll(): void

store์˜ ๋ชจ๋“  ์•„์ดํ…œ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

// ๋กœ๊ทธ์•„์›ƒ ์‹œ ๋ชจ๋“  ์บ์‹œ ํด๋ฆฌ์–ด
function handleLogout() {
  batchManager.clearAll();
}

getPendingCount(): number

ํ˜„์žฌ ๋Œ€๊ธฐ ์ค‘์ธ ์š”์ฒญ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. (๋””๋ฒ„๊น…์šฉ)

console.log(`Pending requests: ${batchManager.getPendingCount()}`);

getBufferSize(): number

ํ˜„์žฌ ๋ฒ„ํผ์— ๋Œ€๊ธฐ ์ค‘์ธ ์•„์ดํ…œ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

console.log(`Buffer size: ${batchManager.getBufferSize()}`);

isBufferEmpty(): boolean

๋ฒ„ํผ๊ฐ€ ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

if (batchManager.isBufferEmpty()) {
  console.log('No pending items');
}

getState(): BatchManagerState

BatchManager์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

const state = batchManager.getState();
console.log(`Status: ${state.status}`);
console.log(`Execution count: ${state.executionCount}`);
console.log(`Total processed: ${state.totalItemsProcessed}`);

getStats()

ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

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}`);

subscribe / getSnapshot (useSyncExternalStore์šฉ)

React์˜ useSyncExternalStore์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ๋ฉ”์„œ๋“œ๋“ค์ž…๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ์ง์ ‘ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์ œ๊ณต๋˜๋Š” hooks๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

// ๋‚ด๋ถ€์ ์œผ๋กœ hooks์—์„œ ์‚ฌ์šฉ๋จ
const snapshot = useSyncExternalStore(
  batchManager.subscribe,
  batchManager.getSnapshot
);

subscribeState / getStateSnapshot

BatchManager ์ƒํƒœ ๋ณ€๊ฒฝ์„ ๊ตฌ๋…ํ•˜๊ธฐ ์œ„ํ•œ ๋ฉ”์„œ๋“œ๋“ค์ž…๋‹ˆ๋‹ค.

// ์ƒํƒœ ๋ณ€๊ฒฝ ๊ตฌ๋…
const unsubscribe = batchManager.subscribeState(() => {
  console.log('State changed:', batchManager.getStateSnapshot());
});

// ๊ตฌ๋… ํ•ด์ œ
unsubscribe();

React Hooks

useBatchedItem

๋‹จ์ผ ์•„์ดํ…œ์„ ๊ฐ€์ ธ์˜ค๋Š” ํ›…์ž…๋‹ˆ๋‹ค. ์•„์ดํ…œ์ด ์—†์œผ๋ฉด ์ž๋™์œผ๋กœ ๋ฐฐ์น˜ ์š”์ฒญ์— ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค.

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>
  );
}

useBatchedItems

์—ฌ๋Ÿฌ ์•„์ดํ…œ์„ ํ•œ ๋ฒˆ์— ๊ฐ€์ ธ์˜ค๋Š” ํ›…์ž…๋‹ˆ๋‹ค.

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>
  );
}

useAllBatchedItems

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>
  );
}

usePreloadItems

์•„์ดํ…œ๋“ค์„ ๋ฏธ๋ฆฌ ๋กœ๋“œํ•˜๋Š” ํ›…์ž…๋‹ˆ๋‹ค. 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>
  );
}

useFlushBatch

๋ฐฐ์น˜๋ฅผ ์ˆ˜๋™์œผ๋กœ 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>;
}

useBatchManagerState

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>
  );
}

useBatchedState

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>;
}

useBatchStats

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 ์–ด๋Œ‘ํ„ฐ

createMemoryStore

์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ„๋‹จํ•œ ์ธ๋ฉ”๋ชจ๋ฆฌ 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,
});

createSimpleZustandAdapter

๋‹จ์ˆœํ•œ 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,
});

createZustandAdapter

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,
});

createReduxAdapter

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)
);

์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜

createBuffer

์—ฌ๋Ÿฌ ํ˜ธ์ถœ์„ ๋ฐฐ์น˜๋กœ ๋ชจ์•„์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฒ„ํผ ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. 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());

Persistence ํ•จ์ˆ˜๋“ค

์ƒํƒœ๋ฅผ 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', // ์ž๋™์œผ๋กœ ์ƒํƒœ ์ €์žฅ/๋ณต์›
});

๊ณ ๊ธ‰ ์‚ฌ์šฉ๋ฒ•

์—ฌ๋Ÿฌ BatchManager ์‚ฌ์šฉ

// 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();
}

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ

๊ธฐ์กด ์ฝ”๋“œ์—์„œ ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜๋Š” ๋ฐฉ๋ฒ•:

Before (๊ธฐ์กด ์ฝ”๋“œ)

const user = userStoreManager.requestUser(userId);
userStoreManager.preloadUsers(userIds);
const pendingCount = userStoreManager.getPendingCount();

After (๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ)

const user = useBatchedItem(userBatchManager, userId);
userBatchManager.preloadItems(userIds);
const pendingCount = userBatchManager.getPendingCount();

๋ผ์ด์„ผ์Šค

MIT

๊ธฐ์—ฌ

์ด์Šˆ๋‚˜ PR์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages