From 2108c5d69ff14d0331b7eb48f1df621a39277dbf Mon Sep 17 00:00:00 2001 From: klmhyeonwoo Date: Fri, 24 Oct 2025 18:51:28 +0900 Subject: [PATCH 1/8] feat: Added retry utility functions --- package/commonUtil/index.ts | 1 + package/commonUtil/retry/index.test.ts | 66 ++++++++++++++++++++++++++ package/commonUtil/retry/index.ts | 21 ++++++++ 3 files changed, 88 insertions(+) create mode 100644 package/commonUtil/retry/index.test.ts create mode 100644 package/commonUtil/retry/index.ts diff --git a/package/commonUtil/index.ts b/package/commonUtil/index.ts index 79d5920..80e53e4 100644 --- a/package/commonUtil/index.ts +++ b/package/commonUtil/index.ts @@ -6,3 +6,4 @@ export { default as encodeBase64 } from "./encodeBase64"; export { default as decodeBase64 } from "./decodeBase64"; export { default as debounce } from "./debounce"; export { default as throttle } from "./throttle"; +export { default as retry } from "./retry"; diff --git a/package/commonUtil/retry/index.test.ts b/package/commonUtil/retry/index.test.ts new file mode 100644 index 0000000..644ed1c --- /dev/null +++ b/package/commonUtil/retry/index.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test, vi } from "vitest"; +import retry from "."; + +describe("retry 유틸 함수 테스트", () => { + test("정상적으로 실행되는 함수는 한번만 실행한다.", async () => { + const fn = vi.fn().mockResolvedValue("success!"); + + const result = await retry(fn, 3); + + expect(result).toBe("success!"); + expect(fn).toHaveBeenCalledTimes(1); + }); + + test("함수를 실행하며 오류가 발생하면 재시도한다.", async () => { + const fn = vi + .fn() + .mockRejectedValueOnce(new Error("fail 1")) + .mockRejectedValueOnce(new Error("fail 2")) + .mockResolvedValue("success!"); + + const result = await retry(fn, 3); + + expect(result).toBe("success!"); + expect(fn).toHaveBeenCalledTimes(3); + }); + + test("재시도 한도를 초과하면 마지막 에러를 던진다.", async () => { + const error = new Error("always fail"); + const fn = vi.fn().mockRejectedValue(error); + + await expect(retry(fn, 2)).rejects.toThrow("always fail"); + expect(fn).toHaveBeenCalledTimes(3); + }); + + test("기본 재시도 횟수는 3번이다.", async () => { + const fn = vi.fn().mockRejectedValue(new Error("fail")); + + await expect(retry(fn)).rejects.toThrow("fail"); + expect(fn).toHaveBeenCalledTimes(4); + }); + + test("타입 안전성: 반환 타입이 올바르게 추론된다.", async () => { + const stringFn = vi.fn().mockResolvedValue("hello"); + const numberFn = vi.fn().mockResolvedValue(42); + const objectFn = vi.fn().mockResolvedValue({ id: 1 }); + + const stringResult = await retry(stringFn); + const numberResult = await retry(numberFn); + const objectResult = await retry(objectFn); + + expect(typeof stringResult).toBe("string"); + expect(typeof numberResult).toBe("number"); + expect(typeof objectResult).toBe("object"); + + expect(stringResult).toBe("hello"); + expect(numberResult).toBe(42); + expect(objectResult).toEqual({ id: 1 }); + }); + + test("0번 재시도 시 한 번만 실행한다.", async () => { + const fn = vi.fn().mockRejectedValue(new Error("fail")); + + await expect(retry(fn, 0)).rejects.toThrow("fail"); + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/package/commonUtil/retry/index.ts b/package/commonUtil/retry/index.ts new file mode 100644 index 0000000..bd348e1 --- /dev/null +++ b/package/commonUtil/retry/index.ts @@ -0,0 +1,21 @@ +export default async function retry( + fn: () => Promise, + loop: number = 3 +): Promise { + let count = 0; + + const attempt = async (): Promise => { + try { + return await fn(); + } catch (e) { + count++; + if (count > loop) { + throw e; + } + console.error(`Retry attempt ${count} / ${loop} failed:`, e); + return attempt(); + } + }; + + return attempt(); +} From ac5345c1df3cb1fe97285dd52069b00083d1568b Mon Sep 17 00:00:00 2001 From: klmhyeonwoo Date: Sat, 25 Oct 2025 19:01:12 +0900 Subject: [PATCH 2/8] fix: remove retry utility output console.error --- README.md | 21 ++++++++++++++++++++- package/commonUtil/retry/index.ts | 1 - 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d37b94f..0c33f6e 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,22 @@ commonUtil.storage.set("user", { id: 1, name: "John" }); // Stores object in loc const user = commonUtil.storage.get<{ id: number; name: string }>("user"); // Retrieves typed object commonUtil.storage.remove("user"); // Removes item from localStorage +// Retry utilities +const result = await commonUtil.retry(async () => { + const response = await fetch("/api/data"); + if (!response.ok) throw new Error("API failed"); + return response.json(); +}, 3); // Retry up to 3 times + +// More retry examples +const userData = await commonUtil.retry(async () => { + return await fetchUserData(); +}); // Uses default 3 retries + +const fileUpload = await commonUtil.retry(async () => { + return await uploadFile(file); +}, 5); // Custom retry count + // Search Query utilities const queryParams = searchQueryUtil.getAllQuery(); // { key: ["value1", "value2"], id: "123" } @@ -182,6 +198,7 @@ storage.set("data", { key: "value" }); - `decodeBase64(str: string, options?: { convertSpecialChars?: boolean }): string` - Decodes a Base64 string back to original text with optional special character handling - `debounce(fn: T, delay?: number): (...args: Parameters) => void` - Creates a debounced function that delays execution until after a specified delay (default 300ms) has passed since its last invocation - `throttle(fn: T, limit?: number): (...args: Parameters) => void` - Creates a throttled function that only executes at most once per specified time interval (default 300ms), ignoring subsequent calls within the limit + <<<<<<< HEAD - `storage.set(key: string, value: T): void` - Stores a value in localStorage with automatic JSON serialization. Supports objects, arrays, and primitive types. Safe for SSR environments. - `storage.get(key: string): T | null` - Retrieves a value from localStorage with automatic JSON parsing. Returns null if key doesn't exist or parsing fails. Type-safe with generic support. - `storage.remove(key: string): void` - Removes a specific item from localStorage. Safe for SSR environments. @@ -191,7 +208,9 @@ storage.set("data", { key: "value" }); - 🔒 **SSR Safe**: All methods handle server-side rendering environments gracefully - 📦 **Type Safe**: Full TypeScript support with generics - 🛡️ **Error Handling**: Comprehensive error handling with automatic cleanup of corrupted data -- 🔄 **Auto Serialization**: Automatic JSON serialization/deserialization for complex data types +- # 🔄 **Auto Serialization**: Automatic JSON serialization/deserialization for complex data types +- `retry(fn: () => Promise, loop?: number): Promise` - Retries an asynchronous function up to the specified number of times (default 3) if it fails. Uses closure to maintain retry count and automatically re-attempts on error. Returns the result of the first successful execution or throws the last error if all retries fail. + > > > > > > > 0f784f4 (docs: Add retry utility markdown docs) ### SearchQueryUtil diff --git a/package/commonUtil/retry/index.ts b/package/commonUtil/retry/index.ts index bd348e1..c702355 100644 --- a/package/commonUtil/retry/index.ts +++ b/package/commonUtil/retry/index.ts @@ -12,7 +12,6 @@ export default async function retry( if (count > loop) { throw e; } - console.error(`Retry attempt ${count} / ${loop} failed:`, e); return attempt(); } }; From a278f589b036759828a08c644999e78da02f9c7d Mon Sep 17 00:00:00 2001 From: klmhyeonwoo Date: Sat, 25 Oct 2025 19:02:21 +0900 Subject: [PATCH 3/8] docs: Remove incorrectly inserted comments --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 0c33f6e..d9b9d50 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,6 @@ storage.set("data", { key: "value" }); - `decodeBase64(str: string, options?: { convertSpecialChars?: boolean }): string` - Decodes a Base64 string back to original text with optional special character handling - `debounce(fn: T, delay?: number): (...args: Parameters) => void` - Creates a debounced function that delays execution until after a specified delay (default 300ms) has passed since its last invocation - `throttle(fn: T, limit?: number): (...args: Parameters) => void` - Creates a throttled function that only executes at most once per specified time interval (default 300ms), ignoring subsequent calls within the limit - <<<<<<< HEAD - `storage.set(key: string, value: T): void` - Stores a value in localStorage with automatic JSON serialization. Supports objects, arrays, and primitive types. Safe for SSR environments. - `storage.get(key: string): T | null` - Retrieves a value from localStorage with automatic JSON parsing. Returns null if key doesn't exist or parsing fails. Type-safe with generic support. - `storage.remove(key: string): void` - Removes a specific item from localStorage. Safe for SSR environments. @@ -210,7 +209,6 @@ storage.set("data", { key: "value" }); - 🛡️ **Error Handling**: Comprehensive error handling with automatic cleanup of corrupted data - # 🔄 **Auto Serialization**: Automatic JSON serialization/deserialization for complex data types - `retry(fn: () => Promise, loop?: number): Promise` - Retries an asynchronous function up to the specified number of times (default 3) if it fails. Uses closure to maintain retry count and automatically re-attempts on error. Returns the result of the first successful execution or throws the last error if all retries fail. - > > > > > > > 0f784f4 (docs: Add retry utility markdown docs) ### SearchQueryUtil From dc6c7bd5ec878bab35b1a38f40617b22e1492890 Mon Sep 17 00:00:00 2001 From: klmhyeonwoo Date: Sat, 25 Oct 2025 19:04:40 +0900 Subject: [PATCH 4/8] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d9b9d50..6e57f95 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ storage.set("data", { key: "value" }); - 🔒 **SSR Safe**: All methods handle server-side rendering environments gracefully - 📦 **Type Safe**: Full TypeScript support with generics - 🛡️ **Error Handling**: Comprehensive error handling with automatic cleanup of corrupted data -- # 🔄 **Auto Serialization**: Automatic JSON serialization/deserialization for complex data types +- 🔄 **Auto Serialization**: Automatic JSON serialization/deserialization for complex data types - `retry(fn: () => Promise, loop?: number): Promise` - Retries an asynchronous function up to the specified number of times (default 3) if it fails. Uses closure to maintain retry count and automatically re-attempts on error. Returns the result of the first successful execution or throws the last error if all retries fail. ### SearchQueryUtil From 55e4086102215b40fc064f2641933397a6eef539 Mon Sep 17 00:00:00 2001 From: klmhyeonwoo Date: Sat, 25 Oct 2025 19:08:52 +0900 Subject: [PATCH 5/8] Update package/commonUtil/retry/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- package/commonUtil/retry/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/commonUtil/retry/index.ts b/package/commonUtil/retry/index.ts index c702355..62a5c89 100644 --- a/package/commonUtil/retry/index.ts +++ b/package/commonUtil/retry/index.ts @@ -9,7 +9,7 @@ export default async function retry( return await fn(); } catch (e) { count++; - if (count > loop) { + if (count >= loop) { throw e; } return attempt(); From 667d981c14d20ab66dee03fcfc213cc870982598 Mon Sep 17 00:00:00 2001 From: klmhyeonwoo Date: Sat, 25 Oct 2025 19:09:13 +0900 Subject: [PATCH 6/8] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e57f95..488ff0e 100644 --- a/README.md +++ b/README.md @@ -208,8 +208,10 @@ storage.set("data", { key: "value" }); - 📦 **Type Safe**: Full TypeScript support with generics - 🛡️ **Error Handling**: Comprehensive error handling with automatic cleanup of corrupted data - 🔄 **Auto Serialization**: Automatic JSON serialization/deserialization for complex data types -- `retry(fn: () => Promise, loop?: number): Promise` - Retries an asynchronous function up to the specified number of times (default 3) if it fails. Uses closure to maintain retry count and automatically re-attempts on error. Returns the result of the first successful execution or throws the last error if all retries fail. +### Retry + +- `retry(fn: () => Promise, loop?: number): Promise` - Retries an asynchronous function up to the specified number of times (default 3) if it fails. Automatically re-attempts on error and returns the result of the first successful execution, or throws the last error if all retries fail. ### SearchQueryUtil - `getAllQuery(): Record` - Parses the current URL's query string and returns an object with key-value pairs. Values appear as arrays when the same key is used multiple times. From 50b52b929971ac8fb7dd4eea96d5d7f2493a7b0f Mon Sep 17 00:00:00 2001 From: klmhyeonwoo Date: Sat, 25 Oct 2025 19:13:20 +0900 Subject: [PATCH 7/8] Update package/commonUtil/retry/index.test.ts --- package/commonUtil/retry/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/commonUtil/retry/index.test.ts b/package/commonUtil/retry/index.test.ts index 644ed1c..f96e728 100644 --- a/package/commonUtil/retry/index.test.ts +++ b/package/commonUtil/retry/index.test.ts @@ -36,7 +36,7 @@ describe("retry 유틸 함수 테스트", () => { const fn = vi.fn().mockRejectedValue(new Error("fail")); await expect(retry(fn)).rejects.toThrow("fail"); - expect(fn).toHaveBeenCalledTimes(4); + expect(fn).toHaveBeenCalledTimes(3); }); test("타입 안전성: 반환 타입이 올바르게 추론된다.", async () => { From cc45149cc6128cbafb80e876d332e8435669d534 Mon Sep 17 00:00:00 2001 From: klmhyeonwoo Date: Sat, 25 Oct 2025 19:14:51 +0900 Subject: [PATCH 8/8] Update package/commonUtil/retry/index.test.ts --- package/commonUtil/retry/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/commonUtil/retry/index.test.ts b/package/commonUtil/retry/index.test.ts index f96e728..6eaf46f 100644 --- a/package/commonUtil/retry/index.test.ts +++ b/package/commonUtil/retry/index.test.ts @@ -29,7 +29,7 @@ describe("retry 유틸 함수 테스트", () => { const fn = vi.fn().mockRejectedValue(error); await expect(retry(fn, 2)).rejects.toThrow("always fail"); - expect(fn).toHaveBeenCalledTimes(3); + expect(fn).toHaveBeenCalledTimes(2); }); test("기본 재시도 횟수는 3번이다.", async () => {