diff --git a/README.ja.md b/README.ja.md
new file mode 100644
index 0000000..8d404c4
--- /dev/null
+++ b/README.ja.md
@@ -0,0 +1,102 @@
+# kr-corekit
+
+言語: [English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md)
+
+明確で実用的な API 設計を重視した JavaScript/TypeScript ユーティリティライブラリです。
+
+## 特徴
+
+- 文字列・配列・オブジェクト・非同期・Promise・日付・数値など 130+ 関数
+- Tree-shaking しやすいモジュール export
+- TypeScript `.d.ts` 型定義を提供
+- ESM/CJS バンドルを提供
+
+## インストール
+
+```bash
+npm install kr-corekit
+# or
+pnpm add kr-corekit
+# or
+yarn add kr-corekit
+```
+
+## クイックスタート
+
+```ts
+import {
+ stringUtil,
+ arrayUtil,
+ objectUtil,
+ asyncUtil,
+ promiseUtil,
+ dateUtil,
+ mathUtil,
+ langUtil,
+} from "kr-corekit";
+
+const id = stringUtil.camelCase("user profile id");
+const rows = arrayUtil.chunk([1, 2, 3, 4, 5], 2);
+const city = objectUtil.get({ user: { profile: { city: "Seoul" } } }, "user.profile.city");
+
+const mapped = await asyncUtil.mapAsync([1, 2, 3], async (v) => v * 2);
+const safe = await promiseUtil.withTimeout(fetch("/api/health"), 1000);
+
+const tomorrow = dateUtil.addDays(new Date(), 1);
+const average = mathUtil.mean([10, 20, 30]);
+const enabled = langUtil.toBoolean("yes");
+```
+
+## モジュール
+
+- `stringUtil`: ケース変換、HTML escape/unescape、truncate、slugify
+- `arrayUtil`: chunk、flatten、uniq/uniqBy、groupBy、sortBy、集合演算、サンプリング
+- `collectionUtil`: 配列/オブジェクト向け map/filter/reduce/find/every/some/includes
+- `objectUtil`: get/set/has/merge/defaults/pick/omit/deepClone/deepFreeze
+- `numberUtil`: clamp、inRange、random、ceil/floor/round、sum/subtract/multiply
+- `mathUtil`: mean/median/min/max/sumBy/minBy/maxBy
+- `dateUtil`: 日時加減算、start/end of day、formatDate、日付比較
+- `langUtil`: toBoolean/toNumber/toString/defaultTo/castArray/isEqual
+- `asyncUtil`: pLimit、mapAsync、filterAsync、eachAsync、series、parallel
+- `promiseUtil`: defer、withTimeout、retryWithDelay、settle、toResult
+- 既存モジュール: `commonUtil`, `functionUtil`, `validationUtil`, `formatUtil`, `typeUtil`, `cookieUtil`, `deviceUtil`, `searchQueryUtil`
+
+## Tree-Shaking インポート
+
+```ts
+import { camelCase } from "kr-corekit/stringUtil";
+import { chunk } from "kr-corekit/arrayUtil";
+import { get } from "kr-corekit/objectUtil";
+import { mapAsync } from "kr-corekit/asyncUtil";
+import { withTimeout } from "kr-corekit/promiseUtil";
+```
+
+## 全 API サンプル
+
+- 公開 API の全サンプルは [`docs/API_EXAMPLES.md`](./docs/API_EXAMPLES.md) を参照してください。
+
+## ベンチマーク
+
+```bash
+npm run benchmark
+```
+
+ビルド後にローカルのマイクロベンチマーク [`benchmark/index.mjs`](./benchmark/index.mjs) を実行します。
+
+最新ローカルサンプル(2026-02-25):
+
+```text
+array.chunk ~1,065,050 ops/s
+object.get ~2,681,055 ops/s
+string.camelCase ~1,902,407 ops/s
+async.mapAsync ~1,514,005 ops/s
+```
+
+## 補足
+
+- API 全体は `package/*/index.ts` と `dist/types/*/index.d.ts` で確認できます。
+- 関数ドキュメントを拡張する場合は多言語 README を同期してください。
+
+## ライセンス
+
+MIT
diff --git a/README.ko.md b/README.ko.md
new file mode 100644
index 0000000..a923851
--- /dev/null
+++ b/README.ko.md
@@ -0,0 +1,102 @@
+# kr-corekit
+
+언어: [English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md)
+
+명확하고 실용적인 API 설계로 만든 JavaScript/TypeScript 유틸리티 모음입니다.
+
+## 핵심 특징
+
+- 문자열/배열/객체/비동기/Promise/날짜/수학/언어 유틸 포함 130개+ 함수
+- 트리 셰이킹에 유리한 모듈 export 구조
+- TypeScript `.d.ts` 타입 제공
+- ESM/CJS 번들 제공
+
+## 설치
+
+```bash
+npm install kr-corekit
+# or
+pnpm add kr-corekit
+# or
+yarn add kr-corekit
+```
+
+## 빠른 사용 예시
+
+```ts
+import {
+ stringUtil,
+ arrayUtil,
+ objectUtil,
+ asyncUtil,
+ promiseUtil,
+ dateUtil,
+ mathUtil,
+ langUtil,
+} from "kr-corekit";
+
+const id = stringUtil.camelCase("user profile id");
+const rows = arrayUtil.chunk([1, 2, 3, 4, 5], 2);
+const city = objectUtil.get({ user: { profile: { city: "Seoul" } } }, "user.profile.city");
+
+const mapped = await asyncUtil.mapAsync([1, 2, 3], async (v) => v * 2);
+const safe = await promiseUtil.withTimeout(fetch("/api/health"), 1000);
+
+const tomorrow = dateUtil.addDays(new Date(), 1);
+const average = mathUtil.mean([10, 20, 30]);
+const enabled = langUtil.toBoolean("yes");
+```
+
+## 모듈 구성
+
+- `stringUtil`: 케이스 변환, HTML escape/unescape, truncate, slugify
+- `arrayUtil`: chunk, flatten, uniq/uniqBy, groupBy, sortBy, 집합 연산, 샘플링
+- `collectionUtil`: 배열/객체 대상 map/filter/reduce/find/every/some/includes
+- `objectUtil`: get/set/has/merge/defaults/pick/omit/deepClone/deepFreeze
+- `numberUtil`: clamp, inRange, random, ceil/floor/round, sum/subtract/multiply
+- `mathUtil`: mean/median/min/max/sumBy/minBy/maxBy
+- `dateUtil`: 일/시간 가감, 하루 시작/끝, formatDate, 날짜 비교
+- `langUtil`: toBoolean/toNumber/toString/defaultTo/castArray/isEqual
+- `asyncUtil`: pLimit, mapAsync, filterAsync, eachAsync, series, parallel
+- `promiseUtil`: defer, withTimeout, retryWithDelay, settle, toResult
+- 기존 모듈: `commonUtil`, `functionUtil`, `validationUtil`, `formatUtil`, `typeUtil`, `cookieUtil`, `deviceUtil`, `searchQueryUtil`
+
+## 트리 셰이킹 import
+
+```ts
+import { camelCase } from "kr-corekit/stringUtil";
+import { chunk } from "kr-corekit/arrayUtil";
+import { get } from "kr-corekit/objectUtil";
+import { mapAsync } from "kr-corekit/asyncUtil";
+import { withTimeout } from "kr-corekit/promiseUtil";
+```
+
+## 전체 API 예제
+
+- 모든 공개 API 예제는 [`docs/API_EXAMPLES.md`](./docs/API_EXAMPLES.md)에서 확인할 수 있습니다.
+
+## 벤치마크
+
+```bash
+npm run benchmark
+```
+
+빌드 후 [`benchmark/index.mjs`](./benchmark/index.mjs) 기반 로컬 마이크로 벤치마크를 실행합니다.
+
+최신 로컬 샘플 (2026-02-25):
+
+```text
+array.chunk ~1,065,050 ops/s
+object.get ~2,681,055 ops/s
+string.camelCase ~1,902,407 ops/s
+async.mapAsync ~1,514,005 ops/s
+```
+
+## 참고
+
+- 전체 API는 `package/*/index.ts` 및 `dist/types/*/index.d.ts`에서 확인할 수 있습니다.
+- 함수별 문서를 더 확장할 경우 다국어 README 파일을 함께 동기화하세요.
+
+## 라이선스
+
+MIT
diff --git a/README.md b/README.md
index 5422901..59dcbea 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,15 @@
# kr-corekit
-A comprehensive collection of TypeScript utility functions for modern web development.
+Language: [English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md)
-## Features
+A utility toolkit for JavaScript and TypeScript built with a clear, practical API design.
-- 🛠️ **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
-- 🧪 **Well-tested**: Extensive test coverage with comprehensive test cases
+## Highlights
+
+- 130+ utility functions across string, array, object, async, promise, date, math, lang and more
+- Tree-shake friendly module exports
+- TypeScript-first with generated `.d.ts` files
+- ESM/CJS bundle output
## Installation
@@ -20,223 +21,82 @@ pnpm add kr-corekit
yarn add kr-corekit
```
-## Usage
-
-### Full Import (All utilities)
+## Quick Start
-```typescript
+```ts
import {
stringUtil,
+ arrayUtil,
objectUtil,
- cookieUtil,
- numberUtil,
- validationUtil,
- commonUtil,
- formatUtil,
- searchQueryUtil,
- typeUtil,
- deviceUtil,
+ asyncUtil,
+ promiseUtil,
+ dateUtil,
+ mathUtil,
+ langUtil,
} from "kr-corekit";
-// String utilities
-const escaped = stringUtil.escapeHtml("
Hello
");
-const unescaped = stringUtil.unescapeHtml("<div>Hello</div>");
-const slug = stringUtil.slugify("Hello World! 안녕하세요"); // "hello-world-안녕하세요"
-
-// Object utilities
-const cleaned = objectUtil.clearNullProperties({ a: 1, b: null, c: 3 });
-const frozen = objectUtil.deepFreeze({ a: { b: 1 } });
-const withoutKey = objectUtil.removeKey("b", { a: 1, b: 2, c: 3 }); // { a: 1, c: 3 }
-
-// Number utilities
-const total = numberUtil.sum(1, 2, 3, 4, 5); // 15
-const difference = numberUtil.subtract(10, 3); // 7
-const product = numberUtil.multiply(2, 3, 4); // 24
-
-// Validation utilities
-const isValid = validationUtil.checkEmail("user@example.com"); // true
-const isHttpUrl = validationUtil.checkHttpUrl("https://example.com"); // true
-const isDomain = validationUtil.checkDomain("example.com"); // true
-const isBase64 = validationUtil.checkBase64("U29tZSB2YWxpZCBiYXNlNjQgc3RyaW5n"); // true
-const isPasswordValid = validationUtil.checkPassword("Abc123!@#", {
- minLength: 8,
- requireUppercase: true,
- requireLowercase: true,
- requireNumber: true,
- requireSpecialChar: true,
-}); // true
-
-// Common utilities
-const empty = commonUtil.isEmpty(""); // true
-const notEmpty = commonUtil.isEmpty("hello"); // false
-const nullCheck = commonUtil.isNull(null); // true
-const notNull = commonUtil.isNull("hello"); // false
-await commonUtil.sleep(1000); // Pauses execution for 1 second
-const copied = await commonUtil.copyToClipboard("Hello, World!"); // true if successful
-const encoded = commonUtil.encodeBase64("Hello 한글!"); // Base64 encoded string
-const decoded = commonUtil.decodeBase64(encoded); // "Hello 한글!"
-const debouncedFn = commonUtil.debounce(() => console.log("Called!"), 300); // Debounced function
-const throttledFn = commonUtil.throttle(() => console.log("Throttled!"), 300); // Throttled 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
-
-// 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" }
-
-// Type utilities
-const isPlain = typeUtil.isPlainObject({}); // true
-const isNotPlain = typeUtil.isPlainObject(new Date()); // false
-
-// Device utilities
-const device = deviceUtil.getDevice(); // { isMobile: false, isTablet: false, isDesktop: true, isIOS: false, isAndroid: false }
-
-// Cookie utilities
-cookieUtil.setCookie("theme", "dark");
-const theme = cookieUtil.getCookie("theme");
-// Format utilities
-const formattedPhone = formatUtil.formatPhoneNumber("01012345678"); // "010-1234-5678"
-```
-
-### Tree-Shaking Optimized Import (Recommended)
-
-For optimal bundle size, import only the functions you need:
+const id = stringUtil.camelCase("user profile id"); // userProfileId
+const rows = arrayUtil.chunk([1, 2, 3, 4, 5], 2); // [[1,2], [3,4], [5]]
+const city = objectUtil.get({ user: { profile: { city: "Seoul" } } }, "user.profile.city");
-```typescript
-// Option 1: Import specific functions (best tree-shaking)
-import { escapeHtml, unescapeHtml } from "kr-corekit";
-import { sum, multiply } from "kr-corekit";
-import { clearNullProperties, deepFreeze } from "kr-corekit";
+const mapped = await asyncUtil.mapAsync([1, 2, 3], async (v) => v * 2);
+const safe = await promiseUtil.withTimeout(fetch("/api/health"), 1000);
-// Option 2: Import from specific utility modules (good tree-shaking)
-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" });
+const tomorrow = dateUtil.addDays(new Date(), 1);
+const average = mathUtil.mean([10, 20, 30]);
+const enabled = langUtil.toBoolean("yes");
```
-### Bundle Size Comparison
-
-- **Full import**: ~8.3KB (2.9KB gzipped)
-- **Tree-shaken import**: Only includes functions you use
-- **Individual module import**: Further optimized for specific utilities
-
-## API Reference
-
-### StringUtil
-
-- `escapeHtml(str: string): string` - Escapes HTML special characters
-- `unescapeHtml(str: string): string` - Unescapes HTML entities
-- `slugify(text: string): string` - Converts a string to URL-friendly slug format. Replaces spaces with hyphens, removes special characters, converts to lowercase, and supports Korean characters (e.g., "Hello World! 안녕" → "hello-world-안녕")
-
-### ObjectUtil
-
-- `clearNullProperties(obj: object): object` - Removes null/undefined properties
-- `deepFreeze(obj: object): object` - Deep freezes an object recursively
-- `removeKey(key: string, obj: Record): object` - Returns a new object with the specified key removed
-
-### NumberUtil
-
-- `sum(...numbers: number[]): number` - Calculates sum of numbers
-- `subtract(...numbers: number[]): number` - Calculates subtraction of numbers
-- `multiply(...numbers: number[]): number` - Calculates multiplication of numbers
-
-### FormatUtil
-
-- `formatPhoneNumber(phone: string): string` - Formats a phone number string to a standard format (e.g., "010-1234-5678")
-- `formatNumberWithCommas(value: number | string | null | undefined): string` - Converts numbers or strings to comma-separated format (e.g., "1,234,567"). Returns empty string for null/undefined values.
-
-### ValidationUtil
-
-- `checkEmail(email: string): boolean` - Validates email format
-- `checkHttpUrl(url: string): boolean` - Validates HTTP/HTTPS URL format
-- `checkDomain(domain: string): boolean` - Validates domain name format
-- `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)
-- `isNull(value: unknown): value is null` - Type guard that checks if a value is null and narrows the type
-- `sleep(ms: number): Promise` - Pauses execution for a specified number of milliseconds
-- `copyToClipboard(text: string): Promise` - Copies text to the user's clipboard. Uses modern Clipboard API with fallback to legacy execCommand method. Returns true if successful, false if failed.
-- `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
-- `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
-- `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
-
-### 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
+## Modules
+
+- `stringUtil`: case conversion, HTML escape/unescape, truncate, slugify
+- `arrayUtil`: chunk, flatten, uniq/uniqBy, groupBy, sortBy, set ops, sampling
+- `collectionUtil`: map/filter/reduce/find/every/some/includes over arrays/objects
+- `objectUtil`: get/set/has/merge/defaults/pick/omit/deepClone/deepFreeze
+- `numberUtil`: clamp, inRange, random, ceil/floor/round, sum/subtract/multiply
+- `mathUtil`: mean/median/min/max/sumBy/minBy/maxBy
+- `dateUtil`: add/sub days/hours, start/end of day, formatDate, date comparisons
+- `langUtil`: toBoolean/toNumber/toString/defaultTo/castArray/isEqual
+- `asyncUtil`: pLimit, mapAsync, filterAsync, eachAsync, series, parallel
+- `promiseUtil`: defer, withTimeout, retryWithDelay, settle, toResult
+- plus existing: `commonUtil`, `functionUtil`, `validationUtil`, `formatUtil`, `typeUtil`, `cookieUtil`, `deviceUtil`, `searchQueryUtil`
+
+## Tree-Shaking Import
+
+```ts
+import { camelCase } from "kr-corekit/stringUtil";
+import { chunk } from "kr-corekit/arrayUtil";
+import { get } from "kr-corekit/objectUtil";
+import { mapAsync } from "kr-corekit/asyncUtil";
+import { withTimeout } from "kr-corekit/promiseUtil";
+```
-- `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.
+## Full API Examples
-### TypeUtil
+- See [`docs/API_EXAMPLES.md`](./docs/API_EXAMPLES.md) for examples of all public APIs.
-- `isPlainObject(value: unknown): boolean` - Checks if a value is a plain object (created by Object literal or Object.create(null)), excluding arrays, dates, and other built-in objects.
+## Benchmark
-### DeviceUtil
+```bash
+npm run benchmark
+```
-- `getDevice(): DeviceInfo` - Detects the user's device environment. Returns information about device type (mobile/tablet/desktop) and operating system (iOS/Android). Uses navigator.userAgent for detection and provides safe fallback for SSR environments.
+This runs a local micro-benchmark script at [`benchmark/index.mjs`](./benchmark/index.mjs) after build.
-### CookieUtil
+Latest local sample (2026-02-25):
-- `setCookie(name: string, value: string, options?: object): void` - Sets a cookie
-- `getCookie(name: string): string | null` - Gets a cookie value
+```text
+array.chunk ~1,065,050 ops/s
+object.get ~2,681,055 ops/s
+string.camelCase ~1,902,407 ops/s
+async.mapAsync ~1,514,005 ops/s
+```
-## Contributing
+## Notes
-Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
+- Full API list is available via module entry files under `package/*/index.ts` and generated types under `dist/types/*/index.d.ts`.
+- If you want function-level docs/examples expanded further, keep language files in sync when updating.
## License
-MIT License - see LICENSE file for details.
+MIT
diff --git a/README.zh-CN.md b/README.zh-CN.md
new file mode 100644
index 0000000..6821c68
--- /dev/null
+++ b/README.zh-CN.md
@@ -0,0 +1,102 @@
+# kr-corekit
+
+语言: [English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md)
+
+一个为 JavaScript/TypeScript 打造的工具函数库,强调清晰且实用的 API 设计。
+
+## 主要特性
+
+- 覆盖字符串、数组、对象、异步、Promise、日期、数学、语言等 130+ 函数
+- 适合 Tree-shaking 的模块导出结构
+- 提供 TypeScript `.d.ts` 类型
+- 提供 ESM/CJS 打包产物
+
+## 安装
+
+```bash
+npm install kr-corekit
+# or
+pnpm add kr-corekit
+# or
+yarn add kr-corekit
+```
+
+## 快速示例
+
+```ts
+import {
+ stringUtil,
+ arrayUtil,
+ objectUtil,
+ asyncUtil,
+ promiseUtil,
+ dateUtil,
+ mathUtil,
+ langUtil,
+} from "kr-corekit";
+
+const id = stringUtil.camelCase("user profile id");
+const rows = arrayUtil.chunk([1, 2, 3, 4, 5], 2);
+const city = objectUtil.get({ user: { profile: { city: "Seoul" } } }, "user.profile.city");
+
+const mapped = await asyncUtil.mapAsync([1, 2, 3], async (v) => v * 2);
+const safe = await promiseUtil.withTimeout(fetch("/api/health"), 1000);
+
+const tomorrow = dateUtil.addDays(new Date(), 1);
+const average = mathUtil.mean([10, 20, 30]);
+const enabled = langUtil.toBoolean("yes");
+```
+
+## 模块
+
+- `stringUtil`: 大小写转换、HTML escape/unescape、truncate、slugify
+- `arrayUtil`: chunk、flatten、uniq/uniqBy、groupBy、sortBy、集合运算、抽样
+- `collectionUtil`: 面向数组/对象的 map/filter/reduce/find/every/some/includes
+- `objectUtil`: get/set/has/merge/defaults/pick/omit/deepClone/deepFreeze
+- `numberUtil`: clamp、inRange、random、ceil/floor/round、sum/subtract/multiply
+- `mathUtil`: mean/median/min/max/sumBy/minBy/maxBy
+- `dateUtil`: 日期加减、当天起止、formatDate、日期比较
+- `langUtil`: toBoolean/toNumber/toString/defaultTo/castArray/isEqual
+- `asyncUtil`: pLimit、mapAsync、filterAsync、eachAsync、series、parallel
+- `promiseUtil`: defer、withTimeout、retryWithDelay、settle、toResult
+- 其他模块: `commonUtil`, `functionUtil`, `validationUtil`, `formatUtil`, `typeUtil`, `cookieUtil`, `deviceUtil`, `searchQueryUtil`
+
+## Tree-Shaking 导入
+
+```ts
+import { camelCase } from "kr-corekit/stringUtil";
+import { chunk } from "kr-corekit/arrayUtil";
+import { get } from "kr-corekit/objectUtil";
+import { mapAsync } from "kr-corekit/asyncUtil";
+import { withTimeout } from "kr-corekit/promiseUtil";
+```
+
+## 完整 API 示例
+
+- 所有公开 API 的示例请查看 [`docs/API_EXAMPLES.md`](./docs/API_EXAMPLES.md)。
+
+## 基准测试
+
+```bash
+npm run benchmark
+```
+
+该命令会在构建后执行本地微基准脚本 [`benchmark/index.mjs`](./benchmark/index.mjs)。
+
+最新本地样例(2026-02-25):
+
+```text
+array.chunk ~1,065,050 ops/s
+object.get ~2,681,055 ops/s
+string.camelCase ~1,902,407 ops/s
+async.mapAsync ~1,514,005 ops/s
+```
+
+## 说明
+
+- 完整 API 可查看 `package/*/index.ts` 与 `dist/types/*/index.d.ts`。
+- 若继续扩展函数文档,请同步更新多语言 README。
+
+## 许可证
+
+MIT
diff --git a/benchmark/index.mjs b/benchmark/index.mjs
new file mode 100644
index 0000000..4f71ed8
--- /dev/null
+++ b/benchmark/index.mjs
@@ -0,0 +1,70 @@
+import { performance } from "node:perf_hooks";
+import * as corekit from "../dist/bundle/kr-corekit.js";
+
+const {
+ chunk,
+ flattenDeep,
+ get,
+ merge,
+ camelCase,
+ isEqual,
+ map,
+ mean,
+ mapAsync,
+} = corekit;
+
+function run(name, fn, iterations = 20000) {
+ fn();
+ const startedAt = performance.now();
+
+ for (let index = 0; index < iterations; index++) {
+ fn();
+ }
+
+ const elapsed = performance.now() - startedAt;
+ const opsPerSec = Math.round((iterations / elapsed) * 1000);
+
+ return { name, iterations, elapsedMs: elapsed.toFixed(2), opsPerSec };
+}
+
+async function runAsync(name, fn, iterations = 2000) {
+ await fn();
+ const startedAt = performance.now();
+
+ for (let index = 0; index < iterations; index++) {
+ await fn();
+ }
+
+ const elapsed = performance.now() - startedAt;
+ const opsPerSec = Math.round((iterations / elapsed) * 1000);
+
+ return { name, iterations, elapsedMs: elapsed.toFixed(2), opsPerSec };
+}
+
+const sampleArray = Array.from({ length: 1000 }, (_, index) => index);
+const nestedArray = [1, [2, [3, [4, [5, [6]]]]]];
+const deepObject = { user: { profile: { address: { city: "Seoul" } } } };
+const mergeTarget = { a: { b: 1 }, c: [1, 2, 3] };
+const mergeSource = { a: { d: 2 }, c: [3, 4], e: true };
+
+const syncResults = [
+ run("array.chunk", () => chunk(sampleArray, 25)),
+ run("array.flattenDeep", () => flattenDeep(nestedArray)),
+ run("object.get", () => get(deepObject, "user.profile.address.city")),
+ run("object.merge", () => merge({ ...mergeTarget }, mergeSource), 5000),
+ run("string.camelCase", () => camelCase("kr corekit benchmark sample")),
+ run("lang.isEqual", () => isEqual({ x: [1, 2] }, { x: [1, 2] }), 10000),
+ run("collection.map", () => map({ a: 1, b: 2, c: 3 }, (v) => v * 2)),
+ run("math.mean", () => mean([1, 2, 3, 4, 5])),
+];
+
+const asyncResults = [
+ await runAsync(
+ "async.mapAsync",
+ () => mapAsync([1, 2, 3, 4], async (value) => value * 2),
+ 1000
+ ),
+];
+
+console.log("Benchmark Results");
+console.table([...syncResults, ...asyncResults]);
diff --git a/docs/API_EXAMPLES.md b/docs/API_EXAMPLES.md
new file mode 100644
index 0000000..bc8dfe6
--- /dev/null
+++ b/docs/API_EXAMPLES.md
@@ -0,0 +1,495 @@
+# kr-corekit API Examples (All Public APIs)
+
+현재 공개된 API(총 134개)에 대한 사용 예제입니다.
+
+## stringUtil
+
+```ts
+import {
+ camelCase,
+ capitalize,
+ escapeHtml,
+ kebabCase,
+ pascalCase,
+ slugify,
+ snakeCase,
+ truncate,
+ unescapeHtml,
+} from "kr-corekit/stringUtil";
+
+camelCase("hello world"); // "helloWorld"
+capitalize("hELLO"); // "Hello"
+escapeHtml("hello
"); // "<div>hello</div>"
+kebabCase("helloWorld"); // "hello-world"
+pascalCase("hello world"); // "HelloWorld"
+slugify("Hello World 안녕"); // "hello-world-안녕"
+snakeCase("helloWorld"); // "hello_world"
+truncate("abcdefghijklmnopqrstuvwxyz", { length: 10 }); // "abcdefg..."
+unescapeHtml("<div>hello</div>"); // "hello
"
+```
+
+## arrayUtil
+
+```ts
+import {
+ chunk,
+ compact,
+ difference,
+ first,
+ flatten,
+ flattenDeep,
+ groupBy,
+ intersection,
+ keyBy,
+ last,
+ partition,
+ sample,
+ sampleSize,
+ shuffle,
+ sortBy,
+ union,
+ uniqBy,
+ unique,
+ unzip,
+ zip,
+} from "kr-corekit/arrayUtil";
+
+chunk([1, 2, 3, 4, 5], 2); // [[1,2], [3,4], [5]]
+compact([0, 1, false, 2, "", 3, null, undefined]); // [1,2,3]
+difference([1, 2, 3], [2]); // [1,3]
+first([10, 20, 30]); // 10
+flatten([1, [2, 3], [4]]); // [1,2,3,4]
+flattenDeep([1, [2, [3, [4]]]]); // [1,2,3,4]
+groupBy([{ id: 1, role: "admin" }, { id: 2, role: "user" }], (v) => v.role);
+intersection([1, 2, 3], [2, 4]); // [2]
+keyBy([{ id: 1, name: "Kim" }, { id: 2, name: "Lee" }], (v) => v.id);
+last([10, 20, 30]); // 30
+partition([1, 2, 3, 4], (n) => n % 2 === 0); // [[2,4], [1,3]]
+sample([1, 2, 3]); // 1 | 2 | 3
+sampleSize([1, 2, 3, 4], 2); // 예: [3,1]
+shuffle([1, 2, 3]); // 예: [2,1,3]
+sortBy([{ id: 3 }, { id: 1 }, { id: 2 }], (v) => v.id); // [{id:1},{id:2},{id:3}]
+union([1, 2], [2, 3], [3, 4]); // [1,2,3,4]
+uniqBy([{ id: 1 }, { id: 1 }, { id: 2 }], (v) => v.id); // [{id:1},{id:2}]
+unique([1, 2, 2, 3]); // [1,2,3]
+unzip([
+ ["a", 1],
+ ["b", 2],
+]); // [["a","b"], [1,2]]
+zip(["a", "b"], [1, 2], [true, false]); // [["a",1,true], ["b",2,false]]
+```
+
+## collectionUtil
+
+```ts
+import {
+ entries,
+ every,
+ filter,
+ find,
+ forEach,
+ includes,
+ keys,
+ map,
+ reduce,
+ size,
+ some,
+ values,
+} from "kr-corekit/collectionUtil";
+
+entries({ a: 1, b: 2 }); // [["a",1], ["b",2]]
+every([2, 4, 6], (v) => v % 2 === 0); // true
+filter({ a: 1, b: 2, c: 3 }, (v) => v > 1); // [2,3]
+find([1, 2, 3], (v) => v > 1); // 2
+forEach({ a: 1, b: 2 }, (value, key) => console.log(key, value));
+includes("hello world", "world"); // true
+includes([1, 2, 3], 2); // true
+keys({ a: 1, b: 2 }); // ["a", "b"]
+map({ a: 1, b: 2 }, (value, key) => `${key}:${value}`); // ["a:1", "b:2"]
+reduce([1, 2, 3], (acc, v) => acc + v, 0); // 6
+size(new Set([1, 2, 3])); // 3
+some([1, 2, 3], (v) => v > 2); // true
+values({ a: 1, b: 2 }); // [1,2]
+```
+
+## objectUtil
+
+```ts
+import {
+ clearNullProperties,
+ defaults,
+ deepClone,
+ deepFreeze,
+ get,
+ has,
+ invert,
+ mapValues,
+ merge,
+ omit,
+ pick,
+ removeKey,
+ set,
+} from "kr-corekit/objectUtil";
+
+clearNullProperties({ a: 1, b: null, c: undefined, d: { e: null, f: 2 } });
+defaults({ a: 1, b: undefined }, { b: 2, c: 3 }); // {a:1,b:2,c:3}
+deepClone({ user: { name: "Kim" }, items: [1, 2, 3] });
+deepFreeze({ config: { enabled: true } });
+get({ user: { profile: [{ city: "Seoul" }] } }, "user.profile[0].city", "N/A"); // "Seoul"
+has({ a: { b: 1 } }, "a.b"); // true
+invert({ a: "x", b: "y" }); // {x:"a", y:"b"}
+mapValues({ a: 1, b: 2 }, (v) => v * 10); // {a:10,b:20}
+merge({ a: { b: 1 }, arr: [1, 2] }, { a: { c: 2 }, arr: [3] });
+omit({ a: 1, b: 2, c: 3 }, ["b"]); // {a:1,c:3}
+pick({ a: 1, b: 2, c: 3 }, ["a", "c"]); // {a:1,c:3}
+removeKey("b", { a: 1, b: 2, c: 3 }); // {a:1,c:3}
+set({}, "user.profile[0].city", "Seoul"); // { user: { profile: [{ city: "Seoul" }] } }
+```
+
+## asyncUtil
+
+```ts
+import {
+ eachAsync,
+ filterAsync,
+ mapAsync,
+ parallel,
+ pLimit,
+ series,
+} from "kr-corekit/asyncUtil";
+
+await eachAsync([1, 2, 3], async (v) => {
+ console.log(v);
+});
+
+await filterAsync([1, 2, 3, 4], async (v) => v % 2 === 0); // [2,4]
+await mapAsync([1, 2, 3], async (v) => v * 2, { concurrency: 2 }); // [2,4,6]
+
+await parallel([
+ async () => 1,
+ async () => 2,
+ async () => 3,
+]); // [1,2,3]
+
+const limit = pLimit(2);
+await Promise.all([
+ limit(async () => "A"),
+ limit(async () => "B"),
+ limit(async () => "C"),
+]);
+
+await series([
+ async () => "step1",
+ async () => "step2",
+]); // ["step1", "step2"]
+```
+
+## promiseUtil
+
+```ts
+import {
+ defer,
+ retryWithDelay,
+ settle,
+ toResult,
+ withTimeout,
+} from "kr-corekit/promiseUtil";
+
+const d = defer();
+setTimeout(() => d.resolve(10), 100);
+await d.promise; // 10
+
+let attempt = 0;
+await retryWithDelay(async () => {
+ attempt++;
+ if (attempt < 3) throw new Error("temporary");
+ return "ok";
+}, { retries: 5, delay: 50, factor: 2 });
+
+await settle([
+ Promise.resolve(1),
+ Promise.reject(new Error("fail")),
+ Promise.resolve(2),
+]); // { fulfilled: [1,2], rejected: [Error] }
+
+await toResult(Promise.resolve("ok")); // { data: "ok", error: null }
+await toResult(Promise.reject(new Error("fail"))); // { data: null, error: Error }
+
+await withTimeout(
+ new Promise((resolve) => setTimeout(() => resolve("late"), 300)),
+ 100,
+ { fallback: () => "fallback" }
+); // "fallback"
+```
+
+## dateUtil
+
+```ts
+import {
+ addDays,
+ addHours,
+ differenceInDays,
+ endOfDay,
+ formatDate,
+ isAfter,
+ isBefore,
+ isSameDay,
+ startOfDay,
+ subDays,
+ subHours,
+} from "kr-corekit/dateUtil";
+
+addDays(new Date("2025-01-01"), 7);
+addHours(new Date("2025-01-01T00:00:00"), 5);
+differenceInDays("2025-01-10", "2025-01-01"); // 9
+endOfDay(new Date());
+formatDate(new Date(), "YYYY/MM/DD HH:mm:ss");
+isAfter("2025-01-02", "2025-01-01"); // true
+isBefore("2025-01-01", "2025-01-02"); // true
+isSameDay("2025-01-01", "2025-01-01T23:59:59"); // true
+startOfDay(new Date());
+subDays(new Date("2025-01-10"), 3);
+subHours(new Date("2025-01-01T10:00:00"), 2);
+```
+
+## langUtil
+
+```ts
+import {
+ castArray,
+ defaultTo,
+ isEqual,
+ toBoolean,
+ toNumber,
+ toString,
+} from "kr-corekit/langUtil";
+
+castArray(1); // [1]
+castArray([1, 2]); // [1,2]
+defaultTo(undefined, "fallback"); // "fallback"
+isEqual({ a: [1, 2] }, { a: [1, 2] }); // true
+toBoolean("yes"); // true
+toBoolean("false"); // false
+toNumber("42", 0); // 42
+toNumber("x", 0); // 0
+toString(null); // ""
+toString(123); // "123"
+```
+
+## mathUtil
+
+```ts
+import {
+ max,
+ maxBy,
+ mean,
+ meanBy,
+ median,
+ min,
+ minBy,
+ sumBy,
+} from "kr-corekit/mathUtil";
+
+max([1, 5, 3]); // 5
+maxBy([{ score: 10 }, { score: 20 }], (v) => v.score); // { score: 20 }
+mean([10, 20, 30]); // 20
+meanBy([{ v: 2 }, { v: 4 }], (x) => x.v); // 3
+median([1, 2, 3, 4]); // 2.5
+min([1, 5, 3]); // 1
+minBy([{ score: 10 }, { score: 20 }], (v) => v.score); // { score: 10 }
+sumBy([{ price: 100 }, { price: 200 }], (item) => item.price); // 300
+```
+
+## numberUtil
+
+```ts
+import {
+ ceil,
+ clamp,
+ floor,
+ inRange,
+ multiply,
+ random,
+ round,
+ subtract,
+ sum,
+} from "kr-corekit/numberUtil";
+
+ceil(1.234, 2); // 1.24
+clamp(10, 0, 5); // 5
+floor(1.239, 2); // 1.23
+inRange(3, 1, 5); // true
+multiply(2, 3, 4); // 24
+random(1, 10); // 1~10 정수
+round(1.235, 2); // 1.24
+subtract(10, 3, 2); // 5
+sum(1, 2, 3); // 6
+```
+
+## commonUtil
+
+```ts
+import {
+ copyToClipboard,
+ debounce,
+ decodeBase64,
+ encodeBase64,
+ isEmpty,
+ isNull,
+ retry,
+ sleep,
+ storage,
+ throttle,
+} from "kr-corekit/commonUtil";
+
+isEmpty(""); // true
+isNull(null); // true
+await sleep(100);
+await copyToClipboard("hello");
+
+const encoded = encodeBase64("Hello 한글");
+decodeBase64(encoded); // "Hello 한글"
+
+const debounced = debounce(() => console.log("run"), 300);
+const throttled = throttle(() => console.log("run"), 300);
+
+debounced();
+throttled();
+
+await retry(async () => {
+ return "success";
+}, 3);
+
+storage.set("user", { id: 1, name: "Kim" });
+storage.get<{ id: number; name: string }>("user");
+storage.remove("user");
+```
+
+## functionUtil
+
+```ts
+import {
+ compose,
+ identity,
+ memoize,
+ noop,
+ once,
+ pipe,
+} from "kr-corekit/functionUtil";
+
+compose(
+ (v) => Number(v) * 2,
+ (v) => Number(v) + 1
+)(3); // 8
+
+identity({ a: 1 }); // { a: 1 }
+
+const squareMemo = memoize((n: number) => n * n);
+squareMemo(5); // 25
+squareMemo(5); // 캐시 결과
+
+noop("ignored", 123); // undefined
+
+const initialize = once(() => "initialized");
+initialize(); // "initialized"
+initialize(); // "initialized"
+
+pipe(
+ (v) => Number(v) + 1,
+ (v) => Number(v) * 2
+)(3); // 8
+```
+
+## formatUtil
+
+```ts
+import {
+ formatNumberWithCommas,
+ formatPhoneNumber,
+} from "kr-corekit/formatUtil";
+
+formatPhoneNumber("01012345678"); // "010-1234-5678"
+formatNumberWithCommas(1234567); // "1,234,567"
+```
+
+## typeUtil
+
+```ts
+import {
+ isArray,
+ isBoolean,
+ isDate,
+ isFunction,
+ isNil,
+ isNumber,
+ isPlainObject,
+ isString,
+} from "kr-corekit/typeUtil";
+
+isArray([1, 2]); // true
+isBoolean(false); // true
+isDate(new Date()); // true
+isFunction(() => null); // true
+isNil(undefined); // true
+isNumber(10); // true
+isPlainObject({ a: 1 }); // true
+isString("hello"); // true
+```
+
+## validationUtil
+
+```ts
+import {
+ checkBase64,
+ checkDomain,
+ checkEmail,
+ checkHttpUrl,
+ checkPassword,
+} from "kr-corekit/validationUtil";
+
+checkEmail("user@example.com"); // true
+checkHttpUrl("https://example.com"); // true
+checkDomain("example.com"); // true
+checkBase64("aGVsbG8="); // true
+checkPassword("Abc123!@#", {
+ minLength: 8,
+ requireUppercase: true,
+ requireLowercase: true,
+ requireNumber: true,
+ requireSpecialChar: true,
+}); // true
+```
+
+## cookieUtil
+
+```ts
+import { getCookie, setCookie } from "kr-corekit/cookieUtil";
+
+setCookie("theme", "dark", { path: "/", maxAge: 3600 });
+getCookie("theme"); // "dark"
+```
+
+## deviceUtil
+
+```ts
+import { getDevice } from "kr-corekit/deviceUtil";
+
+getDevice();
+// { isMobile, isTablet, isDesktop, isIOS, isAndroid }
+```
+
+## searchQueryUtil
+
+```ts
+import { getAllQuery } from "kr-corekit/searchQueryUtil";
+
+getAllQuery();
+// 예: { page: "1", tag: ["typescript", "utility"] }
+```
+
+---
+
+참고:
+
+- 브라우저 환경 의존 API: `copyToClipboard`, `storage`, `getCookie`, `setCookie`, `getAllQuery`, `getDevice`
+- 비동기 API는 `await` 또는 `then/catch` 패턴으로 사용하세요.
diff --git a/package.json b/package.json
index 6776b43..646512a 100644
--- a/package.json
+++ b/package.json
@@ -35,21 +35,56 @@
"require": "./dist/types/stringUtil/index.js",
"import": "./dist/types/stringUtil/index.js"
},
+ "./arrayUtil": {
+ "types": "./dist/types/arrayUtil/index.d.ts",
+ "require": "./dist/types/arrayUtil/index.js",
+ "import": "./dist/types/arrayUtil/index.js"
+ },
+ "./collectionUtil": {
+ "types": "./dist/types/collectionUtil/index.d.ts",
+ "require": "./dist/types/collectionUtil/index.js",
+ "import": "./dist/types/collectionUtil/index.js"
+ },
"./objectUtil": {
"types": "./dist/types/objectUtil/index.d.ts",
"require": "./dist/types/objectUtil/index.js",
"import": "./dist/types/objectUtil/index.js"
},
+ "./asyncUtil": {
+ "types": "./dist/types/asyncUtil/index.d.ts",
+ "require": "./dist/types/asyncUtil/index.js",
+ "import": "./dist/types/asyncUtil/index.js"
+ },
"./cookieUtil": {
"types": "./dist/types/cookieUtil/index.d.ts",
"require": "./dist/types/cookieUtil/index.js",
"import": "./dist/types/cookieUtil/index.js"
},
+ "./dateUtil": {
+ "types": "./dist/types/dateUtil/index.d.ts",
+ "require": "./dist/types/dateUtil/index.js",
+ "import": "./dist/types/dateUtil/index.js"
+ },
+ "./langUtil": {
+ "types": "./dist/types/langUtil/index.d.ts",
+ "require": "./dist/types/langUtil/index.js",
+ "import": "./dist/types/langUtil/index.js"
+ },
+ "./mathUtil": {
+ "types": "./dist/types/mathUtil/index.d.ts",
+ "require": "./dist/types/mathUtil/index.js",
+ "import": "./dist/types/mathUtil/index.js"
+ },
"./numberUtil": {
"types": "./dist/types/numberUtil/index.d.ts",
"require": "./dist/types/numberUtil/index.js",
"import": "./dist/types/numberUtil/index.js"
},
+ "./promiseUtil": {
+ "types": "./dist/types/promiseUtil/index.d.ts",
+ "require": "./dist/types/promiseUtil/index.js",
+ "import": "./dist/types/promiseUtil/index.js"
+ },
"./validationUtil": {
"types": "./dist/types/validationUtil/index.d.ts",
"require": "./dist/types/validationUtil/index.js",
@@ -60,6 +95,11 @@
"require": "./dist/types/commonUtil/index.js",
"import": "./dist/types/commonUtil/index.js"
},
+ "./functionUtil": {
+ "types": "./dist/types/functionUtil/index.d.ts",
+ "require": "./dist/types/functionUtil/index.js",
+ "import": "./dist/types/functionUtil/index.js"
+ },
"./searchQueryUtil": {
"types": "./dist/types/searchQueryUtil/index.d.ts",
"require": "./dist/types/searchQueryUtil/index.js",
@@ -83,6 +123,7 @@
},
"scripts": {
"build": "tsc && vite build",
+ "benchmark": "npm run build && node benchmark/index.mjs",
"build:analyze": "npm run build && npm run analyze:bundle",
"analyze:bundle": "echo 'Bundle Analysis:' && du -h dist/bundle/* && echo '\\nGzipped sizes:' && gzip -c dist/bundle/kr-corekit.js | wc -c | awk '{print \"ESM: \" $1/1024 \" KB\"}' && gzip -c dist/bundle/kr-corekit.cjs | wc -c | awk '{print \"CJS: \" $1/1024 \" KB\"}'",
"pack:preview": "npm pack --dry-run",
diff --git a/package/arrayUtil/chunk/index.test.ts b/package/arrayUtil/chunk/index.test.ts
new file mode 100644
index 0000000..d4e01d6
--- /dev/null
+++ b/package/arrayUtil/chunk/index.test.ts
@@ -0,0 +1,17 @@
+import { describe, expect, test } from "vitest";
+import chunk from ".";
+
+describe("chunk 유틸 함수 테스트", () => {
+ test("배열을 지정된 크기로 분할한다.", () => {
+ expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
+ });
+
+ test("size가 1보다 작거나 유효하지 않으면 1로 처리한다.", () => {
+ expect(chunk([1, 2, 3], 0)).toEqual([[1], [2], [3]]);
+ expect(chunk([1, 2, 3], Number.NaN)).toEqual([[1], [2], [3]]);
+ });
+
+ test("빈 배열은 빈 배열을 반환한다.", () => {
+ expect(chunk([], 3)).toEqual([]);
+ });
+});
diff --git a/package/arrayUtil/chunk/index.ts b/package/arrayUtil/chunk/index.ts
new file mode 100644
index 0000000..93161bf
--- /dev/null
+++ b/package/arrayUtil/chunk/index.ts
@@ -0,0 +1,14 @@
+export default function chunk(array: readonly T[], size: number = 1): T[][] {
+ if (array.length === 0) {
+ return [];
+ }
+
+ const normalizedSize = Number.isFinite(size) ? Math.max(1, Math.floor(size)) : 1;
+ const result: T[][] = [];
+
+ for (let i = 0; i < array.length; i += normalizedSize) {
+ result.push(array.slice(i, i + normalizedSize));
+ }
+
+ return result;
+}
diff --git a/package/arrayUtil/compact/index.test.ts b/package/arrayUtil/compact/index.test.ts
new file mode 100644
index 0000000..e278287
--- /dev/null
+++ b/package/arrayUtil/compact/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import compact from ".";
+
+describe("compact 유틸 함수 테스트", () => {
+ test("falsy 값을 제거한다.", () => {
+ expect(compact([0, 1, false, 2, "", 3, null, undefined])).toEqual([1, 2, 3]);
+ });
+
+ test("truthy 값은 유지한다.", () => {
+ expect(compact(["a", "b", "c"])).toEqual(["a", "b", "c"]);
+ });
+});
diff --git a/package/arrayUtil/compact/index.ts b/package/arrayUtil/compact/index.ts
new file mode 100644
index 0000000..fe56fde
--- /dev/null
+++ b/package/arrayUtil/compact/index.ts
@@ -0,0 +1,7 @@
+type Falsy = false | 0 | 0n | "" | null | undefined;
+
+export default function compact(
+ array: readonly T[]
+): Array> {
+ return array.filter(Boolean) as Array>;
+}
diff --git a/package/arrayUtil/difference/index.test.ts b/package/arrayUtil/difference/index.test.ts
new file mode 100644
index 0000000..e67d675
--- /dev/null
+++ b/package/arrayUtil/difference/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import difference from ".";
+
+describe("difference 유틸 함수 테스트", () => {
+ test("두 번째 배열에 없는 값만 반환한다.", () => {
+ expect(difference([1, 2, 3, 4], [2, 4])).toEqual([1, 3]);
+ });
+
+ test("제외 배열이 비어 있으면 원본 복사본을 반환한다.", () => {
+ expect(difference([1, 2], [])).toEqual([1, 2]);
+ });
+});
diff --git a/package/arrayUtil/difference/index.ts b/package/arrayUtil/difference/index.ts
new file mode 100644
index 0000000..5c50b01
--- /dev/null
+++ b/package/arrayUtil/difference/index.ts
@@ -0,0 +1,4 @@
+export default function difference(array: readonly T[], values: readonly T[]): T[] {
+ const excludeSet = new Set(values);
+ return array.filter((item) => !excludeSet.has(item));
+}
diff --git a/package/arrayUtil/first/index.test.ts b/package/arrayUtil/first/index.test.ts
new file mode 100644
index 0000000..b14e673
--- /dev/null
+++ b/package/arrayUtil/first/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import first from ".";
+
+describe("first 유틸 함수 테스트", () => {
+ test("첫 번째 값을 반환한다.", () => {
+ expect(first([1, 2, 3])).toBe(1);
+ });
+
+ test("빈 배열은 undefined를 반환한다.", () => {
+ expect(first([])).toBeUndefined();
+ });
+});
diff --git a/package/arrayUtil/first/index.ts b/package/arrayUtil/first/index.ts
new file mode 100644
index 0000000..d67a7c4
--- /dev/null
+++ b/package/arrayUtil/first/index.ts
@@ -0,0 +1,3 @@
+export default function first(array: readonly T[]): T | undefined {
+ return array[0];
+}
diff --git a/package/arrayUtil/flatten/index.test.ts b/package/arrayUtil/flatten/index.test.ts
new file mode 100644
index 0000000..0d964c7
--- /dev/null
+++ b/package/arrayUtil/flatten/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import flatten from ".";
+
+describe("flatten 유틸 함수 테스트", () => {
+ test("1단계 중첩 배열을 평탄화한다.", () => {
+ expect(flatten([1, [2, 3], [4]])).toEqual([1, 2, 3, 4]);
+ });
+
+ test("빈 배열은 빈 배열을 반환한다.", () => {
+ expect(flatten([])).toEqual([]);
+ });
+});
diff --git a/package/arrayUtil/flatten/index.ts b/package/arrayUtil/flatten/index.ts
new file mode 100644
index 0000000..ea4f5f2
--- /dev/null
+++ b/package/arrayUtil/flatten/index.ts
@@ -0,0 +1,3 @@
+export default function flatten(array: readonly (T | readonly T[])[]): T[] {
+ return array.flat(1) as T[];
+}
diff --git a/package/arrayUtil/flattenDeep/index.test.ts b/package/arrayUtil/flattenDeep/index.test.ts
new file mode 100644
index 0000000..d1d998e
--- /dev/null
+++ b/package/arrayUtil/flattenDeep/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import flattenDeep from ".";
+
+describe("flattenDeep 유틸 함수 테스트", () => {
+ test("모든 중첩 배열을 평탄화한다.", () => {
+ expect(flattenDeep([1, [2, [3, [4]]]])).toEqual([1, 2, 3, 4]);
+ });
+
+ test("원시 값이 섞여 있어도 동작한다.", () => {
+ expect(flattenDeep(["a", [1, ["b"]]])).toEqual(["a", 1, "b"]);
+ });
+});
diff --git a/package/arrayUtil/flattenDeep/index.ts b/package/arrayUtil/flattenDeep/index.ts
new file mode 100644
index 0000000..d28dddf
--- /dev/null
+++ b/package/arrayUtil/flattenDeep/index.ts
@@ -0,0 +1,16 @@
+function flatDeep(array: readonly unknown[], result: T[]): T[] {
+ array.forEach((value) => {
+ if (Array.isArray(value)) {
+ flatDeep(value, result);
+ return;
+ }
+
+ result.push(value as T);
+ });
+
+ return result;
+}
+
+export default function flattenDeep(array: readonly unknown[]): T[] {
+ return flatDeep(array, []);
+}
diff --git a/package/arrayUtil/groupBy/index.test.ts b/package/arrayUtil/groupBy/index.test.ts
new file mode 100644
index 0000000..819b550
--- /dev/null
+++ b/package/arrayUtil/groupBy/index.test.ts
@@ -0,0 +1,24 @@
+import { describe, expect, test } from "vitest";
+import groupBy from ".";
+
+describe("groupBy 유틸 함수 테스트", () => {
+ test("iteratee 결과 기준으로 그룹화한다.", () => {
+ const users = [
+ { id: 1, role: "admin" },
+ { id: 2, role: "user" },
+ { id: 3, role: "admin" },
+ ];
+
+ expect(groupBy(users, (user) => user.role)).toEqual({
+ admin: [
+ { id: 1, role: "admin" },
+ { id: 3, role: "admin" },
+ ],
+ user: [{ id: 2, role: "user" }],
+ });
+ });
+
+ test("빈 배열은 빈 객체를 반환한다.", () => {
+ expect(groupBy([], (value) => value)).toEqual({});
+ });
+});
diff --git a/package/arrayUtil/groupBy/index.ts b/package/arrayUtil/groupBy/index.ts
new file mode 100644
index 0000000..186ffc5
--- /dev/null
+++ b/package/arrayUtil/groupBy/index.ts
@@ -0,0 +1,18 @@
+export default function groupBy(
+ array: readonly T[],
+ iteratee: (value: T, index: number, array: readonly T[]) => K
+): Record {
+ const result = {} as Record;
+
+ array.forEach((value, index) => {
+ const key = iteratee(value, index, array);
+
+ if (!Object.prototype.hasOwnProperty.call(result, key)) {
+ result[key] = [];
+ }
+
+ result[key].push(value);
+ });
+
+ return result;
+}
diff --git a/package/arrayUtil/index.ts b/package/arrayUtil/index.ts
new file mode 100644
index 0000000..78a8ce7
--- /dev/null
+++ b/package/arrayUtil/index.ts
@@ -0,0 +1,20 @@
+export { default as chunk } from "./chunk";
+export { default as compact } from "./compact";
+export { default as unique } from "./unique";
+export { default as uniqBy } from "./uniqBy";
+export { default as groupBy } from "./groupBy";
+export { default as keyBy } from "./keyBy";
+export { default as flatten } from "./flatten";
+export { default as flattenDeep } from "./flattenDeep";
+export { default as difference } from "./difference";
+export { default as intersection } from "./intersection";
+export { default as union } from "./union";
+export { default as zip } from "./zip";
+export { default as unzip } from "./unzip";
+export { default as first } from "./first";
+export { default as last } from "./last";
+export { default as shuffle } from "./shuffle";
+export { default as sample } from "./sample";
+export { default as sampleSize } from "./sampleSize";
+export { default as partition } from "./partition";
+export { default as sortBy } from "./sortBy";
diff --git a/package/arrayUtil/intersection/index.test.ts b/package/arrayUtil/intersection/index.test.ts
new file mode 100644
index 0000000..8bdd2db
--- /dev/null
+++ b/package/arrayUtil/intersection/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import intersection from ".";
+
+describe("intersection 유틸 함수 테스트", () => {
+ test("교집합 값을 반환한다.", () => {
+ expect(intersection([1, 2, 2, 3], [2, 3, 4])).toEqual([2, 3]);
+ });
+
+ test("교집합이 없으면 빈 배열을 반환한다.", () => {
+ expect(intersection([1, 2], [3, 4])).toEqual([]);
+ });
+});
diff --git a/package/arrayUtil/intersection/index.ts b/package/arrayUtil/intersection/index.ts
new file mode 100644
index 0000000..f27df37
--- /dev/null
+++ b/package/arrayUtil/intersection/index.ts
@@ -0,0 +1,16 @@
+export default function intersection(
+ first: readonly T[],
+ second: readonly T[]
+): T[] {
+ const secondSet = new Set(second);
+ const seen = new Set();
+
+ return first.filter((value) => {
+ if (!secondSet.has(value) || seen.has(value)) {
+ return false;
+ }
+
+ seen.add(value);
+ return true;
+ });
+}
diff --git a/package/arrayUtil/keyBy/index.test.ts b/package/arrayUtil/keyBy/index.test.ts
new file mode 100644
index 0000000..2b6c379
--- /dev/null
+++ b/package/arrayUtil/keyBy/index.test.ts
@@ -0,0 +1,27 @@
+import { describe, expect, test } from "vitest";
+import keyBy from ".";
+
+describe("keyBy 유틸 함수 테스트", () => {
+ test("iteratee 결과를 key로 객체를 생성한다.", () => {
+ const users = [
+ { id: 1, name: "Kim" },
+ { id: 2, name: "Lee" },
+ ];
+
+ expect(keyBy(users, (user) => user.id)).toEqual({
+ 1: { id: 1, name: "Kim" },
+ 2: { id: 2, name: "Lee" },
+ });
+ });
+
+ test("같은 key가 있으면 마지막 값으로 덮어쓴다.", () => {
+ const users = [
+ { id: 1, name: "Kim" },
+ { id: 1, name: "Park" },
+ ];
+
+ expect(keyBy(users, (user) => user.id)).toEqual({
+ 1: { id: 1, name: "Park" },
+ });
+ });
+});
diff --git a/package/arrayUtil/keyBy/index.ts b/package/arrayUtil/keyBy/index.ts
new file mode 100644
index 0000000..5d9d9df
--- /dev/null
+++ b/package/arrayUtil/keyBy/index.ts
@@ -0,0 +1,13 @@
+export default function keyBy(
+ array: readonly T[],
+ iteratee: (value: T, index: number, array: readonly T[]) => K
+): Record {
+ const result = {} as Record;
+
+ array.forEach((value, index) => {
+ const key = iteratee(value, index, array);
+ result[key] = value;
+ });
+
+ return result;
+}
diff --git a/package/arrayUtil/last/index.test.ts b/package/arrayUtil/last/index.test.ts
new file mode 100644
index 0000000..9001679
--- /dev/null
+++ b/package/arrayUtil/last/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import last from ".";
+
+describe("last 유틸 함수 테스트", () => {
+ test("마지막 값을 반환한다.", () => {
+ expect(last([1, 2, 3])).toBe(3);
+ });
+
+ test("빈 배열은 undefined를 반환한다.", () => {
+ expect(last([])).toBeUndefined();
+ });
+});
diff --git a/package/arrayUtil/last/index.ts b/package/arrayUtil/last/index.ts
new file mode 100644
index 0000000..2b09ca6
--- /dev/null
+++ b/package/arrayUtil/last/index.ts
@@ -0,0 +1,7 @@
+export default function last(array: readonly T[]): T | undefined {
+ if (array.length === 0) {
+ return undefined;
+ }
+
+ return array[array.length - 1];
+}
diff --git a/package/arrayUtil/partition/index.test.ts b/package/arrayUtil/partition/index.test.ts
new file mode 100644
index 0000000..916db60
--- /dev/null
+++ b/package/arrayUtil/partition/index.test.ts
@@ -0,0 +1,15 @@
+import { describe, expect, test } from "vitest";
+import partition from ".";
+
+describe("partition 유틸 함수 테스트", () => {
+ test("조건에 따라 배열을 두 그룹으로 분리한다.", () => {
+ expect(partition([1, 2, 3, 4], (value) => value % 2 === 0)).toEqual([
+ [2, 4],
+ [1, 3],
+ ]);
+ });
+
+ test("모든 값이 참이면 두 번째 배열은 비어 있다.", () => {
+ expect(partition([1, 2], () => true)).toEqual([[1, 2], []]);
+ });
+});
diff --git a/package/arrayUtil/partition/index.ts b/package/arrayUtil/partition/index.ts
new file mode 100644
index 0000000..4dfe331
--- /dev/null
+++ b/package/arrayUtil/partition/index.ts
@@ -0,0 +1,18 @@
+export default function partition(
+ array: readonly T[],
+ predicate: (value: T, index: number, array: readonly T[]) => boolean
+): [T[], T[]] {
+ const truthy: T[] = [];
+ const falsy: T[] = [];
+
+ array.forEach((value, index) => {
+ if (predicate(value, index, array)) {
+ truthy.push(value);
+ return;
+ }
+
+ falsy.push(value);
+ });
+
+ return [truthy, falsy];
+}
diff --git a/package/arrayUtil/sample/index.test.ts b/package/arrayUtil/sample/index.test.ts
new file mode 100644
index 0000000..c56e469
--- /dev/null
+++ b/package/arrayUtil/sample/index.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, test, vi } from "vitest";
+import sample from ".";
+
+describe("sample 유틸 함수 테스트", () => {
+ test("랜덤 요소 하나를 반환한다.", () => {
+ vi.spyOn(Math, "random").mockReturnValue(0.6);
+
+ expect(sample([10, 20, 30])).toBe(20);
+
+ vi.restoreAllMocks();
+ });
+
+ test("빈 배열은 undefined를 반환한다.", () => {
+ expect(sample([])).toBeUndefined();
+ });
+});
diff --git a/package/arrayUtil/sample/index.ts b/package/arrayUtil/sample/index.ts
new file mode 100644
index 0000000..7b03add
--- /dev/null
+++ b/package/arrayUtil/sample/index.ts
@@ -0,0 +1,8 @@
+export default function sample(array: readonly T[]): T | undefined {
+ if (array.length === 0) {
+ return undefined;
+ }
+
+ const index = Math.floor(Math.random() * array.length);
+ return array[index];
+}
diff --git a/package/arrayUtil/sampleSize/index.test.ts b/package/arrayUtil/sampleSize/index.test.ts
new file mode 100644
index 0000000..0259405
--- /dev/null
+++ b/package/arrayUtil/sampleSize/index.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, test, vi } from "vitest";
+import sampleSize from ".";
+
+describe("sampleSize 유틸 함수 테스트", () => {
+ test("지정한 개수만큼 랜덤 샘플을 반환한다.", () => {
+ vi.spyOn(Math, "random")
+ .mockReturnValueOnce(0.1)
+ .mockReturnValueOnce(0.5)
+ .mockReturnValueOnce(0.8);
+
+ expect(sampleSize([1, 2, 3, 4], 2)).toEqual([4, 3]);
+
+ vi.restoreAllMocks();
+ });
+
+ test("size가 음수면 빈 배열을 반환한다.", () => {
+ expect(sampleSize([1, 2, 3], -1)).toEqual([]);
+ });
+});
diff --git a/package/arrayUtil/sampleSize/index.ts b/package/arrayUtil/sampleSize/index.ts
new file mode 100644
index 0000000..b6ef2a7
--- /dev/null
+++ b/package/arrayUtil/sampleSize/index.ts
@@ -0,0 +1,10 @@
+import shuffle from "../shuffle";
+
+export default function sampleSize(array: readonly T[], size: number = 1): T[] {
+ if (size <= 0) {
+ return [];
+ }
+
+ const normalizedSize = Math.min(array.length, Math.floor(size));
+ return shuffle(array).slice(0, normalizedSize);
+}
diff --git a/package/arrayUtil/shuffle/index.test.ts b/package/arrayUtil/shuffle/index.test.ts
new file mode 100644
index 0000000..99e444f
--- /dev/null
+++ b/package/arrayUtil/shuffle/index.test.ts
@@ -0,0 +1,23 @@
+import { describe, expect, test, vi } from "vitest";
+import shuffle from ".";
+
+describe("shuffle 유틸 함수 테스트", () => {
+ test("배열 요소를 섞어 반환한다.", () => {
+ vi.spyOn(Math, "random")
+ .mockReturnValueOnce(0.1)
+ .mockReturnValueOnce(0.5)
+ .mockReturnValueOnce(0.8);
+
+ expect(shuffle([1, 2, 3, 4])).toEqual([4, 3, 2, 1]);
+
+ vi.restoreAllMocks();
+ });
+
+ test("원본 배열은 변경하지 않는다.", () => {
+ const source = [1, 2, 3];
+
+ shuffle(source);
+
+ expect(source).toEqual([1, 2, 3]);
+ });
+});
diff --git a/package/arrayUtil/shuffle/index.ts b/package/arrayUtil/shuffle/index.ts
new file mode 100644
index 0000000..5e5aa99
--- /dev/null
+++ b/package/arrayUtil/shuffle/index.ts
@@ -0,0 +1,12 @@
+export default function shuffle(array: readonly T[]): T[] {
+ const result = [...array];
+
+ for (let i = result.length - 1; i > 0; i--) {
+ const randomIndex = Math.floor(Math.random() * (i + 1));
+ const temp = result[i];
+ result[i] = result[randomIndex];
+ result[randomIndex] = temp;
+ }
+
+ return result;
+}
diff --git a/package/arrayUtil/sortBy/index.test.ts b/package/arrayUtil/sortBy/index.test.ts
new file mode 100644
index 0000000..65ab891
--- /dev/null
+++ b/package/arrayUtil/sortBy/index.test.ts
@@ -0,0 +1,27 @@
+import { describe, expect, test } from "vitest";
+import sortBy from ".";
+
+describe("sortBy 유틸 함수 테스트", () => {
+ test("기준 값을 기준으로 오름차순 정렬한다.", () => {
+ const users = [
+ { id: 3, name: "Charlie" },
+ { id: 1, name: "Alice" },
+ { id: 2, name: "Bob" },
+ ];
+
+ expect(sortBy(users, (user) => user.id)).toEqual([
+ { id: 1, name: "Alice" },
+ { id: 2, name: "Bob" },
+ { id: 3, name: "Charlie" },
+ ]);
+ });
+
+ test("원본 배열은 변경하지 않는다.", () => {
+ const values = [3, 1, 2];
+
+ const result = sortBy(values, (value) => value);
+
+ expect(values).toEqual([3, 1, 2]);
+ expect(result).toEqual([1, 2, 3]);
+ });
+});
diff --git a/package/arrayUtil/sortBy/index.ts b/package/arrayUtil/sortBy/index.ts
new file mode 100644
index 0000000..fe2ed81
--- /dev/null
+++ b/package/arrayUtil/sortBy/index.ts
@@ -0,0 +1,33 @@
+type SortValue = string | number | bigint | Date;
+
+function normalizeSortValue(value: SortValue): string | number | bigint {
+ if (value instanceof Date) {
+ return value.getTime();
+ }
+
+ return value;
+}
+
+export default function sortBy(
+ array: readonly T[],
+ iteratee: (value: T, index: number, array: readonly T[]) => SortValue
+): T[] {
+ return array
+ .map((value, index) => ({
+ value,
+ index,
+ criteria: normalizeSortValue(iteratee(value, index, array)),
+ }))
+ .sort((a, b) => {
+ if (a.criteria === b.criteria) {
+ return a.index - b.index;
+ }
+
+ if (typeof a.criteria === "string" || typeof b.criteria === "string") {
+ return String(a.criteria).localeCompare(String(b.criteria));
+ }
+
+ return a.criteria > b.criteria ? 1 : -1;
+ })
+ .map(({ value }) => value);
+}
diff --git a/package/arrayUtil/union/index.test.ts b/package/arrayUtil/union/index.test.ts
new file mode 100644
index 0000000..8a04c8f
--- /dev/null
+++ b/package/arrayUtil/union/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import union from ".";
+
+describe("union 유틸 함수 테스트", () => {
+ test("배열들의 합집합을 순서대로 반환한다.", () => {
+ expect(union([1, 2], [2, 3], [3, 4])).toEqual([1, 2, 3, 4]);
+ });
+
+ test("인자가 없으면 빈 배열을 반환한다.", () => {
+ expect(union()).toEqual([]);
+ });
+});
diff --git a/package/arrayUtil/union/index.ts b/package/arrayUtil/union/index.ts
new file mode 100644
index 0000000..9cd7486
--- /dev/null
+++ b/package/arrayUtil/union/index.ts
@@ -0,0 +1,17 @@
+export default function union(...arrays: ReadonlyArray): T[] {
+ const result: T[] = [];
+ const seen = new Set();
+
+ arrays.forEach((array) => {
+ array.forEach((value) => {
+ if (seen.has(value)) {
+ return;
+ }
+
+ seen.add(value);
+ result.push(value);
+ });
+ });
+
+ return result;
+}
diff --git a/package/arrayUtil/uniqBy/index.test.ts b/package/arrayUtil/uniqBy/index.test.ts
new file mode 100644
index 0000000..85080b9
--- /dev/null
+++ b/package/arrayUtil/uniqBy/index.test.ts
@@ -0,0 +1,21 @@
+import { describe, expect, test } from "vitest";
+import uniqBy from ".";
+
+describe("uniqBy 유틸 함수 테스트", () => {
+ test("기준 값이 중복되면 첫 번째 값만 유지한다.", () => {
+ const users = [
+ { id: 1, name: "Kim" },
+ { id: 1, name: "Park" },
+ { id: 2, name: "Lee" },
+ ];
+
+ expect(uniqBy(users, (user) => user.id)).toEqual([
+ { id: 1, name: "Kim" },
+ { id: 2, name: "Lee" },
+ ]);
+ });
+
+ test("빈 배열은 빈 배열을 반환한다.", () => {
+ expect(uniqBy([], (value) => value)).toEqual([]);
+ });
+});
diff --git a/package/arrayUtil/uniqBy/index.ts b/package/arrayUtil/uniqBy/index.ts
new file mode 100644
index 0000000..83986da
--- /dev/null
+++ b/package/arrayUtil/uniqBy/index.ts
@@ -0,0 +1,17 @@
+export default function uniqBy(
+ array: readonly T[],
+ iteratee: (value: T, index: number, array: readonly T[]) => K
+): T[] {
+ const seen = new Set();
+
+ return array.filter((value, index) => {
+ const key = iteratee(value, index, array);
+
+ if (seen.has(key)) {
+ return false;
+ }
+
+ seen.add(key);
+ return true;
+ });
+}
diff --git a/package/arrayUtil/unique/index.test.ts b/package/arrayUtil/unique/index.test.ts
new file mode 100644
index 0000000..f17b209
--- /dev/null
+++ b/package/arrayUtil/unique/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import unique from ".";
+
+describe("unique 유틸 함수 테스트", () => {
+ test("중복된 값을 제거한다.", () => {
+ expect(unique([1, 2, 2, 3, 3, 3])).toEqual([1, 2, 3]);
+ });
+
+ test("원래 순서를 유지한다.", () => {
+ expect(unique(["b", "a", "b", "c", "a"])).toEqual(["b", "a", "c"]);
+ });
+});
diff --git a/package/arrayUtil/unique/index.ts b/package/arrayUtil/unique/index.ts
new file mode 100644
index 0000000..b7674f3
--- /dev/null
+++ b/package/arrayUtil/unique/index.ts
@@ -0,0 +1,3 @@
+export default function unique(array: readonly T[]): T[] {
+ return [...new Set(array)];
+}
diff --git a/package/arrayUtil/unzip/index.test.ts b/package/arrayUtil/unzip/index.test.ts
new file mode 100644
index 0000000..10183c4
--- /dev/null
+++ b/package/arrayUtil/unzip/index.test.ts
@@ -0,0 +1,20 @@
+import { describe, expect, test } from "vitest";
+import unzip from ".";
+
+describe("unzip 유틸 함수 테스트", () => {
+ test("zip된 배열을 원래 형태로 되돌린다.", () => {
+ expect(
+ unzip([
+ ["a", 1],
+ ["b", 2],
+ ])
+ ).toEqual([
+ ["a", "b"],
+ [1, 2],
+ ]);
+ });
+
+ test("빈 배열은 빈 배열을 반환한다.", () => {
+ expect(unzip([])).toEqual([]);
+ });
+});
diff --git a/package/arrayUtil/unzip/index.ts b/package/arrayUtil/unzip/index.ts
new file mode 100644
index 0000000..987b520
--- /dev/null
+++ b/package/arrayUtil/unzip/index.ts
@@ -0,0 +1,7 @@
+export default function unzip(array: ReadonlyArray): T[][] {
+ const maxLength = array.reduce((max, row) => Math.max(max, row.length), 0);
+
+ return Array.from({ length: maxLength }, (_, index) =>
+ array.map((row) => row[index])
+ );
+}
diff --git a/package/arrayUtil/zip/index.test.ts b/package/arrayUtil/zip/index.test.ts
new file mode 100644
index 0000000..b3f95c9
--- /dev/null
+++ b/package/arrayUtil/zip/index.test.ts
@@ -0,0 +1,15 @@
+import { describe, expect, test } from "vitest";
+import zip from ".";
+
+describe("zip 유틸 함수 테스트", () => {
+ test("같은 인덱스끼리 묶는다.", () => {
+ expect(zip(["a", "b"], [1, 2], [true, false])).toEqual([
+ ["a", 1, true],
+ ["b", 2, false],
+ ]);
+ });
+
+ test("길이가 다르면 undefined로 채운다.", () => {
+ expect(zip([1], [2, 3])).toEqual([[1, 2], [undefined, 3]]);
+ });
+});
diff --git a/package/arrayUtil/zip/index.ts b/package/arrayUtil/zip/index.ts
new file mode 100644
index 0000000..1e735dd
--- /dev/null
+++ b/package/arrayUtil/zip/index.ts
@@ -0,0 +1,7 @@
+export default function zip(...arrays: ReadonlyArray): Array> {
+ const maxLength = arrays.reduce((max, array) => Math.max(max, array.length), 0);
+
+ return Array.from({ length: maxLength }, (_, index) =>
+ arrays.map((array) => array[index])
+ );
+}
diff --git a/package/asyncUtil/eachAsync/index.test.ts b/package/asyncUtil/eachAsync/index.test.ts
new file mode 100644
index 0000000..f066611
--- /dev/null
+++ b/package/asyncUtil/eachAsync/index.test.ts
@@ -0,0 +1,14 @@
+import { describe, expect, test } from "vitest";
+import eachAsync from ".";
+
+describe("async eachAsync 유틸 함수 테스트", () => {
+ test("모든 요소를 순회한다.", async () => {
+ const result: number[] = [];
+
+ await eachAsync([1, 2, 3], async (value) => {
+ result.push(value * 2);
+ });
+
+ expect(result).toEqual([2, 4, 6]);
+ });
+});
diff --git a/package/asyncUtil/eachAsync/index.ts b/package/asyncUtil/eachAsync/index.ts
new file mode 100644
index 0000000..1eaf6f4
--- /dev/null
+++ b/package/asyncUtil/eachAsync/index.ts
@@ -0,0 +1,13 @@
+import mapAsync from "../mapAsync";
+
+interface EachAsyncOptions {
+ concurrency?: number;
+}
+
+export default async function eachAsync(
+ array: readonly T[],
+ iteratee: (value: T, index: number, array: readonly T[]) => Promise | void,
+ options: EachAsyncOptions = {}
+): Promise {
+ await mapAsync(array, iteratee, options);
+}
diff --git a/package/asyncUtil/filterAsync/index.test.ts b/package/asyncUtil/filterAsync/index.test.ts
new file mode 100644
index 0000000..5fcf593
--- /dev/null
+++ b/package/asyncUtil/filterAsync/index.test.ts
@@ -0,0 +1,10 @@
+import { describe, expect, test } from "vitest";
+import filterAsync from ".";
+
+describe("async filterAsync 유틸 함수 테스트", () => {
+ test("비동기 predicate로 필터링한다.", async () => {
+ const result = await filterAsync([1, 2, 3, 4], async (value) => value % 2 === 0);
+
+ expect(result).toEqual([2, 4]);
+ });
+});
diff --git a/package/asyncUtil/filterAsync/index.ts b/package/asyncUtil/filterAsync/index.ts
new file mode 100644
index 0000000..967e156
--- /dev/null
+++ b/package/asyncUtil/filterAsync/index.ts
@@ -0,0 +1,15 @@
+import mapAsync from "../mapAsync";
+
+interface FilterAsyncOptions {
+ concurrency?: number;
+}
+
+export default async function filterAsync(
+ array: readonly T[],
+ predicate: (value: T, index: number, array: readonly T[]) => Promise | boolean,
+ options: FilterAsyncOptions = {}
+): Promise {
+ const matches = await mapAsync(array, predicate, options);
+
+ return array.filter((_, index) => matches[index]);
+}
diff --git a/package/asyncUtil/index.ts b/package/asyncUtil/index.ts
new file mode 100644
index 0000000..9caa0b9
--- /dev/null
+++ b/package/asyncUtil/index.ts
@@ -0,0 +1,6 @@
+export { default as eachAsync } from "./eachAsync";
+export { default as filterAsync } from "./filterAsync";
+export { default as mapAsync } from "./mapAsync";
+export { default as parallel } from "./parallel";
+export { default as pLimit } from "./pLimit";
+export { default as series } from "./series";
diff --git a/package/asyncUtil/mapAsync/index.test.ts b/package/asyncUtil/mapAsync/index.test.ts
new file mode 100644
index 0000000..2fee775
--- /dev/null
+++ b/package/asyncUtil/mapAsync/index.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, test } from "vitest";
+import mapAsync from ".";
+
+describe("async mapAsync 유틸 함수 테스트", () => {
+ test("비동기 iteratee 결과를 매핑한다.", async () => {
+ const result = await mapAsync([1, 2, 3], async (value) => value * 2);
+
+ expect(result).toEqual([2, 4, 6]);
+ });
+
+ test("concurrency 옵션을 적용한다.", async () => {
+ let running = 0;
+ let maxRunning = 0;
+
+ const result = await mapAsync(
+ [1, 2, 3, 4],
+ async (value) => {
+ running++;
+ maxRunning = Math.max(maxRunning, running);
+ await new Promise((resolve) => setTimeout(resolve, 20));
+ running--;
+ return value;
+ },
+ { concurrency: 2 }
+ );
+
+ expect(result).toEqual([1, 2, 3, 4]);
+ expect(maxRunning).toBeLessThanOrEqual(2);
+ });
+});
diff --git a/package/asyncUtil/mapAsync/index.ts b/package/asyncUtil/mapAsync/index.ts
new file mode 100644
index 0000000..2a1d870
--- /dev/null
+++ b/package/asyncUtil/mapAsync/index.ts
@@ -0,0 +1,25 @@
+import pLimit from "../pLimit";
+
+interface MapAsyncOptions {
+ concurrency?: number;
+}
+
+export default async function mapAsync(
+ array: readonly T[],
+ iteratee: (value: T, index: number, array: readonly T[]) => Promise | R,
+ options: MapAsyncOptions = {}
+): Promise {
+ const { concurrency = Number.POSITIVE_INFINITY } = options;
+
+ if (!Number.isFinite(concurrency)) {
+ return Promise.all(array.map((value, index) => iteratee(value, index, array)));
+ }
+
+ const limit = pLimit(concurrency);
+
+ return Promise.all(
+ array.map((value, index) =>
+ limit(async () => iteratee(value, index, array))
+ )
+ );
+}
diff --git a/package/asyncUtil/pLimit/index.test.ts b/package/asyncUtil/pLimit/index.test.ts
new file mode 100644
index 0000000..ffcecf9
--- /dev/null
+++ b/package/asyncUtil/pLimit/index.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, test } from "vitest";
+import pLimit from ".";
+
+describe("async pLimit 유틸 함수 테스트", () => {
+ test("동시 실행 수를 제한한다.", async () => {
+ const limit = pLimit(2);
+
+ let running = 0;
+ let maxRunning = 0;
+
+ const createTask = (delay: number) =>
+ limit(async () => {
+ running++;
+ maxRunning = Math.max(maxRunning, running);
+
+ await new Promise((resolve) => setTimeout(resolve, delay));
+
+ running--;
+ return delay;
+ });
+
+ const results = await Promise.all([
+ createTask(30),
+ createTask(30),
+ createTask(30),
+ createTask(30),
+ ]);
+
+ expect(results).toEqual([30, 30, 30, 30]);
+ expect(maxRunning).toBeLessThanOrEqual(2);
+ });
+});
diff --git a/package/asyncUtil/pLimit/index.ts b/package/asyncUtil/pLimit/index.ts
new file mode 100644
index 0000000..726b9d8
--- /dev/null
+++ b/package/asyncUtil/pLimit/index.ts
@@ -0,0 +1,42 @@
+export type LimitTask = () => Promise;
+
+export default function pLimit(concurrency: number = 1) {
+ const normalizedConcurrency = Math.max(1, Math.floor(concurrency));
+
+ let activeCount = 0;
+ const queue: Array<() => void> = [];
+
+ const runNext = () => {
+ if (activeCount >= normalizedConcurrency || queue.length === 0) {
+ return;
+ }
+
+ const nextTask = queue.shift();
+
+ if (!nextTask) {
+ return;
+ }
+
+ activeCount++;
+ nextTask();
+ };
+
+ const schedule = (task: LimitTask): Promise => {
+ return new Promise((resolve, reject) => {
+ const runTask = () => {
+ task()
+ .then(resolve)
+ .catch(reject)
+ .finally(() => {
+ activeCount--;
+ runNext();
+ });
+ };
+
+ queue.push(runTask);
+ runNext();
+ });
+ };
+
+ return schedule;
+}
diff --git a/package/asyncUtil/parallel/index.test.ts b/package/asyncUtil/parallel/index.test.ts
new file mode 100644
index 0000000..bf5b46e
--- /dev/null
+++ b/package/asyncUtil/parallel/index.test.ts
@@ -0,0 +1,14 @@
+import { describe, expect, test } from "vitest";
+import parallel from ".";
+
+describe("async parallel 유틸 함수 테스트", () => {
+ test("작업을 병렬로 실행한다.", async () => {
+ const result = await parallel([
+ async () => 1,
+ async () => 2,
+ async () => 3,
+ ]);
+
+ expect(result).toEqual([1, 2, 3]);
+ });
+});
diff --git a/package/asyncUtil/parallel/index.ts b/package/asyncUtil/parallel/index.ts
new file mode 100644
index 0000000..13b88fd
--- /dev/null
+++ b/package/asyncUtil/parallel/index.ts
@@ -0,0 +1,5 @@
+export default async function parallel(
+ tasks: ReadonlyArray<() => Promise | T>
+): Promise {
+ return Promise.all(tasks.map((task) => task()));
+}
diff --git a/package/asyncUtil/series/index.test.ts b/package/asyncUtil/series/index.test.ts
new file mode 100644
index 0000000..6dd6509
--- /dev/null
+++ b/package/asyncUtil/series/index.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, test } from "vitest";
+import series from ".";
+
+describe("async series 유틸 함수 테스트", () => {
+ test("작업을 순차적으로 실행한다.", async () => {
+ const order: number[] = [];
+
+ const result = await series([
+ async () => {
+ order.push(1);
+ return 1;
+ },
+ async () => {
+ order.push(2);
+ return 2;
+ },
+ ]);
+
+ expect(order).toEqual([1, 2]);
+ expect(result).toEqual([1, 2]);
+ });
+});
diff --git a/package/asyncUtil/series/index.ts b/package/asyncUtil/series/index.ts
new file mode 100644
index 0000000..d50cc81
--- /dev/null
+++ b/package/asyncUtil/series/index.ts
@@ -0,0 +1,11 @@
+export default async function series(
+ tasks: ReadonlyArray<() => Promise | T>
+): Promise {
+ const results: T[] = [];
+
+ for (const task of tasks) {
+ results.push(await task());
+ }
+
+ return results;
+}
diff --git a/package/collectionUtil/_toEntries.ts b/package/collectionUtil/_toEntries.ts
new file mode 100644
index 0000000..1f2eaff
--- /dev/null
+++ b/package/collectionUtil/_toEntries.ts
@@ -0,0 +1,9 @@
+export type Collection = readonly T[] | Record;
+
+export default function toEntries(collection: Collection): Array<[string, T]> {
+ if (Array.isArray(collection)) {
+ return collection.map((value, index) => [String(index), value]);
+ }
+
+ return Object.entries(collection);
+}
diff --git a/package/collectionUtil/entries/index.test.ts b/package/collectionUtil/entries/index.test.ts
new file mode 100644
index 0000000..7099e97
--- /dev/null
+++ b/package/collectionUtil/entries/index.test.ts
@@ -0,0 +1,15 @@
+import { describe, expect, test } from "vitest";
+import entries from ".";
+
+describe("collection entries 유틸 함수 테스트", () => {
+ test("키-값 쌍 배열을 반환한다.", () => {
+ expect(entries({ a: 1 })).toEqual([["a", 1]]);
+ });
+
+ test("배열은 인덱스 기준 엔트리를 반환한다.", () => {
+ expect(entries(["x", "y"])).toEqual([
+ ["0", "x"],
+ ["1", "y"],
+ ]);
+ });
+});
diff --git a/package/collectionUtil/entries/index.ts b/package/collectionUtil/entries/index.ts
new file mode 100644
index 0000000..211f786
--- /dev/null
+++ b/package/collectionUtil/entries/index.ts
@@ -0,0 +1,5 @@
+import toEntries, { type Collection } from "../_toEntries";
+
+export default function entries(collection: Collection): Array<[string, T]> {
+ return toEntries(collection);
+}
diff --git a/package/collectionUtil/every/index.test.ts b/package/collectionUtil/every/index.test.ts
new file mode 100644
index 0000000..640e232
--- /dev/null
+++ b/package/collectionUtil/every/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import every from ".";
+
+describe("collection every 유틸 함수 테스트", () => {
+ test("모든 값이 조건을 만족하면 true다.", () => {
+ expect(every([2, 4, 6], (value) => value % 2 === 0)).toBe(true);
+ });
+
+ test("하나라도 조건을 만족하지 않으면 false다.", () => {
+ expect(every({ a: 1, b: 2 }, (value) => value > 1)).toBe(false);
+ });
+});
diff --git a/package/collectionUtil/every/index.ts b/package/collectionUtil/every/index.ts
new file mode 100644
index 0000000..c51f15b
--- /dev/null
+++ b/package/collectionUtil/every/index.ts
@@ -0,0 +1,10 @@
+import toEntries, { type Collection } from "../_toEntries";
+
+export default function every(
+ collection: Collection,
+ predicate: (value: T, key: string, collection: Collection) => boolean
+): boolean {
+ return toEntries(collection).every(([key, value]) =>
+ predicate(value, key, collection)
+ );
+}
diff --git a/package/collectionUtil/filter/index.test.ts b/package/collectionUtil/filter/index.test.ts
new file mode 100644
index 0000000..9f52785
--- /dev/null
+++ b/package/collectionUtil/filter/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import filter from ".";
+
+describe("collection filter 유틸 함수 테스트", () => {
+ test("조건에 맞는 값만 반환한다.", () => {
+ expect(filter([1, 2, 3, 4], (value) => value % 2 === 0)).toEqual([2, 4]);
+ });
+
+ test("객체에서도 동작한다.", () => {
+ expect(filter({ a: 1, b: 2, c: 3 }, (value) => value > 1)).toEqual([2, 3]);
+ });
+});
diff --git a/package/collectionUtil/filter/index.ts b/package/collectionUtil/filter/index.ts
new file mode 100644
index 0000000..72423a2
--- /dev/null
+++ b/package/collectionUtil/filter/index.ts
@@ -0,0 +1,10 @@
+import toEntries, { type Collection } from "../_toEntries";
+
+export default function filter(
+ collection: Collection,
+ predicate: (value: T, key: string, collection: Collection) => boolean
+): T[] {
+ return toEntries(collection)
+ .filter(([key, value]) => predicate(value, key, collection))
+ .map(([, value]) => value);
+}
diff --git a/package/collectionUtil/find/index.test.ts b/package/collectionUtil/find/index.test.ts
new file mode 100644
index 0000000..9aa5984
--- /dev/null
+++ b/package/collectionUtil/find/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import find from ".";
+
+describe("collection find 유틸 함수 테스트", () => {
+ test("조건을 만족하는 첫 값을 반환한다.", () => {
+ expect(find([1, 2, 3], (value) => value > 1)).toBe(2);
+ });
+
+ test("값이 없으면 undefined를 반환한다.", () => {
+ expect(find({ a: 1 }, (value) => value > 10)).toBeUndefined();
+ });
+});
diff --git a/package/collectionUtil/find/index.ts b/package/collectionUtil/find/index.ts
new file mode 100644
index 0000000..838fdcb
--- /dev/null
+++ b/package/collectionUtil/find/index.ts
@@ -0,0 +1,12 @@
+import toEntries, { type Collection } from "../_toEntries";
+
+export default function find(
+ collection: Collection,
+ predicate: (value: T, key: string, collection: Collection) => boolean
+): T | undefined {
+ const entry = toEntries(collection).find(([key, value]) =>
+ predicate(value, key, collection)
+ );
+
+ return entry?.[1];
+}
diff --git a/package/collectionUtil/forEach/index.test.ts b/package/collectionUtil/forEach/index.test.ts
new file mode 100644
index 0000000..5503b30
--- /dev/null
+++ b/package/collectionUtil/forEach/index.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, test } from "vitest";
+import forEach from ".";
+
+describe("collection forEach 유틸 함수 테스트", () => {
+ test("배열 순회가 가능하다.", () => {
+ const result: number[] = [];
+ forEach([1, 2, 3], (value) => result.push(value * 2));
+ expect(result).toEqual([2, 4, 6]);
+ });
+
+ test("객체 순회가 가능하다.", () => {
+ const result: string[] = [];
+ forEach({ a: 1, b: 2 }, (value, key) => result.push(`${key}:${value}`));
+ expect(result).toEqual(["a:1", "b:2"]);
+ });
+});
diff --git a/package/collectionUtil/forEach/index.ts b/package/collectionUtil/forEach/index.ts
new file mode 100644
index 0000000..8c783d4
--- /dev/null
+++ b/package/collectionUtil/forEach/index.ts
@@ -0,0 +1,10 @@
+import toEntries, { type Collection } from "../_toEntries";
+
+export default function forEach(
+ collection: Collection,
+ iteratee: (value: T, key: string, collection: Collection) => void
+): void {
+ toEntries(collection).forEach(([key, value]) => {
+ iteratee(value, key, collection);
+ });
+}
diff --git a/package/collectionUtil/includes/index.test.ts b/package/collectionUtil/includes/index.test.ts
new file mode 100644
index 0000000..c8e9393
--- /dev/null
+++ b/package/collectionUtil/includes/index.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, test } from "vitest";
+import includes from ".";
+
+describe("collection includes 유틸 함수 테스트", () => {
+ test("배열/객체 값 포함 여부를 확인한다.", () => {
+ expect(includes([1, 2, 3], 2)).toBe(true);
+ expect(includes({ a: "x", b: "y" }, "z")).toBe(false);
+ });
+
+ test("문자열 포함 여부를 확인한다.", () => {
+ expect(includes("hello world", "world")).toBe(true);
+ });
+});
diff --git a/package/collectionUtil/includes/index.ts b/package/collectionUtil/includes/index.ts
new file mode 100644
index 0000000..c7a073a
--- /dev/null
+++ b/package/collectionUtil/includes/index.ts
@@ -0,0 +1,12 @@
+import toEntries, { type Collection } from "../_toEntries";
+
+export default function includes(
+ collection: Collection | string,
+ target: T | string
+): boolean {
+ if (typeof collection === "string") {
+ return collection.includes(String(target));
+ }
+
+ return toEntries(collection).some(([, value]) => value === target);
+}
diff --git a/package/collectionUtil/index.ts b/package/collectionUtil/index.ts
new file mode 100644
index 0000000..a7d12ad
--- /dev/null
+++ b/package/collectionUtil/index.ts
@@ -0,0 +1,12 @@
+export { default as entries } from "./entries";
+export { default as every } from "./every";
+export { default as filter } from "./filter";
+export { default as find } from "./find";
+export { default as forEach } from "./forEach";
+export { default as includes } from "./includes";
+export { default as keys } from "./keys";
+export { default as map } from "./map";
+export { default as reduce } from "./reduce";
+export { default as size } from "./size";
+export { default as some } from "./some";
+export { default as values } from "./values";
diff --git a/package/collectionUtil/keys/index.test.ts b/package/collectionUtil/keys/index.test.ts
new file mode 100644
index 0000000..de9f785
--- /dev/null
+++ b/package/collectionUtil/keys/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import keys from ".";
+
+describe("collection keys 유틸 함수 테스트", () => {
+ test("키 배열을 반환한다.", () => {
+ expect(keys({ a: 1, b: 2 })).toEqual(["a", "b"]);
+ });
+
+ test("배열은 인덱스 키를 반환한다.", () => {
+ expect(keys([10, 20])).toEqual(["0", "1"]);
+ });
+});
diff --git a/package/collectionUtil/keys/index.ts b/package/collectionUtil/keys/index.ts
new file mode 100644
index 0000000..2d8cfbf
--- /dev/null
+++ b/package/collectionUtil/keys/index.ts
@@ -0,0 +1,9 @@
+import { type Collection } from "../_toEntries";
+
+export default function keys(collection: Collection): string[] {
+ if (Array.isArray(collection)) {
+ return collection.map((_, index) => String(index));
+ }
+
+ return Object.keys(collection);
+}
diff --git a/package/collectionUtil/map/index.test.ts b/package/collectionUtil/map/index.test.ts
new file mode 100644
index 0000000..8619a88
--- /dev/null
+++ b/package/collectionUtil/map/index.test.ts
@@ -0,0 +1,15 @@
+import { describe, expect, test } from "vitest";
+import map from ".";
+
+describe("collection map 유틸 함수 테스트", () => {
+ test("배열을 변환한다.", () => {
+ expect(map([1, 2, 3], (value) => value * 2)).toEqual([2, 4, 6]);
+ });
+
+ test("객체를 변환한다.", () => {
+ expect(map({ a: 1, b: 2 }, (value, key) => `${key}:${value}`)).toEqual([
+ "a:1",
+ "b:2",
+ ]);
+ });
+});
diff --git a/package/collectionUtil/map/index.ts b/package/collectionUtil/map/index.ts
new file mode 100644
index 0000000..a0d5f6e
--- /dev/null
+++ b/package/collectionUtil/map/index.ts
@@ -0,0 +1,10 @@
+import toEntries, { type Collection } from "../_toEntries";
+
+export default function map(
+ collection: Collection,
+ iteratee: (value: T, key: string, collection: Collection) => R
+): R[] {
+ return toEntries(collection).map(([key, value]) =>
+ iteratee(value, key, collection)
+ );
+}
diff --git a/package/collectionUtil/reduce/index.test.ts b/package/collectionUtil/reduce/index.test.ts
new file mode 100644
index 0000000..f3697cb
--- /dev/null
+++ b/package/collectionUtil/reduce/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import reduce from ".";
+
+describe("collection reduce 유틸 함수 테스트", () => {
+ test("누적 계산을 수행한다.", () => {
+ expect(reduce([1, 2, 3], (acc, value) => acc + value, 0)).toBe(6);
+ });
+
+ test("객체에서도 누적 계산을 수행한다.", () => {
+ expect(reduce({ a: 1, b: 2 }, (acc, value, key) => ({ ...acc, [key]: value * 2 }), {} as Record)).toEqual({ a: 2, b: 4 });
+ });
+});
diff --git a/package/collectionUtil/reduce/index.ts b/package/collectionUtil/reduce/index.ts
new file mode 100644
index 0000000..803b1b5
--- /dev/null
+++ b/package/collectionUtil/reduce/index.ts
@@ -0,0 +1,12 @@
+import toEntries, { type Collection } from "../_toEntries";
+
+export default function reduce(
+ collection: Collection,
+ iteratee: (accumulator: R, value: T, key: string, collection: Collection) => R,
+ initialValue: R
+): R {
+ return toEntries(collection).reduce(
+ (accumulator, [key, value]) => iteratee(accumulator, value, key, collection),
+ initialValue
+ );
+}
diff --git a/package/collectionUtil/size/index.test.ts b/package/collectionUtil/size/index.test.ts
new file mode 100644
index 0000000..4d1b82f
--- /dev/null
+++ b/package/collectionUtil/size/index.test.ts
@@ -0,0 +1,15 @@
+import { describe, expect, test } from "vitest";
+import size from ".";
+
+describe("collection size 유틸 함수 테스트", () => {
+ test("배열/객체/문자열 크기를 반환한다.", () => {
+ expect(size([1, 2, 3])).toBe(3);
+ expect(size({ a: 1, b: 2 })).toBe(2);
+ expect(size("abc")).toBe(3);
+ });
+
+ test("Map/Set 크기를 반환한다.", () => {
+ expect(size(new Map([["a", 1]]))).toBe(1);
+ expect(size(new Set([1, 2]))).toBe(2);
+ });
+});
diff --git a/package/collectionUtil/size/index.ts b/package/collectionUtil/size/index.ts
new file mode 100644
index 0000000..11b5147
--- /dev/null
+++ b/package/collectionUtil/size/index.ts
@@ -0,0 +1,13 @@
+import { type Collection } from "../_toEntries";
+
+export default function size(collection: Collection | string | Map | Set): number {
+ if (typeof collection === "string" || Array.isArray(collection)) {
+ return collection.length;
+ }
+
+ if (collection instanceof Map || collection instanceof Set) {
+ return collection.size;
+ }
+
+ return Object.keys(collection).length;
+}
diff --git a/package/collectionUtil/some/index.test.ts b/package/collectionUtil/some/index.test.ts
new file mode 100644
index 0000000..3edaf1c
--- /dev/null
+++ b/package/collectionUtil/some/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import some from ".";
+
+describe("collection some 유틸 함수 테스트", () => {
+ test("하나라도 조건을 만족하면 true다.", () => {
+ expect(some([1, 2, 3], (value) => value === 2)).toBe(true);
+ });
+
+ test("조건을 만족하지 않으면 false다.", () => {
+ expect(some({ a: 1, b: 2 }, (value) => value > 10)).toBe(false);
+ });
+});
diff --git a/package/collectionUtil/some/index.ts b/package/collectionUtil/some/index.ts
new file mode 100644
index 0000000..27d8172
--- /dev/null
+++ b/package/collectionUtil/some/index.ts
@@ -0,0 +1,10 @@
+import toEntries, { type Collection } from "../_toEntries";
+
+export default function some(
+ collection: Collection,
+ predicate: (value: T, key: string, collection: Collection) => boolean
+): boolean {
+ return toEntries(collection).some(([key, value]) =>
+ predicate(value, key, collection)
+ );
+}
diff --git a/package/collectionUtil/values/index.test.ts b/package/collectionUtil/values/index.test.ts
new file mode 100644
index 0000000..3cd2f0f
--- /dev/null
+++ b/package/collectionUtil/values/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import values from ".";
+
+describe("collection values 유틸 함수 테스트", () => {
+ test("값 배열을 반환한다.", () => {
+ expect(values({ a: 1, b: 2 })).toEqual([1, 2]);
+ });
+
+ test("배열은 복사본을 반환한다.", () => {
+ expect(values([1, 2, 3])).toEqual([1, 2, 3]);
+ });
+});
diff --git a/package/collectionUtil/values/index.ts b/package/collectionUtil/values/index.ts
new file mode 100644
index 0000000..ee9fd8b
--- /dev/null
+++ b/package/collectionUtil/values/index.ts
@@ -0,0 +1,9 @@
+import { type Collection } from "../_toEntries";
+
+export default function values(collection: Collection): T[] {
+ if (Array.isArray(collection)) {
+ return [...collection];
+ }
+
+ return Object.values(collection);
+}
diff --git a/package/dateUtil/_toDate.ts b/package/dateUtil/_toDate.ts
new file mode 100644
index 0000000..7fd358a
--- /dev/null
+++ b/package/dateUtil/_toDate.ts
@@ -0,0 +1,5 @@
+export type DateInput = Date | string | number;
+
+export default function toDate(value: DateInput): Date {
+ return value instanceof Date ? new Date(value.getTime()) : new Date(value);
+}
diff --git a/package/dateUtil/addDays/index.test.ts b/package/dateUtil/addDays/index.test.ts
new file mode 100644
index 0000000..3bc0f85
--- /dev/null
+++ b/package/dateUtil/addDays/index.test.ts
@@ -0,0 +1,9 @@
+import { describe, expect, test } from "vitest";
+import addDays from ".";
+
+describe("date addDays 유틸 함수 테스트", () => {
+ test("일수를 더한 Date를 반환한다.", () => {
+ const result = addDays(new Date("2025-01-01T00:00:00.000Z"), 2);
+ expect(result.toISOString()).toBe("2025-01-03T00:00:00.000Z");
+ });
+});
diff --git a/package/dateUtil/addDays/index.ts b/package/dateUtil/addDays/index.ts
new file mode 100644
index 0000000..dc23a5d
--- /dev/null
+++ b/package/dateUtil/addDays/index.ts
@@ -0,0 +1,7 @@
+import toDate, { type DateInput } from "../_toDate";
+
+export default function addDays(date: DateInput, amount: number): Date {
+ const result = toDate(date);
+ result.setDate(result.getDate() + amount);
+ return result;
+}
diff --git a/package/dateUtil/addHours/index.test.ts b/package/dateUtil/addHours/index.test.ts
new file mode 100644
index 0000000..5a427e0
--- /dev/null
+++ b/package/dateUtil/addHours/index.test.ts
@@ -0,0 +1,9 @@
+import { describe, expect, test } from "vitest";
+import addHours from ".";
+
+describe("date addHours 유틸 함수 테스트", () => {
+ test("시간을 더한 Date를 반환한다.", () => {
+ const result = addHours(new Date("2025-01-01T00:00:00.000Z"), 5);
+ expect(result.toISOString()).toBe("2025-01-01T05:00:00.000Z");
+ });
+});
diff --git a/package/dateUtil/addHours/index.ts b/package/dateUtil/addHours/index.ts
new file mode 100644
index 0000000..91ea7ed
--- /dev/null
+++ b/package/dateUtil/addHours/index.ts
@@ -0,0 +1,7 @@
+import toDate, { type DateInput } from "../_toDate";
+
+export default function addHours(date: DateInput, amount: number): Date {
+ const result = toDate(date);
+ result.setHours(result.getHours() + amount);
+ return result;
+}
diff --git a/package/dateUtil/differenceInDays/index.test.ts b/package/dateUtil/differenceInDays/index.test.ts
new file mode 100644
index 0000000..ff51222
--- /dev/null
+++ b/package/dateUtil/differenceInDays/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import differenceInDays from ".";
+
+describe("date differenceInDays 유틸 함수 테스트", () => {
+ test("일수 차이를 반환한다.", () => {
+ expect(differenceInDays("2025-01-03", "2025-01-01")).toBe(2);
+ });
+
+ test("역순이면 음수를 반환한다.", () => {
+ expect(differenceInDays("2025-01-01", "2025-01-03")).toBe(-2);
+ });
+});
diff --git a/package/dateUtil/differenceInDays/index.ts b/package/dateUtil/differenceInDays/index.ts
new file mode 100644
index 0000000..3e2474b
--- /dev/null
+++ b/package/dateUtil/differenceInDays/index.ts
@@ -0,0 +1,10 @@
+import toDate, { type DateInput } from "../_toDate";
+
+const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
+
+export default function differenceInDays(dateLeft: DateInput, dateRight: DateInput): number {
+ const left = toDate(dateLeft).getTime();
+ const right = toDate(dateRight).getTime();
+
+ return Math.trunc((left - right) / MILLISECONDS_IN_DAY);
+}
diff --git a/package/dateUtil/endOfDay/index.test.ts b/package/dateUtil/endOfDay/index.test.ts
new file mode 100644
index 0000000..c079f45
--- /dev/null
+++ b/package/dateUtil/endOfDay/index.test.ts
@@ -0,0 +1,15 @@
+import { describe, expect, test } from "vitest";
+import endOfDay from ".";
+
+describe("date endOfDay 유틸 함수 테스트", () => {
+ test("해당 날짜의 종료 시각을 반환한다.", () => {
+ const result = endOfDay(new Date(2025, 0, 1, 12, 34, 56, 789));
+ expect(result.getFullYear()).toBe(2025);
+ expect(result.getMonth()).toBe(0);
+ expect(result.getDate()).toBe(1);
+ expect(result.getHours()).toBe(23);
+ expect(result.getMinutes()).toBe(59);
+ expect(result.getSeconds()).toBe(59);
+ expect(result.getMilliseconds()).toBe(999);
+ });
+});
diff --git a/package/dateUtil/endOfDay/index.ts b/package/dateUtil/endOfDay/index.ts
new file mode 100644
index 0000000..035381b
--- /dev/null
+++ b/package/dateUtil/endOfDay/index.ts
@@ -0,0 +1,7 @@
+import toDate, { type DateInput } from "../_toDate";
+
+export default function endOfDay(date: DateInput): Date {
+ const result = toDate(date);
+ result.setHours(23, 59, 59, 999);
+ return result;
+}
diff --git a/package/dateUtil/formatDate/index.test.ts b/package/dateUtil/formatDate/index.test.ts
new file mode 100644
index 0000000..3716adf
--- /dev/null
+++ b/package/dateUtil/formatDate/index.test.ts
@@ -0,0 +1,14 @@
+import { describe, expect, test } from "vitest";
+import formatDate from ".";
+
+describe("date formatDate 유틸 함수 테스트", () => {
+ test("기본 포맷으로 날짜를 변환한다.", () => {
+ expect(formatDate(new Date(2025, 0, 2, 3, 4, 5))).toBe("2025-01-02");
+ });
+
+ test("사용자 지정 포맷을 지원한다.", () => {
+ expect(formatDate(new Date(2025, 0, 2, 3, 4, 5), "YYYY/MM/DD HH:mm:ss")).toBe(
+ "2025/01/02 03:04:05"
+ );
+ });
+});
diff --git a/package/dateUtil/formatDate/index.ts b/package/dateUtil/formatDate/index.ts
new file mode 100644
index 0000000..5a8a227
--- /dev/null
+++ b/package/dateUtil/formatDate/index.ts
@@ -0,0 +1,26 @@
+import toDate, { type DateInput } from "../_toDate";
+
+function pad(value: number): string {
+ return String(value).padStart(2, "0");
+}
+
+export default function formatDate(
+ date: DateInput,
+ format: string = "YYYY-MM-DD"
+): string {
+ const value = toDate(date);
+
+ const tokenMap: Record = {
+ YYYY: String(value.getFullYear()),
+ MM: pad(value.getMonth() + 1),
+ DD: pad(value.getDate()),
+ HH: pad(value.getHours()),
+ mm: pad(value.getMinutes()),
+ ss: pad(value.getSeconds()),
+ };
+
+ return Object.entries(tokenMap).reduce(
+ (result, [token, tokenValue]) => result.replaceAll(token, tokenValue),
+ format
+ );
+}
diff --git a/package/dateUtil/index.ts b/package/dateUtil/index.ts
new file mode 100644
index 0000000..ef6562c
--- /dev/null
+++ b/package/dateUtil/index.ts
@@ -0,0 +1,11 @@
+export { default as addDays } from "./addDays";
+export { default as addHours } from "./addHours";
+export { default as differenceInDays } from "./differenceInDays";
+export { default as endOfDay } from "./endOfDay";
+export { default as formatDate } from "./formatDate";
+export { default as isAfter } from "./isAfter";
+export { default as isBefore } from "./isBefore";
+export { default as isSameDay } from "./isSameDay";
+export { default as startOfDay } from "./startOfDay";
+export { default as subDays } from "./subDays";
+export { default as subHours } from "./subHours";
diff --git a/package/dateUtil/isAfter/index.test.ts b/package/dateUtil/isAfter/index.test.ts
new file mode 100644
index 0000000..4dce508
--- /dev/null
+++ b/package/dateUtil/isAfter/index.test.ts
@@ -0,0 +1,8 @@
+import { describe, expect, test } from "vitest";
+import isAfter from ".";
+
+describe("date isAfter 유틸 함수 테스트", () => {
+ test("뒤 시각이면 true를 반환한다.", () => {
+ expect(isAfter("2025-01-03", "2025-01-02")).toBe(true);
+ });
+});
diff --git a/package/dateUtil/isAfter/index.ts b/package/dateUtil/isAfter/index.ts
new file mode 100644
index 0000000..8f2bbbc
--- /dev/null
+++ b/package/dateUtil/isAfter/index.ts
@@ -0,0 +1,5 @@
+import toDate, { type DateInput } from "../_toDate";
+
+export default function isAfter(date: DateInput, compareDate: DateInput): boolean {
+ return toDate(date).getTime() > toDate(compareDate).getTime();
+}
diff --git a/package/dateUtil/isBefore/index.test.ts b/package/dateUtil/isBefore/index.test.ts
new file mode 100644
index 0000000..d94cb65
--- /dev/null
+++ b/package/dateUtil/isBefore/index.test.ts
@@ -0,0 +1,8 @@
+import { describe, expect, test } from "vitest";
+import isBefore from ".";
+
+describe("date isBefore 유틸 함수 테스트", () => {
+ test("앞선 시각이면 true를 반환한다.", () => {
+ expect(isBefore("2025-01-01", "2025-01-02")).toBe(true);
+ });
+});
diff --git a/package/dateUtil/isBefore/index.ts b/package/dateUtil/isBefore/index.ts
new file mode 100644
index 0000000..2354326
--- /dev/null
+++ b/package/dateUtil/isBefore/index.ts
@@ -0,0 +1,5 @@
+import toDate, { type DateInput } from "../_toDate";
+
+export default function isBefore(date: DateInput, compareDate: DateInput): boolean {
+ return toDate(date).getTime() < toDate(compareDate).getTime();
+}
diff --git a/package/dateUtil/isSameDay/index.test.ts b/package/dateUtil/isSameDay/index.test.ts
new file mode 100644
index 0000000..d8ceda7
--- /dev/null
+++ b/package/dateUtil/isSameDay/index.test.ts
@@ -0,0 +1,14 @@
+import { describe, expect, test } from "vitest";
+import isSameDay from ".";
+
+describe("date isSameDay 유틸 함수 테스트", () => {
+ test("같은 날짜면 true를 반환한다.", () => {
+ expect(
+ isSameDay(new Date(2025, 0, 1, 0, 0, 0), new Date(2025, 0, 1, 23, 59, 59))
+ ).toBe(true);
+ });
+
+ test("다른 날짜면 false를 반환한다.", () => {
+ expect(isSameDay("2025-01-01", "2025-01-02")).toBe(false);
+ });
+});
diff --git a/package/dateUtil/isSameDay/index.ts b/package/dateUtil/isSameDay/index.ts
new file mode 100644
index 0000000..81f9126
--- /dev/null
+++ b/package/dateUtil/isSameDay/index.ts
@@ -0,0 +1,12 @@
+import toDate, { type DateInput } from "../_toDate";
+
+export default function isSameDay(date: DateInput, compareDate: DateInput): boolean {
+ const left = toDate(date);
+ const right = toDate(compareDate);
+
+ return (
+ left.getFullYear() === right.getFullYear() &&
+ left.getMonth() === right.getMonth() &&
+ left.getDate() === right.getDate()
+ );
+}
diff --git a/package/dateUtil/startOfDay/index.test.ts b/package/dateUtil/startOfDay/index.test.ts
new file mode 100644
index 0000000..6f1fe53
--- /dev/null
+++ b/package/dateUtil/startOfDay/index.test.ts
@@ -0,0 +1,15 @@
+import { describe, expect, test } from "vitest";
+import startOfDay from ".";
+
+describe("date startOfDay 유틸 함수 테스트", () => {
+ test("해당 날짜의 시작 시각을 반환한다.", () => {
+ const result = startOfDay(new Date(2025, 0, 1, 12, 34, 56, 789));
+ expect(result.getFullYear()).toBe(2025);
+ expect(result.getMonth()).toBe(0);
+ expect(result.getDate()).toBe(1);
+ expect(result.getHours()).toBe(0);
+ expect(result.getMinutes()).toBe(0);
+ expect(result.getSeconds()).toBe(0);
+ expect(result.getMilliseconds()).toBe(0);
+ });
+});
diff --git a/package/dateUtil/startOfDay/index.ts b/package/dateUtil/startOfDay/index.ts
new file mode 100644
index 0000000..1b31dfd
--- /dev/null
+++ b/package/dateUtil/startOfDay/index.ts
@@ -0,0 +1,7 @@
+import toDate, { type DateInput } from "../_toDate";
+
+export default function startOfDay(date: DateInput): Date {
+ const result = toDate(date);
+ result.setHours(0, 0, 0, 0);
+ return result;
+}
diff --git a/package/dateUtil/subDays/index.test.ts b/package/dateUtil/subDays/index.test.ts
new file mode 100644
index 0000000..7228fb2
--- /dev/null
+++ b/package/dateUtil/subDays/index.test.ts
@@ -0,0 +1,9 @@
+import { describe, expect, test } from "vitest";
+import subDays from ".";
+
+describe("date subDays 유틸 함수 테스트", () => {
+ test("일수를 뺀 Date를 반환한다.", () => {
+ const result = subDays(new Date("2025-01-03T00:00:00.000Z"), 2);
+ expect(result.toISOString()).toBe("2025-01-01T00:00:00.000Z");
+ });
+});
diff --git a/package/dateUtil/subDays/index.ts b/package/dateUtil/subDays/index.ts
new file mode 100644
index 0000000..06bddf3
--- /dev/null
+++ b/package/dateUtil/subDays/index.ts
@@ -0,0 +1,7 @@
+import toDate, { type DateInput } from "../_toDate";
+
+export default function subDays(date: DateInput, amount: number): Date {
+ const result = toDate(date);
+ result.setDate(result.getDate() - amount);
+ return result;
+}
diff --git a/package/dateUtil/subHours/index.test.ts b/package/dateUtil/subHours/index.test.ts
new file mode 100644
index 0000000..8eaf055
--- /dev/null
+++ b/package/dateUtil/subHours/index.test.ts
@@ -0,0 +1,9 @@
+import { describe, expect, test } from "vitest";
+import subHours from ".";
+
+describe("date subHours 유틸 함수 테스트", () => {
+ test("시간을 뺀 Date를 반환한다.", () => {
+ const result = subHours(new Date("2025-01-01T05:00:00.000Z"), 5);
+ expect(result.toISOString()).toBe("2025-01-01T00:00:00.000Z");
+ });
+});
diff --git a/package/dateUtil/subHours/index.ts b/package/dateUtil/subHours/index.ts
new file mode 100644
index 0000000..cf8a9b9
--- /dev/null
+++ b/package/dateUtil/subHours/index.ts
@@ -0,0 +1,7 @@
+import toDate, { type DateInput } from "../_toDate";
+
+export default function subHours(date: DateInput, amount: number): Date {
+ const result = toDate(date);
+ result.setHours(result.getHours() - amount);
+ return result;
+}
diff --git a/package/functionUtil/compose/index.test.ts b/package/functionUtil/compose/index.test.ts
new file mode 100644
index 0000000..be1918a
--- /dev/null
+++ b/package/functionUtil/compose/index.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, test } from "vitest";
+import compose from ".";
+
+describe("compose 유틸 함수 테스트", () => {
+ test("오른쪽에서 왼쪽 순서로 함수를 합성한다.", () => {
+ const fn = compose(
+ (value) => Number(value) * 2,
+ (value) => Number(value) + 1
+ );
+
+ expect(fn(3)).toBe(8);
+ });
+});
diff --git a/package/functionUtil/compose/index.ts b/package/functionUtil/compose/index.ts
new file mode 100644
index 0000000..ed12fd8
--- /dev/null
+++ b/package/functionUtil/compose/index.ts
@@ -0,0 +1,6 @@
+type UnaryFunction = (value: unknown) => unknown;
+
+export default function compose(...fns: UnaryFunction[]): UnaryFunction {
+ return (value: unknown) =>
+ fns.reduceRight((acc, fn) => fn(acc), value);
+}
diff --git a/package/functionUtil/identity/index.test.ts b/package/functionUtil/identity/index.test.ts
new file mode 100644
index 0000000..e06f704
--- /dev/null
+++ b/package/functionUtil/identity/index.test.ts
@@ -0,0 +1,11 @@
+import { describe, expect, test } from "vitest";
+import identity from ".";
+
+describe("identity 유틸 함수 테스트", () => {
+ test("입력값을 그대로 반환한다.", () => {
+ const obj = { a: 1 };
+
+ expect(identity(obj)).toBe(obj);
+ expect(identity(123)).toBe(123);
+ });
+});
diff --git a/package/functionUtil/identity/index.ts b/package/functionUtil/identity/index.ts
new file mode 100644
index 0000000..711dac6
--- /dev/null
+++ b/package/functionUtil/identity/index.ts
@@ -0,0 +1,3 @@
+export default function identity(value: T): T {
+ return value;
+}
diff --git a/package/functionUtil/index.ts b/package/functionUtil/index.ts
new file mode 100644
index 0000000..7659cd7
--- /dev/null
+++ b/package/functionUtil/index.ts
@@ -0,0 +1,6 @@
+export { default as compose } from "./compose";
+export { default as identity } from "./identity";
+export { default as memoize } from "./memoize";
+export { default as noop } from "./noop";
+export { default as once } from "./once";
+export { default as pipe } from "./pipe";
diff --git a/package/functionUtil/memoize/index.test.ts b/package/functionUtil/memoize/index.test.ts
new file mode 100644
index 0000000..5fbdd5b
--- /dev/null
+++ b/package/functionUtil/memoize/index.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, test, vi } from "vitest";
+import memoize from ".";
+
+describe("memoize 유틸 함수 테스트", () => {
+ test("같은 키는 캐시된 결과를 반환한다.", () => {
+ const fn = vi.fn((value: number) => value * 2);
+ const memoized = memoize(fn);
+
+ expect(memoized(2)).toBe(4);
+ expect(memoized(2)).toBe(4);
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ test("resolver를 사용해 커스텀 캐시 키를 만들 수 있다.", () => {
+ const fn = vi.fn((a: number, b: number) => a + b);
+ const memoized = memoize(fn, (a, b) => `${a}:${b}`);
+
+ expect(memoized(1, 2)).toBe(3);
+ expect(memoized(1, 2)).toBe(3);
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/package/functionUtil/memoize/index.ts b/package/functionUtil/memoize/index.ts
new file mode 100644
index 0000000..217c72a
--- /dev/null
+++ b/package/functionUtil/memoize/index.ts
@@ -0,0 +1,31 @@
+type Memoized unknown, K> = ((
+ ...args: Parameters
+) => ReturnType) & {
+ cache: Map>;
+};
+
+export default function memoize<
+ T extends (...args: unknown[]) => unknown,
+ K = unknown
+>(
+ fn: T,
+ resolver?: (...args: Parameters) => K
+): Memoized {
+ const cache = new Map>();
+
+ const memoized = ((...args: Parameters) => {
+ const key = resolver ? resolver(...args) : (args[0] as K);
+
+ if (cache.has(key)) {
+ return cache.get(key) as ReturnType;
+ }
+
+ const result = fn(...args) as ReturnType;
+ cache.set(key, result);
+ return result;
+ }) as Memoized;
+
+ memoized.cache = cache;
+
+ return memoized;
+}
diff --git a/package/functionUtil/noop/index.test.ts b/package/functionUtil/noop/index.test.ts
new file mode 100644
index 0000000..f550a7e
--- /dev/null
+++ b/package/functionUtil/noop/index.test.ts
@@ -0,0 +1,8 @@
+import { describe, expect, test } from "vitest";
+import noop from ".";
+
+describe("noop 유틸 함수 테스트", () => {
+ test("항상 undefined를 반환한다.", () => {
+ expect(noop(1, 2, 3)).toBeUndefined();
+ });
+});
diff --git a/package/functionUtil/noop/index.ts b/package/functionUtil/noop/index.ts
new file mode 100644
index 0000000..186b962
--- /dev/null
+++ b/package/functionUtil/noop/index.ts
@@ -0,0 +1 @@
+export default function noop(..._args: unknown[]): void {}
diff --git a/package/functionUtil/once/index.test.ts b/package/functionUtil/once/index.test.ts
new file mode 100644
index 0000000..6676385
--- /dev/null
+++ b/package/functionUtil/once/index.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, test, vi } from "vitest";
+import once from ".";
+
+describe("once 유틸 함수 테스트", () => {
+ test("첫 호출 결과를 캐시하고 이후에는 재실행하지 않는다.", () => {
+ const fn = vi.fn((value: number) => value * 2);
+ const wrapped = once(fn);
+
+ expect(wrapped(2)).toBe(4);
+ expect(wrapped(3)).toBe(4);
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/package/functionUtil/once/index.ts b/package/functionUtil/once/index.ts
new file mode 100644
index 0000000..be1551f
--- /dev/null
+++ b/package/functionUtil/once/index.ts
@@ -0,0 +1,15 @@
+export default function once unknown>(
+ fn: T
+): (...args: Parameters) => ReturnType {
+ let called = false;
+ let result: ReturnType;
+
+ return (...args: Parameters) => {
+ if (!called) {
+ called = true;
+ result = fn(...args) as ReturnType;
+ }
+
+ return result;
+ };
+}
diff --git a/package/functionUtil/pipe/index.test.ts b/package/functionUtil/pipe/index.test.ts
new file mode 100644
index 0000000..7f563cc
--- /dev/null
+++ b/package/functionUtil/pipe/index.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, test } from "vitest";
+import pipe from ".";
+
+describe("pipe 유틸 함수 테스트", () => {
+ test("왼쪽에서 오른쪽 순서로 함수를 합성한다.", () => {
+ const fn = pipe(
+ (value) => Number(value) + 1,
+ (value) => Number(value) * 2
+ );
+
+ expect(fn(3)).toBe(8);
+ });
+});
diff --git a/package/functionUtil/pipe/index.ts b/package/functionUtil/pipe/index.ts
new file mode 100644
index 0000000..edcab24
--- /dev/null
+++ b/package/functionUtil/pipe/index.ts
@@ -0,0 +1,5 @@
+type UnaryFunction = (value: unknown) => unknown;
+
+export default function pipe(...fns: UnaryFunction[]): UnaryFunction {
+ return (value: unknown) => fns.reduce((acc, fn) => fn(acc), value);
+}
diff --git a/package/index.ts b/package/index.ts
index 40a1131..fe9619e 100644
--- a/package/index.ts
+++ b/package/index.ts
@@ -1,10 +1,18 @@
// 네임스페이스에 대한 익스포트 진행
export * as stringUtil from "./stringUtil";
+export * as arrayUtil from "./arrayUtil";
+export * as collectionUtil from "./collectionUtil";
export * as objectUtil from "./objectUtil";
export * as cookieUtil from "./cookieUtil";
+export * as asyncUtil from "./asyncUtil";
+export * as dateUtil from "./dateUtil";
+export * as langUtil from "./langUtil";
+export * as mathUtil from "./mathUtil";
export * as numberUtil from "./numberUtil";
+export * as promiseUtil from "./promiseUtil";
export * as validationUtil from "./validationUtil";
export * as commonUtil from "./commonUtil";
+export * as functionUtil from "./functionUtil";
export * as searchQueryUtil from "./searchQueryUtil";
export * as typeUtil from "./typeUtil";
export * as formatUtil from "./formatUtil";
@@ -12,11 +20,19 @@ export * as deviceUtil from "./deviceUtil";
// 개별 함수에 대한 익스포트 진행
export * from "./stringUtil";
+export * from "./arrayUtil";
+export * from "./collectionUtil";
export * from "./objectUtil";
export * from "./cookieUtil";
+export * from "./asyncUtil";
+export * from "./dateUtil";
+export * from "./langUtil";
+export * from "./mathUtil";
export * from "./numberUtil";
+export * from "./promiseUtil";
export * from "./validationUtil";
export * from "./commonUtil";
+export * from "./functionUtil";
export * from "./searchQueryUtil";
export * from "./typeUtil";
export * from "./formatUtil";
diff --git a/package/langUtil/castArray/index.test.ts b/package/langUtil/castArray/index.test.ts
new file mode 100644
index 0000000..6e95f67
--- /dev/null
+++ b/package/langUtil/castArray/index.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, test } from "vitest";
+import castArray from ".";
+
+describe("lang castArray 유틸 함수 테스트", () => {
+ test("단일 값을 배열로 감싼다.", () => {
+ expect(castArray(1)).toEqual([1]);
+ });
+
+ test("배열 값은 복사본을 반환한다.", () => {
+ expect(castArray([1, 2])).toEqual([1, 2]);
+ });
+});
diff --git a/package/langUtil/castArray/index.ts b/package/langUtil/castArray/index.ts
new file mode 100644
index 0000000..8c3fad2
--- /dev/null
+++ b/package/langUtil/castArray/index.ts
@@ -0,0 +1,3 @@
+export default function castArray(value: T | readonly T[]): T[] {
+ return Array.isArray(value) ? [...value] : [value as T];
+}
diff --git a/package/langUtil/defaultTo/index.test.ts b/package/langUtil/defaultTo/index.test.ts
new file mode 100644
index 0000000..cf53bba
--- /dev/null
+++ b/package/langUtil/defaultTo/index.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, test } from "vitest";
+import defaultTo from ".";
+
+describe("lang defaultTo 유틸 함수 테스트", () => {
+ test("nullish 값이면 기본값을 반환한다.", () => {
+ expect(defaultTo(null, "fallback")).toBe("fallback");
+ expect(defaultTo(undefined, "fallback")).toBe("fallback");
+ });
+
+ test("유효한 값이면 원래 값을 반환한다.", () => {
+ expect(defaultTo("value", "fallback")).toBe("value");
+ });
+});
diff --git a/package/langUtil/defaultTo/index.ts b/package/langUtil/defaultTo/index.ts
new file mode 100644
index 0000000..a7a72fb
--- /dev/null
+++ b/package/langUtil/defaultTo/index.ts
@@ -0,0 +1,11 @@
+export default function defaultTo(value: T, defaultValue: T): T {
+ if (value === null || value === undefined) {
+ return defaultValue;
+ }
+
+ if (typeof value === "number" && Number.isNaN(value)) {
+ return defaultValue;
+ }
+
+ return value;
+}
diff --git a/package/langUtil/index.ts b/package/langUtil/index.ts
new file mode 100644
index 0000000..0d0e367
--- /dev/null
+++ b/package/langUtil/index.ts
@@ -0,0 +1,6 @@
+export { default as castArray } from "./castArray";
+export { default as defaultTo } from "./defaultTo";
+export { default as isEqual } from "./isEqual";
+export { default as toBoolean } from "./toBoolean";
+export { default as toNumber } from "./toNumber";
+export { default as toString } from "./toString";
diff --git a/package/langUtil/isEqual/index.test.ts b/package/langUtil/isEqual/index.test.ts
new file mode 100644
index 0000000..10adc86
--- /dev/null
+++ b/package/langUtil/isEqual/index.test.ts
@@ -0,0 +1,21 @@
+import { describe, expect, test } from "vitest";
+import isEqual from ".";
+
+describe("lang isEqual 유틸 함수 테스트", () => {
+ test("중첩 객체를 깊게 비교한다.", () => {
+ expect(isEqual({ a: { b: 1 } }, { a: { b: 1 } })).toBe(true);
+ });
+
+ test("구조가 다르면 false를 반환한다.", () => {
+ expect(isEqual({ a: 1 }, { a: 2 })).toBe(false);
+ });
+
+ test("순환 참조도 비교한다.", () => {
+ const left: { self?: unknown } = {};
+ const right: { self?: unknown } = {};
+ left.self = left;
+ right.self = right;
+
+ expect(isEqual(left, right)).toBe(true);
+ });
+});
diff --git a/package/langUtil/isEqual/index.ts b/package/langUtil/isEqual/index.ts
new file mode 100644
index 0000000..7f2de84
--- /dev/null
+++ b/package/langUtil/isEqual/index.ts
@@ -0,0 +1,134 @@
+function isObjectLike(value: unknown): value is Record {
+ return typeof value === "object" && value !== null;
+}
+
+function equalArrays(
+ left: readonly unknown[],
+ right: readonly unknown[],
+ stack: WeakMap