배경
프론트엔드 프로젝트를 진행하며 자주 반복되는 데이터 가공 로직(날짜 포맷팅, 유효성 검사, 금액 표기 등)을 매번 새로 구현하는 번거로움 및 의존성 관리를 해결하고자 했습니다.
프로젝트 간의 일관성을 유지하고, 검증된 로직을 재사용함으로써 전체적인 개발 생산성을 높이기 위해 라이브러리화를 진행했습니다.
꾸준히 지속적으로 유지 보수 중입니다.
해결한 문제
- 타입 안정성 확보: 기존 JavaScript 기반 유틸리티의 불확실한 파라미터와 리턴 타입을 TypeScript로 엄격하게 정의하여, 런타임 에러를 방지하고 개발자에게 정확한 타입 힌트를 제공합니다.
- 모듈화 및 확장성: 각 기능을 도메인별(date, number, validation 등)로 분리하여 관리함으로써 필요한 기능만 선택적으로 가져와 사용할 수 있도록 구조화했습니다.
- 검증된 로직 제공: 유틸리티 함수마다 발생할 수 있는 엣지 케이스를 고려하여
Jest기반의 단위 테스트를 작성하였고, 이를 통해 로직의 신뢰도를 높였습니다.
기술적 선택 근거
- TypeScript: 데이터의 흐름을 명확히 파악하고, 컴파일 단계에서 오류를 잡아내어 라이브러리의 안정성을 극대화하기 위해 채택했습니다.
- Babel: 최신 ECMAScript 문법을 다양한 브라우저 및 Node.js 환경에서 동작할 수 있도록 하위 호환성을 보장하기 위해 설정했습니다.
- Jest: 복잡한 설정 없이도 빠르게 테스트 환경을 구축할 수 있으며, 코드 커버리지를 확인하며 누락 없는 테스트를 수행하기에 최적이라 판단했습니다.
- npm 배포:
package.json설정을 통해 모듈의 진입점을 관리하고, npm 생태계를 통해 다른 프로젝트에서 쉽게 설치하여 사용할 수 있도록 구성했습니다.
README
isa-util
A set of JavaScript utilities for use in the browser.
Installation
Using npm
npm install isa-util
Using Yarn
yarn add isa-util
📌 Quick Usage Examples
import {
isArray,
getQuery,
encryptData,
decryptData,
generateSalt,
generatePassword,
generatePasswordWithSaltAndEncrypt,
decryptPasswordWithSalt
} from 'isa-util';
async function demoEncryption() {
const message = 'Secret message';
const { password, salt, encryptedData } = await generatePasswordWithSaltAndEncrypt(16, message);
const decrypted = await decryptPasswordWithSalt(
encryptedData.encryptedData,
encryptedData.iv,
password,
salt
);
console.log(decrypted); // 'Secret message'
}
demoEncryption();
---
✅ Type Checking
isArray([1, 2, 3]); // true
isString('hello'); // true
isObject({ a: 1 }); // true
🔍 Query String Utilities
const query = getQuery();
console.log(query.get('page')); // e.g. '1'
query.set('page', '2');
setQuery(query); // URL 업데이트
💱 Formatting
addComma(1234567); // '1,234,567'
camelCase('@#@assd-wsd_asd fkfk'); // assdWsdAsdFkfk
formatClass(
'a',
{ b: true, c: false },
['d', { e: true, f: 0 }, ['g', ['', null]]],
0,
undefined,
); //a b d e g 0
📦 CDN Script Import
loadCDN('jquery', 'https://code.jquery.com/jquery-3.6.0.min.js');
📁 File Download
const blob = new Blob(['Hello world'], { type: 'text/plain' });
download(blob, 'hello.txt', 'text/plain');
⏳ Debounce & Throttle
const debounced = debounce(() => console.log('called'), 300);
debounced();
debounced(); // 마지막 호출만 실행됨
const throttled = throttle(() => console.log('tick'), 1000);
throttled();
throttled(); // 1초에 한 번만 실행됨
📱 Platform Detection
getPlatform(); // { os: 'iOS', browser: 'Safari', mobile: true }
isMobile(); // true/false
isDarkMode(); // true/false
🔐 Encryption
const salt = generateSalt();
const password = generatePassword(16);
const encrypted = await encryptData('secret', password, salt);
const decrypted = await decryptData(
encrypted.encryptedData,
encrypted.iv,
password,
salt,
);
📅 Date & Time
formatDate(new Date(), 'YYYY-MM-DD'); // '2025-05-13'
timeAgo(new Date(Date.now() - 60000)); // '1 minute ago'
isToday(new Date()); // true
🧠 Storage
setLocalStorage('key', { name: 'isa' });
getLocalStorage('key'); // { name: 'isa' }
removeLocalStorage('key');
const db = await idb.open('my-db', {
version: 1,
schema: {
users: { keyPath: 'id', autoIncrement: true },
},
});
console.log(db.objectStoreNames); // ["users"]
🧩 DOM Utilities
addClass(document.body, 'dark');
hasClass(document.body, 'dark'); // true
toggleClass(document.body, 'dark'); // toggle
🚀 Async Utilities
// FlushQueue
const fq = new FlushQueue();
fq.add('job1', async () => console.log('job 1'));
fq.add('job2', async () => console.log('job 2'));
await fq.flush(); // 병렬 실행
// JobQueue
const jq = new JobQueue<string>(async (job) => {
console.log('processing', job);
await new Promise((r) => setTimeout(r, 300));
});
jq.enqueue('task A');
jq.enqueue('task B'); // 순차 실행
API Documentation
Type Checking
-
isArray(arg: any): booleanChecks if the argument is an array.Usage Example:
isArray([1, 2, 3]); // true isArray('hello'); // false -
isFunction(arg: any): boolean
Checks if the argument is a function.Usage Example:
isFunction(() => {}); // true isFunction(123); // false -
isObject(arg: any): boolean
Checks if the argument is an object.Usage Example:
isObject({ key: 'value' }); // true isObject([1, 2, 3]); // false -
isString(arg: any): boolean
Checks if the argument is a string.Usage Example:
isString('Hello, world!'); // true isString(123); // false -
isNumber(arg: any): boolean
Checks if the argument is a number.Usage Example:
isNumber(42); // true isNumber('Hello'); // false -
isSymbol(arg: any): boolean
Checks if the argument is a symbol.Usage Example:
isSymbol(Symbol('test')); // true isSymbol('not a symbol'); // false -
isBlob(arg: any): boolean
Checks if the argument is a Blob.Usage Example:
isBlob(new Blob()); // true isBlob('not a blob'); // false -
isUndefined(arg: any): boolean
Checks if the argument is undefined.Usage Example:
isUndefined(undefined); // true isUndefined(null); // false -
isFalsy(arg: any): boolean
Checks if the argument is falsy (false, 0, "", null, undefined, NaN).isUndefined(undefined); // true isUndefined(null); // false -
isFalsy(arg: any): boolean
Checks if the argument is falsy (false, 0, "", null, undefined, NaN).Usage Example:
isFalsy(null); // true isFalsy(0); // true isFalsy('hello'); // false -
isTruthy(arg: any): boolean
Checks if the argument is truthy (not falsy).Usage Example:
isTruthy(1); // true isTruthy(''); // false
Query String Utilities
-
getQuery(): URLSearchParamsRetrieves the current URL query parameters as aURLSearchParamsobject.Usage Example:
const queryParams = getQuery(); console.log(queryParams.get('user')); // prints the value of 'user' query parameter -
setQuery(arg: URLSearchParams): void
Sets the URL query parameters using aURLSearchParamsobject.Usage Example:
const params = new URLSearchParams(); params.append('page', '1'); setQuery(params); // Updates the URL's query params to ?page=1
Formatting Utilities
-
addComma(arg: number): stringFormats a number by adding commas as thousand separators.Usage Example:
addComma(1000000); // "1,000,000" -
camelCase(input: string): stringFormats a string by camelCase.Usage Example:
camelCase('@#@assd-wsd_asd fkfk'); // assdWsdAsdFkfk -
pascalCase(input: string): stringFormats a string by pascalCase.Usage Example:
pascalCase('@#@assd-wsd_asd fkfk'); // AssdWsdAsdFkfk -
snakeCase(input: string): stringFormats a string by snakeCase.Usage Example:
snakeCase('@#@assd-wsd_asd fkfk'); // assd_wsd_asd_fkfk -
formatClass(...args: any): string|cx(...args): stringFormats args by className.Usage Example:
formatClass('a', 0, 1); // 'a 0 1'
Script Importing
-
loadCDN(id: string, src: string, options?: ScriptAttribute): Promise<void>Dynamically loads a script from a CDN with optional attributes.Usage Example:
loadCDN( 'lodash', 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js', );
File Exporting
-
download(data: Blob, name: string, type: string): voidTriggers a download for a given Blob with the specified filename and MIME type.Usage Example:
const blob = new Blob(['Hello, world!'], { type: 'text/plain' }); download(blob, 'hello.txt', 'text/plain');
Utility Functions
-
debounce(func: Function, wait: number): Function & { cancel: () => void; pending: () => boolean; }Creates a debounced function that delays invokingfuncuntil afterwaitmilliseconds have passed. Returns a function withcancelandpendingmethods.Usage Example:
const handler = debounce(() => console.log('called!'), 300); handler(); handler(); // Only the last call within 300ms will be executed -
throttle<T extends any[], R>(func: FuncType<T, R>, wait: number): (...args: T) => R | voidCreates a throttled function that only invokesfuncat most once perwaitmilliseconds.Usage Example:
const throttledLog = throttle(() => console.log('Logged!'), 1000); throttledLog(); throttledLog(); // Will log only once per 1000ms -
getPlatform(): { os: string, browser: string, mobile: boolean } | null
Returns an object containing the user's platform information, including the operating system, browser, and whether the user is on a mobile device.Usage Example:
const platform = getPlatform(); console.log(platform.os, platform.browser); // logs current platform info -
JobQueue<T>(processJob: (job: T) => Promise<void>): enqueue(job: T)
A class that manages a queue of asynchronous jobs and processes them one at a time. Jobs are added to the queue using enqueue, and the class ensures they are processed sequentially.Usage Example:
const queue = new JobQueue<string>(async (job) => { console.log(`Processing job: ${job}`); await new Promise((resolve) => setTimeout(resolve, 1000)); // 1초 대기 }); queue.enqueue('Job 1'); queue.enqueue('Job 2'); queue.enqueue('Job 3'); -
FlushQueue(options: FlushQueueOptions): add(id: string, job: AsyncJob)
A class that manages a queue of asynchronous jobs with optional debounce and custom start/finish handlers. Jobs can be added, canceled, and flushed either in parallel or sequentially.Usage Example:
const queue = new FlushQueue({ debounceMs: 300, // Debounce for 300ms before flushing onStart: (id) => console.log(`Starting job ${id}`), onFinish: (id, error) => { if (error) { console.error(`Job ${id} failed`, error); } else { console.log(`Job ${id} finished`); } }, }); const job1: AsyncJob = async () => { console.log('Processing Job 1'); await new Promise((resolve) => setTimeout(resolve, 1000)); }; const job2: AsyncJob = async () => { console.log('Processing Job 2'); await new Promise((resolve) => setTimeout(resolve, 1000)); }; queue.add('job1', job1); queue.add('job2', job2); // Force flush all jobs sequentially queue.flush(false); -
sleep(ms: number): Promise<void>A utility function that pauses the execution for the specified number of milliseconds by returning a Promise that resolves after the given delay.Usage Example:
async function example() { console.log('Start'); await sleep(1000); // Sleep for 1 second console.log('End after 1 second'); } example();
Crypto Utilities
Note: Encryption uses AES-GCM with 256-bit keys derived via PBKDF2 (SHA-256, 100,000 iterations).
encryptData(data: string, password: string, salt: string): Promise<{ iv: number[], encryptedData: number[] }>Encrypts a string using AES-GCM with a password-derived key and returns the IV and encrypted data.
Usage Example:
const { iv, encryptedData } = await encryptData('Hello', 'password', 'salt');
console.log(encryptedData);
-
decryptData(encryptedData: number[], iv: number[], password: string, salt: string): Promise<string>
Decrypts AES-GCM encrypted data using the provided IV, password, and salt.Usage Example:
const decrypted = await decryptData(encryptedData, iv, 'password', 'salt'); console.log(decrypted); // 'Hello' -
generateSalt(): stringGenerates a 16-byte random salt string. -
generatePassword(length: number): stringGenerates a random password of the specified length. -
generatePasswordWithSalt(length: number): { password: string, salt: string }Generates a random password and salt. -
generatePasswordWithSaltAndEncrypt(length: number, data: string): Promise<{ password: string, salt: string, encryptedData: { iv: number[], encryptedData: number[] } }>Generates a password and salt, encrypts the provided data, and returns all components. -
decryptPasswordWithSalt(encryptedData: number[], iv: number[], password: string, salt: string): Promise<string>Decrypts data using the given encryptedData, IV, password, and salt. -
decryptPasswordWithSaltAndEncrypt(encryptedData: number[], iv: number[], password: string, salt: string, data: string): Promise<string>Decrypts and verifies that the decrypted data matches the original input.
Date & Time Utilities
-
formatDate(date: Date, format: string): string
Formats a JavaScriptDateobject into a custom string format like'YYYY-MM-DD HH:mm:ss'.Usage Example:
formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss'); // "2025-05-13 12:34:56" -
timeAgo(date: Date | string): string
Returns a human-readable time difference string like "5 minutes ago" or "2 days ago".Usage Example:
timeAgo(new Date('2025-05-12')); // "1 day ago" -
isToday(date: Date): booleanReturnstrueif the given date is today.Usage Example:
isToday(new Date('2005-05-12')); // "false"
Environment Detection
-
isMobile(): booleanDetects whether the current device is a mobile device. -
isDarkMode(): booleanDetects whether the user's system prefers dark mode. -
isTouchDevice(): booleanChecks if the current device supports touch interactions.
Storage Utilities
-
setLocalStorage(key: string, value: any): void
Sets a value inlocalStorage.Usage Example:
setLocalStorage('user', { name: 'Alice' }); -
getLocalStorage(key: string): any
Retrieves a value fromlocalStorage.Usage Example:
const user = getLocalStorage('user'); console.log(user.name); // "Alice" -
removeLocalStorage(key: string): voidRemoves a value fromlocalStorage.IndexDB - idb
interface IDBOpenOptions: { version?: number; schema?: Record<string, IDBObjectStoreParameters>; } interface CursorOptions<T = any> { range?: IDBKeyRange | null; offset?: number; limit?: number; filter?: (value: T, key: IDBValidKey) => boolean; sort?: (a: T, b: T) => number; } -
idb.open(name: string, options?: IDBOpenOptions): Promise<IDBDatabase>Opens an IndexedDB database, creating object stores if they don’t exist. Usage Example:const db = await idb.open('my-db', { version: 1, schema: { users: { keyPath: 'id', autoIncrement: true }, }, }); console.log(db.objectStoreNames); // ["users"] -
idb.get<T>(dbName: string, store: string, key: IDBValidKey): Promise<T | undefined>Retrieves a single item from the specified object store by key. Usage Example:const user = await idb.get('my-db', 'users', 1); console.log(user?.name); // "Alice" -
idb.put<T>(dbName: string, store: string, value: T): Promise<IDBValidKey>Inserts or updates an item in the object store. Usage Example:const key = await idb.put('my-db', 'users', { name: 'Alice' }); console.log(key); // 1 -
idb.clear(dbName: string, store: string): Promise<void>Removes all items from the specified object store. Usage Example:await idb.clear('my-db', 'users'); -
idb.deleteDB(name: string): Promise<void>Deletes the entire database. Usage Example:await idb.deleteDB('my-db'); -
idb.cursor.each<T>(dbName: string, store: string, callback: (value: T, key: IDBValidKey, cursor: IDBCursorWithValue) => void): Promise<void>Iterates over all items in the store and executes the callback for each. Usage Example:await idb.cursor.each('my-db', 'users', (value, key) => { console.log(key, value.name); }); -
idb.cursor.query<T>(dbName: string, store: string, options?: CursorOptions<T>): Promise<T[]>Advanced cursor query with optionalfilter,sort,offset, andlimit. Usage Example:const users = await idb.cursor.query('my-db', 'users', { filter: (user) => user.age >= 18, sort: (a, b) => a.age - b.age, offset: 1, limit: 2, }); console.log(users);
DOM Utilities
-
hasClass(el: Element, className: string): boolean
Checks if an element contains a class.Usage Example:
hasClass(document.body, 'dark-mode'); // true/false -
addClass(el: Element, className: string): void
Adds a class to an element.Usage Example:
addClass(document.body, 'dark-mode'); -
removeClass(el: Element, className: string): void
Removes a class from an element.Usage Example:
removeClass(document.body, 'dark-mode'); -
toggleClass(el: Element, className: string): voidToggles a class on an element.
License
This project is licensed under the MIT License. For more details, see the LICENSE.