diff --git a/README.md b/README.md index a05ab5b..bf28a36 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A comprehensive collection of TypeScript utility functions for modern web develo ## Features -- ๐Ÿ› ๏ธ **Comprehensive**: String, object, cookie, number, validation, format, search query, device, type and common utilities +- ๐Ÿ› ๏ธ **Comprehensive**: String, object, cookie, number, validation, format, search query, device, type, storage and common utilities - ๐Ÿ“ฆ **Tree-shakable**: Import only what you need - ๐Ÿ”’ **Type-safe**: Full TypeScript support with type definitions - โšก **Lightweight**: Minimal dependencies and optimized for performance @@ -76,6 +76,11 @@ const encoded = commonUtil.encodeBase64("Hello ํ•œ๊ธ€!"); // Base64 encoded stri const decoded = commonUtil.decodeBase64(encoded); // "Hello ํ•œ๊ธ€!" const debouncedFn = commonUtil.debounce(() => console.log("Called!"), 300); // Debounced function +// Storage +commonUtil.storage.set("user", { id: 1, name: "John" }); // Stores object in localStorage +const user = commonUtil.storage.get<{ id: number; name: string }>("user"); // Retrieves typed object +commonUtil.storage.remove("user"); // Removes item from localStorage + // Search Query utilities const queryParams = searchQueryUtil.getAllQuery(); // { key: ["value1", "value2"], id: "123" } @@ -107,11 +112,13 @@ import { clearNullProperties, deepFreeze } from "kr-corekit"; import { escapeHtml } from "kr-corekit/stringUtil"; import { sum } from "kr-corekit/numberUtil"; import { clearNullProperties } from "kr-corekit/objectUtil"; +import { storage } from "kr-corekit/commonUtil"; // Usage remains the same const escaped = escapeHtml("
Hello
"); const total = sum(1, 2, 3, 4, 5); const cleaned = clearNullProperties({ a: 1, b: null, c: 3 }); +storage.set("data", { key: "value" }); ``` ### Bundle Size Comparison @@ -151,6 +158,19 @@ const cleaned = clearNullProperties({ a: 1, b: null, c: 3 }); - `checkBase64(value: string): boolean` - Validates whether a string is a valid base64 encoded value - `checkPassword(password: string, options?: { minLength?: number; maxLength?: number; requireUppercase?: boolean; requireLowercase?: boolean; requireNumber?: boolean; requireSpecialChar?: boolean }): boolean` - Validates password strength and requirements +### StorageUtil + +- `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. +- `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. +- `remove(key: string): void` - Removes a specific item from localStorage. Safe for SSR environments. + +**Features:** + +- ๐Ÿ”’ **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 + ### CommonUtil - `isEmpty(value: unknown): boolean` - Checks if a value is empty (null, undefined, "", 0, [], {}, empty Set/Map, NaN, or invalid Date) @@ -160,6 +180,16 @@ const cleaned = clearNullProperties({ a: 1, b: null, c: 3 }); - `encodeBase64(str: string, options?: { convertSpecialChars?: boolean }): string` - Encodes a string to Base64 format with optional special character handling - `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 +- `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. + +**Storage Features:** + +- ๐Ÿ”’ **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 ### SearchQueryUtil diff --git a/package/commonUtil/index.ts b/package/commonUtil/index.ts index b1d4460..1235d4c 100644 --- a/package/commonUtil/index.ts +++ b/package/commonUtil/index.ts @@ -5,3 +5,4 @@ export { default as copyToClipboard } from "./copyToClipboard"; export { default as encodeBase64 } from "./encodeBase64"; export { default as decodeBase64 } from "./decodeBase64"; export { default as debounce } from "./debounce"; +export { default as storage } from "./storage"; diff --git a/package/commonUtil/storage/index.test.ts b/package/commonUtil/storage/index.test.ts new file mode 100644 index 0000000..4908947 --- /dev/null +++ b/package/commonUtil/storage/index.test.ts @@ -0,0 +1,86 @@ +import { beforeEach, describe, expect, Mock, test, vi } from "vitest"; +import storage from "."; + +describe("storage", () => { + const mockLocalStorage = () => { + let store: Record = {}; + + return { + getItem: vi.fn((key: string) => store[key] || null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value; + }), + removeItem: vi.fn((key: string) => { + delete store[key]; + }), + clear: vi.fn(() => { + store = {}; + }), + }; + }; + + vi.stubGlobal("localStorage", mockLocalStorage()); + + describe("storage ์œ ํ‹ธ๋ฆฌํ‹ฐ", () => { + beforeEach(() => { + (window.localStorage.clear as Mock)(); + }); + + describe("set ๋ฐ get ๊ธฐ๋Šฅ", () => { + test("๋ฌธ์ž์—ด ๊ฐ’์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ €์žฅํ•˜๊ณ  ๋ถˆ๋Ÿฌ์™€์•ผ ํ•ฉ๋‹ˆ๋‹ค.", () => { + const key = "test-string"; + const value = "hello world"; + storage.set(key, value); + expect(storage.get(key)).toBe(value); + }); + + test("๊ฐ์ฒด ๊ฐ’์„ JSON์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ €์žฅํ•˜๊ณ , ํŒŒ์‹ฑํ•˜์—ฌ ๋ถˆ๋Ÿฌ์™€์•ผ ํ•ฉ๋‹ˆ๋‹ค.", () => { + const key = "test-object"; + const value = { name: "John Yeom", version: 9.9, isReady: true }; + storage.set(key, value); + expect(storage.get(key)).toEqual(value); + }); + + test("๋ฐฐ์—ด ๊ฐ’์„ JSON์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ €์žฅํ•˜๊ณ , ํŒŒ์‹ฑํ•˜์—ฌ ๋ถˆ๋Ÿฌ์™€์•ผ ํ•ฉ๋‹ˆ๋‹ค.", () => { + const key = "test-array"; + const value = [1, "test", { id: 3 }]; + storage.set(key, value); + expect(storage.get(key)).toEqual(value); + }); + + test("์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ‚ค๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋ ค๊ณ  ํ•˜๋ฉด null์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", () => { + expect(storage.get("non-existent-key")).toBeNull(); + }); + }); + + describe("remove ๊ธฐ๋Šฅ", () => { + test("์ €์žฅ๋œ ๊ฐ’์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ œ๊ฑฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", () => { + const key = "item-to-remove"; + const value = "some data"; + + storage.set(key, value); + expect(storage.get(key)).toBe(value); + + storage.remove(key); + expect(storage.get(key)).toBeNull(); + }); + }); + + describe("์˜ˆ์™ธ ์ฒ˜๋ฆฌ", () => { + test("localStorage์— ์ €์žฅ๋œ ๊ฐ’์ด ์†์ƒ๋œ JSON์ผ ๊ฒฝ์šฐ, null์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", () => { + const key = "corrupted-json"; + const corruptedValue = '{"name": "Gemini", "version": 1.5,}'; // ๋งˆ์ง€๋ง‰์— ์ž˜๋ชป๋œ ์‰ผํ‘œ + + (window.localStorage.getItem as Mock).mockReturnValueOnce( + corruptedValue + ); + + const spy = vi.spyOn(console, "error").mockImplementation(() => {}); + + expect(storage.get(key)).toBeNull(); + + spy.mockRestore(); + }); + }); + }); +}); diff --git a/package/commonUtil/storage/index.ts b/package/commonUtil/storage/index.ts new file mode 100644 index 0000000..e30b0cc --- /dev/null +++ b/package/commonUtil/storage/index.ts @@ -0,0 +1,74 @@ +/** + * localStorage๋ฅผ ํŽธ๋ฆฌํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋“ค์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * * ๊ฐ์ฒด๋‚˜ ๋ฐฐ์—ด๋„ ์ €์žฅ ๋ฐ ์กฐํšŒ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + * * SSR ํ™˜๊ฒฝ์—์„œ๋Š” ๋™์ž‘ํ•˜์ง€ ์•Š์œผ๋ฉฐ, ํ•ด๋‹น ๊ฒฝ์šฐ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. + */ +const storage = { + /** + * localStorage์— ๊ฐ’์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * @param {string} key - ์ €์žฅํ•  ๋ฐ์ดํ„ฐ์˜ ํ‚ค + * @param {T} value - ์ €์žฅํ•  ๋ฐ์ดํ„ฐ. ๊ฐ์ฒด๋‚˜ ๋ฐฐ์—ด๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + */ + set(key: string, value: T): void { + if (typeof window === "undefined") { + console.warn( + `localStorage is not available in SSR environment. Set operation for key "${key}" was ignored.` + ); + return; + } + + try { + const serializedValue = JSON.stringify(value); + window.localStorage.setItem(key, serializedValue); + } catch (error) { + console.error(`Error setting item "${key}" to localStorage`, error); + } + }, + + /** + * localStorage์—์„œ ๊ฐ’์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * @param {string} key - ๊ฐ€์ ธ์˜ฌ ๋ฐ์ดํ„ฐ์˜ ํ‚ค + * @returns {T | null} ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ’์ด ์—†๊ฑฐ๋‚˜ ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ null์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + get(key: string): T | null { + if (typeof window === "undefined") { + return null; + } + + try { + const serializedValue = window.localStorage.getItem(key); + + if (serializedValue === null) { + return null; + } + + return JSON.parse(serializedValue) as T; + } catch (error) { + console.error(`Error getting item "${key}" from localStorage`, error); + window.localStorage.removeItem(key); + return null; + } + }, + + /** + * localStorage์—์„œ ๊ฐ’์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + * @param {string} key - ์ œ๊ฑฐํ•  ๋ฐ์ดํ„ฐ์˜ ํ‚ค + */ + remove(key: string): void { + if (typeof window === "undefined") { + console.warn( + `localStorage is not available in SSR environment. Remove operation for key "${key}" was ignored.` + ); + return; + } + + try { + window.localStorage.removeItem(key); + } catch (error) { + console.error(`Error removing item "${key}" from localStorage`, error); + } + }, +}; + +export default storage;