feat: 유틸리티 추가
🕐 커밋 시간: 2025. 08. 13. 13:27:54 📊 변경 통계: • 총 파일: 3개 📁 추가된 파일: + com.twin.app.shoptime/src/utils/fp.js + com.twin.app.shoptime/src/utils/lodash.js + com.twin.app.shoptime/src/utils/lodashFpEx.js 🔧 주요 변경 내용: • 공통 유틸리티 함수 최적화
This commit is contained in:
12
com.twin.app.shoptime/src/utils/fp.js
Normal file
12
com.twin.app.shoptime/src/utils/fp.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// FP bootstrap: use locally-extended lodash instance
|
||||
// './lodash' already mixes in our custom extensions from './lodashFpEx'
|
||||
import fp from './lodash';
|
||||
|
||||
export const {
|
||||
pipe, flow, curry, compose,
|
||||
map, filter, reduce, get, set,
|
||||
isEmpty, isNotEmpty, isNil, isNotNil,
|
||||
mapAsync, reduceAsync, filterAsync,
|
||||
} = fp;
|
||||
|
||||
export default fp;
|
||||
4
com.twin.app.shoptime/src/utils/lodash.js
Normal file
4
com.twin.app.shoptime/src/utils/lodash.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import fp from 'lodash/fp';
|
||||
import fpEx from './lodashFpEx';
|
||||
|
||||
export default fp.mixin(fpEx);
|
||||
468
com.twin.app.shoptime/src/utils/lodashFpEx.js
Normal file
468
com.twin.app.shoptime/src/utils/lodashFpEx.js
Normal file
@@ -0,0 +1,468 @@
|
||||
import fp from 'lodash/fp';
|
||||
|
||||
/**
|
||||
* 대상 인자가 promise(thenable)인지 여부
|
||||
* @param {*} x
|
||||
*/
|
||||
const isPromise = (x) => fp.isFunction(fp.get('then', x)) && fp.isFunction(fp.get('catch', x));
|
||||
|
||||
const fnPromisify = (fn, ...args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
resolve(fn(...args));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 대상 인자를 promise로 lift (like monad lift)
|
||||
*/
|
||||
const promisify = (a, ...args) => {
|
||||
const cond = fp.cond([
|
||||
[fp.isFunction, () => fnPromisify(a, ...args)],
|
||||
[isPromise, fp.identity],
|
||||
[fp.T, (a) => Promise.resolve(a)],
|
||||
]);
|
||||
const result = cond(a);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* promise가 또다른 promise를 resolve하는 경우, promise의 중첩을 제거하기 위한 helper 함수
|
||||
*
|
||||
* @param {*} thenable
|
||||
*/
|
||||
const flatPromise = (thenable) =>
|
||||
isPromise(thenable) ? thenable.then((x) => flatPromise(x)) : thenable;
|
||||
|
||||
/**
|
||||
* lodash 형태의 promise then
|
||||
*/
|
||||
const then = fp.curry((fn, thenable) => promisify(thenable).then(flatPromise(fn)));
|
||||
|
||||
/**
|
||||
* lodash 형태의 promise catch
|
||||
*/
|
||||
const otherwise = fp.curry((fn, thenable) => promisify(thenable).catch(flatPromise(fn)));
|
||||
|
||||
/**
|
||||
* lodash 형태의 promise finally
|
||||
*/
|
||||
const _finally = fp.curry((fn, thenable) => promisify(thenable).finally(flatPromise(fn)));
|
||||
|
||||
/**
|
||||
* invert boolean
|
||||
* @param {*} x
|
||||
*/
|
||||
const not = (x) => !x;
|
||||
|
||||
/**
|
||||
* 대상이 비어있지 않은지 여부
|
||||
*/
|
||||
const isNotEmpty = fp.pipe(fp.isEmpty, not);
|
||||
|
||||
/**
|
||||
* 대상 인자를 boolean 타입으로 변환\
|
||||
* (예외)'true'문자열이면 true, 'false'문자열이면 false
|
||||
*
|
||||
* @param {*} a
|
||||
*/
|
||||
const toBool = (a) =>
|
||||
fp.cond([
|
||||
[fp.equals('true'), fp.T],
|
||||
[fp.equals('false'), fp.F],
|
||||
[fp.T, (a) => !!a],
|
||||
])(a);
|
||||
|
||||
/**
|
||||
* 삼항식 helper 함수\
|
||||
* (isTrue가 true면 t(실행)반환, false면 f(실행)반환)
|
||||
*/
|
||||
const ternary = fp.curry((evaluator, trueHandler, falseHandler, a) => {
|
||||
const executor = fp.curry((t, f, a, isTrue) => {
|
||||
const result = isTrue ? (fp.isFunction(t) ? t(a) : t) : fp.isFunction(f) ? f(a) : f;
|
||||
return result;
|
||||
});
|
||||
const getEvaluator = (fn) => (fp.isNil(fn) ? fp.identity : fn);
|
||||
const result = executor(trueHandler, falseHandler, a, getEvaluator(evaluator)(a));
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* a인자가 t타입인지 여부
|
||||
*/
|
||||
const instanceOf = fp.curry((t, a) => a instanceof t);
|
||||
|
||||
/**
|
||||
* 대상 문자열을 pascalcase 문자열로 변환
|
||||
*/
|
||||
const pascalCase = fp.pipe(fp.camelCase, fp.upperFirst);
|
||||
|
||||
/**
|
||||
* (collection) fp.map의 비동기 함수\
|
||||
* mapper 함수로 비동기 함수를 받아서 처리해준다.
|
||||
*/
|
||||
const mapAsync = fp.curry(async (asyncMapper, arr) => {
|
||||
const composer = fp.pipe(
|
||||
fp.flatMapDeep(fp.pipe(asyncMapper, promisify)),
|
||||
async (a) => await Promise.all(a),
|
||||
);
|
||||
const result = await composer(arr);
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* (collection) fp.filter의 비동기 함수\
|
||||
* 필터함수로 비동기 함수를 받아서 처리해준다.
|
||||
*/
|
||||
const filterAsync = fp.curry(async (asyncFilter, arr) => {
|
||||
const composer = fp.pipe(
|
||||
mapAsync(async (item) => ((await asyncFilter(item)) ? item : false)),
|
||||
then(fp.filter(fp.pipe(fp.equals(false), not))),
|
||||
);
|
||||
const result = await composer(arr);
|
||||
|
||||
return result;
|
||||
});
|
||||
/**
|
||||
* (collection) fp.find의 비동기 함수
|
||||
*/
|
||||
const findAsync = fp.curry(async (asyncFn, arr) => {
|
||||
const composer = fp.pipe(
|
||||
mapAsync(asyncFn),
|
||||
then(fp.indexOf(true)),
|
||||
then((idx) => fp.get(`[${idx}]`, arr)),
|
||||
otherwise(fp.always(undefined)),
|
||||
);
|
||||
const result = await composer(arr);
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* asyncFn의 시작은 await accPromise가 되어야 한다.\
|
||||
* 순차적으로 실행된다.\
|
||||
* (ex 300ms이 걸리는 5개의 promise가 있다면, 최소 1500ms+alpah의 시간이 소요된다.\
|
||||
* 상기의 mapAsync의 경우 300+alpah의 시간만 소요된다.(Promise.all과 Promise.resolve의 차이))
|
||||
*/
|
||||
const reduceAsync = fp.curry((asyncFn, initAcc, dest) => {
|
||||
const initAccPromise = Promise.resolve(initAcc);
|
||||
const result = fp.reduce(asyncFn, initAccPromise, dest);
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* 비동기 forEach
|
||||
* 실행함수로 비동기 함수를 받아서 처리해준다
|
||||
* 순차실행
|
||||
*/
|
||||
const forEachAsync = fp.curry(async (cb, collection) => {
|
||||
const loopResults = [];
|
||||
const iterator = fp.entries(collection);
|
||||
|
||||
for (const e of iterator) {
|
||||
loopResults.push(await cb(e[1], e[0]));
|
||||
}
|
||||
|
||||
return loopResults;
|
||||
});
|
||||
|
||||
/**
|
||||
* value로 object key 조회
|
||||
*/
|
||||
const key = fp.curry((a, v) => {
|
||||
const composer = fp.pipe(
|
||||
fp.entries,
|
||||
fp.find(([k, val]) => fp.equals(v, val)),
|
||||
fp.head,
|
||||
);
|
||||
const result = composer(a);
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* 대상 문자열이 json형식 문자열인지 여부
|
||||
* @param {String} a
|
||||
*/
|
||||
const isJson = (a) => {
|
||||
const composer = fp.pipe(fp.attempt, fp.isError);
|
||||
return fp.isString(a) && !composer(() => JSON.parse(a));
|
||||
};
|
||||
|
||||
/**
|
||||
* shallow freeze 보완
|
||||
* (대상 object의 refence 타입의 properties까지 object.freeze 처리)
|
||||
* @param {*} obj
|
||||
*/
|
||||
const deepFreeze = (obj) => {
|
||||
const freezeRecursively = (v) => (isRef(v) && !Object.isFrozen(v) ? deepFreeze(v) : v);
|
||||
const composer = fp.pipe(Object.freeze, fp.forOwn(freezeRecursively));
|
||||
const result = composer(obj);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const transformObjectKey = fp.curry((transformFn, dest) => {
|
||||
const convertRecursively = (dest) => {
|
||||
const convertTo = (o) => {
|
||||
const composer = fp.pipe(
|
||||
fp.entries,
|
||||
fp.reduce((acc, [k, v]) => {
|
||||
const cond = fp.cond([
|
||||
[fp.isPlainObject, convertTo],
|
||||
[fp.isArray, (v) => v.map(cond)],
|
||||
[fp.T, (a) => a],
|
||||
]);
|
||||
const transformedKey = transformFn(k);
|
||||
if (!fp.has(transformedKey, acc)) {
|
||||
acc[transformedKey] = cond(v);
|
||||
return acc;
|
||||
} else {
|
||||
throw new Error(
|
||||
`${transformedKey} already exist. duplicated property name is not supported.`,
|
||||
);
|
||||
}
|
||||
}, {}),
|
||||
);
|
||||
const result = composer(o);
|
||||
return result;
|
||||
};
|
||||
const result = convertTo(dest);
|
||||
return result;
|
||||
};
|
||||
|
||||
const result = fp.isObject(dest) || fp.isArray(dest) ? convertRecursively(dest) : dest;
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* 대상 object의 property key문자열을 camelcase 문자열로 변환
|
||||
*/
|
||||
const toCamelcase = transformObjectKey(fp.camelCase);
|
||||
|
||||
/**
|
||||
* 대상 object의 property key문자열을 snakecase 문자열로 변환
|
||||
*/
|
||||
const toSnakecase = transformObjectKey(fp.snakeCase);
|
||||
const toPascalcase = transformObjectKey(pascalCase);
|
||||
|
||||
/**
|
||||
* date형식 문자열 여부
|
||||
* @param {string} str date형식 문자열
|
||||
*/
|
||||
const isDatetimeString = (str) => isNaN(str) && !isNaN(Date.parse(str));
|
||||
/**
|
||||
* applicative functor pattern 구현체
|
||||
* (주로 fp.pipe함수에서 함수의 인자 순서를 변경하기 위해 사용)
|
||||
*/
|
||||
const ap = fp.curry((a, curried) => curried(a));
|
||||
|
||||
/**
|
||||
* 대상 인자가 undefined 또는 null이 아닌지 여부
|
||||
*/
|
||||
const isNotNil = fp.pipe(fp.isNil, not);
|
||||
|
||||
/**
|
||||
* a인자를 인자로, evaluator함수 실행,
|
||||
* true면 trueHandler에 a인자 대입
|
||||
* false면 a 반환
|
||||
*/
|
||||
const ifT = fp.curry((evaluator, trueHandler, a) => {
|
||||
const isValidParams = fp.every(fp.isFunction, [evaluator, trueHandler]);
|
||||
|
||||
if (isValidParams) {
|
||||
return fp.pipe(evaluator, fp.equals(true))(a) ? trueHandler(a) : a;
|
||||
} else {
|
||||
throw new Error('invalid parameter');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* a인자를 인자로, evaluator함수 실행,
|
||||
* false면 falseHandler에 a인자 대입
|
||||
* true면 a 반환
|
||||
*/
|
||||
const ifF = fp.curry((evaluator, falseHandler, a) => {
|
||||
const isValidParams = fp.every(fp.isFunction, [evaluator, falseHandler]);
|
||||
|
||||
if (isValidParams) {
|
||||
return fp.pipe(evaluator, fp.equals(false))(a) ? falseHandler(a) : a;
|
||||
} else {
|
||||
throw new Error('invalid parameter(s)');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* arr인자 배열에 a인자가 포함되지 않았는지 여부
|
||||
*/
|
||||
const notIncludes = fp.curry((a, arr) => {
|
||||
const composer = fp.pipe(fp.includes, ap(arr), not);
|
||||
const result = composer(a);
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* a인자와 b인자가 다른지 여부 (deep equal)
|
||||
*/
|
||||
const notEquals = fp.curry((a, b) => fp.pipe(fp.equals(a), not)(b));
|
||||
|
||||
/**
|
||||
* arr인자의 idx인자의 index에 해당하는 요소 제거
|
||||
*/
|
||||
const removeByIndex = fp.curry((idx, arr) => {
|
||||
if (fp.isArray(arr)) {
|
||||
const cloned = fp.cloneDeep(arr);
|
||||
cloned.splice(fp.toNumber(idx), 1);
|
||||
|
||||
return cloned;
|
||||
}
|
||||
return arr;
|
||||
});
|
||||
|
||||
/**
|
||||
* arr 인자의 마지막 요소 제거 (immutable)
|
||||
*
|
||||
* @param {*} arr
|
||||
*/
|
||||
const removeLast = (a) => {
|
||||
const nextA = fp.cloneDeep(a);
|
||||
if (fp.isArray(a)) {
|
||||
nextA.pop();
|
||||
}
|
||||
if (fp.isString(a)) {
|
||||
return nextA.substring(0, fp.size(a) - 1);
|
||||
}
|
||||
return nextA;
|
||||
};
|
||||
|
||||
/**
|
||||
* fp.concat alias
|
||||
*/
|
||||
const append = fp.concat;
|
||||
|
||||
/**
|
||||
* array 인자의 (index상)앞쪽에 value인자를 추가
|
||||
*/
|
||||
const prepend = fp.curry((array, value) =>
|
||||
fp.isArray(value) ? fp.concat(value, array) : fp.concat([value], array),
|
||||
);
|
||||
|
||||
/**
|
||||
* key(index)를 포함한 fp.map
|
||||
*/
|
||||
const mapWithKey = fp.curry((f, a) => fp.map.convert({ cap: false })(f, a));
|
||||
|
||||
/**
|
||||
* key(index)를 포함한 fp.forEach
|
||||
*/
|
||||
const forEachWithKey = fp.curry((f, a) => fp.forEach.convert({ cap: false })(f, a));
|
||||
|
||||
/**
|
||||
* key(index)를 포함한 reduce
|
||||
*/
|
||||
const reduceWithKey = fp.curry((f, acc, a) => fp.reduce.convert({ cap: false })(f, acc, a));
|
||||
|
||||
/**
|
||||
* null, undefined, Boolean, Number, String
|
||||
*
|
||||
*/
|
||||
const isVal = (a) => fp.isNil(a) || fp.isBoolean(a) || fp.isNumber(a) || fp.isString(a);
|
||||
|
||||
/**
|
||||
* Array, Object, Function
|
||||
*/
|
||||
const isRef = fp.pipe(isVal, not);
|
||||
|
||||
const isFalsy = (a) => {
|
||||
return fp.isNil(a) || fp.some(fp.equals(a), [0, -0, NaN, false, '']);
|
||||
};
|
||||
|
||||
const isTruthy = (a) => !isFalsy(a);
|
||||
|
||||
/**
|
||||
* fp.getOr override
|
||||
*
|
||||
* fp.getOr의 반환값이 null인 경우, 기본값 반환되게 수정한 버전
|
||||
* circular dependency 때문에 closure로 작성
|
||||
*/
|
||||
const getOr = (({ curry, getOr }) => {
|
||||
const _getOr = curry((defaultValue, path, target) => {
|
||||
const val = fp.get(path, target);
|
||||
return fp.isNil(val) ? defaultValue : val;
|
||||
});
|
||||
return _getOr;
|
||||
})(fp);
|
||||
|
||||
export default {
|
||||
mapAsync,
|
||||
filterAsync,
|
||||
reduceAsync,
|
||||
findAsync,
|
||||
forEachAsync,
|
||||
promisify,
|
||||
then,
|
||||
andThen: then,
|
||||
otherwise,
|
||||
catch: otherwise,
|
||||
finally: _finally,
|
||||
|
||||
isPromise,
|
||||
isNotEmpty,
|
||||
isNotNil,
|
||||
isJson,
|
||||
notEquals,
|
||||
isNotEqual: notEquals,
|
||||
isVal,
|
||||
isPrimitive: isVal,
|
||||
isRef,
|
||||
isReference: isRef,
|
||||
not,
|
||||
notIncludes,
|
||||
toBool,
|
||||
|
||||
deepFreeze,
|
||||
key,
|
||||
keyByVal: key,
|
||||
|
||||
// string
|
||||
transformObjectKey,
|
||||
toCamelcase,
|
||||
toCamelKey: toCamelcase,
|
||||
toSnakecase,
|
||||
toSnakeKey: toSnakecase,
|
||||
toPascalcase,
|
||||
pascalCase,
|
||||
isDatetimeString,
|
||||
|
||||
ap,
|
||||
instanceOf,
|
||||
|
||||
ternary,
|
||||
ifT,
|
||||
ifF,
|
||||
|
||||
// array
|
||||
removeByIndex,
|
||||
removeByIdx: removeByIndex,
|
||||
removeLast,
|
||||
append,
|
||||
prepend,
|
||||
|
||||
mapWithKey,
|
||||
mapWithIdx: mapWithKey,
|
||||
forEachWithKey,
|
||||
forEachWithIdx: forEachWithKey,
|
||||
reduceWithKey,
|
||||
reduceWithIdx: reduceWithKey,
|
||||
isFalsy,
|
||||
isTruthy,
|
||||
|
||||
getOr,
|
||||
};
|
||||
Reference in New Issue
Block a user