December 13th 2018

이전에 Underscore 라이브러리의 메소드를 구현해 보는 연습을 했습니다. 이번에는 async 라이브러리 중 몇가지 메소드를 구현해 보는 연습을 하겠습니다.
(async 라이브러리 : http://caolan.github.io/async/index.html)
라이브러리의 함수를 만드는 것은 이전의 그것과 비슷하지만, async는 자바스크립트의 비동기 라이브러리이기 때문에 동기적인 패턴에서 비동기적인 패턴으로 생각을 바꾸는 것이 처음에 어려웠습니다.
Contents
async.each 함수는 collection의 요소를 하나씩 꺼내어 iteratee 함수에 인자로 건내줍니다. iteratee 함수는 비동기적으로 작동하기 때문에 먼저 실행된 iteratee가 먼저 완료된다는 보장은 없습니다.
iteratee 함수는 collection의 요소와 iteratee가 완료된 후 실행하는 callback 두 개의 인자를 받습니다. 만약 비동기 함수가 실행 중 에러가 발생하면 이 callback에 error가 전달되고 최종콜백이 실행됩니다.
function each(collection, iteratee, callback) {
let count = 0;
let errorHappened = false;
if (!Array.isArray(collection)) {
collection = Object.keys(collection).map(key => collection[key]);
}
if (!collection.length) {
callback();
}
for (let i = 0; i < collection.length; i++) {
iteratee(collection[i], handleAsyncResult);
}
function handleAsyncResult(err) {
if (!errorHappened) {
if (err) {
errorHappened = true;
callback(err);
} else {
count++;
if (count === collection.length) {
callback(null);
}
}
}
}
}
async.every는 자바스크립트 배열의 every 메소드와 유사합니다. collection의 모든 요소가 주어진 테스트에 통과하면 true를 반환합니다. iteratee의 결과가 한번이라도 false가 나오면 최종콜백이 바로 실행됩니다. 위의 each와 마찬가지로 iteratee는 비동기로 작동하여 어떠한 iteratee가 먼저 완료될 지 예상할 수는 없습니다.
function every(collection, iteratee, callback) {
let count = 0;
let errorHappened = false;
let testResult = true;
if (!Array.isArray(collection)) {
collection = Object.keys(collection).map(key => collection[key]);
}
for (let i = 0; i < collection.length; i++) {
iteratee(collection[i], handleAsyncResult);
}
function handleAsyncResult(err, result) {
if (!errorHappened && testResult) {
if (err) {
errorHappened = true;
callback(err);
} else {
count++;
if (!result) {
testResult = false;
callback(err, testResult);
} else if (count === collection.length) {
callback(err, testResult);
}
}
}
}
}
한번 더 만들어 본 버전
function every(coll, iteratee, callback) {
var terminated = false;
var result = true;
var count = 0;
for (var i = 0; i < coll.length; i++) {
iteratee(coll[i], handleResult);
}
function handleResult(err, truthyFalsy) {
var boolean = !!truthyFalsy;
if (!terminated) {
if (err) {
callback(err);
terminated = true;
return;
}
if (!boolean) {
callback(null, boolean);
terminated = true;
return;
}
count++;
result = boolean;
if (count === coll.length) {
callback(null, result);
}
}
}
}
async.filter 또한 자바스크립트의 Array.prototype.filter와 유사합니다. 인자로 전달 받은 iteratee 함수의 테스트를 통과한 요소만 모은 새로운 배열을 반환합니다. 그리고, 기존의 배열 요소의 순서를 지켜야 합니다.
function filter(collection, iteratee, callback) {
let filteredArray = [];
let errorHappended = false;
let count = 0;
let coll;
if (!Array.isArray(collection)) {
coll = Object.keys(collection).map(key => collection[key]);
} else {
coll = collection.slice();
}
for (let i = 0; i < coll.length; i++) {
iteratee(coll[i], function handleAsyncResult(err, result) {
if (!errorHappened) {
if (err) {
errorHappened = true;
callback(err);
} else {
count++;
if (result) {
filteredArray[i] = coll[i];
}
if (count === coll.length) {
filteredArray = filteredArray.filter(el => el !== undefined);
callback(err, filteredArray);
}
}
}
});
}
}
async.some은 비동기로 작동하는 함수 iteratee의 결과값이 한번이라도 true이면 최종 콜백을 바로 싱행해 주는 함수입니다. iteratee의 인자로는 각 collection의 요소와 iteratee의 결과값을 받는 callback 함수입니다.
function some(collection, iteratee, callback) {
let countInvocation = 0;
let errorHappened = false;
let wasAnythingPassed = false;
if (!Array.isArray(collection)) {
collection = Object.keys(collection).map(key => collection[key]);
}
for (let i = 0; i < collection; i++) {
iteratee(collection[i], handleAsyncResult);
}
function handleAsyncResult(err, result) {
if (!errorHappend) {
if (err) {
errorHappend = true;
callback(err);
} else if (!wasAnythingPassed) {
countInvocation++;
if (result) {
wasAnythingPassed = true;
callback(err, wasAnythingPassed);
} else if (countInvocation === collection.length) {
callback(err, wasAnythingPassed);
}
}
}
}
}
collection의 요소를 전달 받은 비동기 함수 iteratee가 반환하는 새로운 값을 모은 배열을 생성하는 메소드입니다. iteratee에 두번째 인자로 전달하는 callback은 두 개의 argument를 받으며 하나는 error이고, 다른 하나는 iteratee가 변형한 새로운 요소입니다. iteratee 실행 중 에러가 발생하면 바로 최종 callback(async.map의 세번째 인자)을 실행합니다.
각각의 iteratee함수는 비동기적으로 작동하기 때문에 어떠한 함수로부터 먼저 응답을 받을지 확신할 수 없습니다. 하지만, async.map이 반환하는 최종 결과값은 처음에 받은 collection 요소의 순서를 지켜야 합니다.
만약 배열대신 객체를 인자로 전달 받으면, 결과값은 배열로 반환합니다. 결과값은 대체로 Object.keys의 순서로 반환하지만, 이것은 자바스크립트 엔진에 따라 상이할 수 있습니다.
function map(collection, iteratee, callback) {
const mappedArray = [];
let countInvocation = 0;
let errorHappened = false;
let coll;
if (!collection) {
callback(null, mappedArray);
return;
}
if (!Array.isArray(collection)) {
coll = Object.keys(collection).map(key => collection[key]);
} else {
coll = collection.slice();
}
for (let i = 0; i < coll.length; i++) {
iteratee(coll[i], function handleAsyncResult(err, result) {
if (!errorHappened) {
if (err) {
errorHappened = true;
callback(er);
} else {
countInvocation++;
mappedArray[i] = result;
if (countInvocation === coll.length) {
callback(err, mappedArray);
}
}
}
});
}
}
Reduce는 배열 또는 객체인 collection을 하나의 값으로 만들어 주는 함수입니다. 비동기 콜백인 iteratee는 collection의 요소와 기존의 누적값을 전달 받아 실행 후 새로운 값을 반환합니다. Array.prototype.reduce와 유사하지만, iteratee가 비동기적으로 그리고 순차적으로 진행되어야 다음 iteratee에 누적값을 전달할 수 있다는 점을 인지해야 합니다.
function reduce(collection, memo, iteratee, callback) {
let countInvocatioin = 0;
let errorHappened = false;
let coll;
let idx = 0;
if (!Array.isArray(collection)) {
coll = Object.keys(collection).map(key => collection[key]);
} else {
coll = collection.slice();
}
iteratee(memo, coll[idx], handleAsyncResult);
function handleAsyncResult(err, result) {
if (!errorHappened) {
if (err) {
errorHappened = true;
callback(err);
} else {
countInvocation++;
memo = result;
idx++;
if (countInvocation === coll.length) {
callback(err, memo);
} else {
iteratee(memo, coll[idx], handleAsyncResult);
}
}
}
}
}
memoize는 비동기로 진행되는 함수의 결과를 캐싱합니다. 캐싱을 하기 위해 해시를 생성할 때 함수에 전달되는 argument 중 콜백은 제외하고 hash 함수에 전달됩니다. 만약 hasher가 생략된 경우에는 첫 번째 인자를 이용해 해시키를 생성합니다.
function memoize(fn, hasher) {
const cache = {};
return function(...args) {
const callback = args[args.length - 1];
const cbArgs = args.slice(0, args.length - 1);
let key = hasher ? hasher(...args) : JSON.stringify(args[0]);
if (cache[key] !== undefined) {
callback(null, cache[key]);
} else {
fn(...cbArgs, function handleAsyncResult(err, result) {
if (err) {
callback(err);
} else {
cache[key] = result;
callback(null, cache[key]);
}
});
}
};
}
iteratee가 반환한 값을 키로 사용하고 해당 속성에 대한 값은 collection 아이템인 배열로 이루어진 객체를 반환해주는 메소드입니다. 비동기 함수인 iteratee에 collection의 아이템을 전달하면, iteratee는 groupBy가 반환하는 객체의 key값을 리턴합니다.
iteratee는 비동기 함수이기 때문에 호출된 순서대롤 응답을 받는다는 보장이 없습니다. 하지만, 반환되는 객체의 값으로 할당되는 배열은, 원본 collection의 순서를 지켜야 합니다.
function groupBy(collection, iteratee, callback) {
const group = {};
let errorHappened = false;
let invocationCount = 0;
let coll;
if (!Array.isArray(collection)) {
coll = Object.keys(collection).map(key => collection[key]);
} else {
coll = collection.slice();
}
for (let i = 0; i < coll.length; i++) {
iteratee(coll[i], function handleAsyncResult(err, result) {
if (!errorHappened) {
if (err) {
errorHappened = true;
callback(err);
} else {
invocationCount++;
if (group[result] !== undefined) {
group[result][i] = coll[i];
} else {
group[result] = [];
group[result][i] = coll[i];
}
if (invocationCount === coll.length) {
trimArrayOfObject(group);
callback(null, group);
}
}
}
});
}
function trimArrayOfObject(group) {
for (let key in group) {
group[key] = group[key].filter(el => el !== undefined);
}
}
}
첫 번째 인자로 받는 tasks(collection)에 포함된 함수를 순차적으로 실행합니다. 다시 말해서, 각 함수는 이전의 함수가 완료된 다음에 실행이 되어야 합니다.
만약 하나의 함수라도 에러가 발생하면 callback함수가 실행이 되고 그 다음의 task의 요소인 함수는 실행이 되지 않아야 합니다. tasks의 함수가 끝까지 완료가 되면 callback(optional) 함수가 주어진 경우 각 함수의 결과값을 배열로 반환합니다.
function series(tasks, callback) {
let invocationCount = 0;
let i = 0;
if (Array.isArray(tasks)) {
const completedArray = [];
if (tasks.length === 0) {
callback(null, completedArray);
return;
}
invokeInSeries(i);
function invokeInSeries(idx) {
tasks[idx](function(err, ...result) {
if (err) {
callback(err);
return;
} else {
invocationCount++;
if (result.length === 1) {
arrayCompleted.push(result[0]);
} else {
arrayCompleted.push([...result]);
}
if (invocationCount === tasks.length) {
callback(null, completedArray);
} else {
invokeInSeries(++i);
}
}
});
}
} else {
const completedObject = {};
const keys = Object.keys(tasks).map(key => tasks[key]);
invokeInSeries(i);
function invokeInSeries(idx) {
tasks[keys[i]](function(err, ...result) {
if (err) {
callback(err);
break;
} else {
invocationCount++;
if (result.length === 1) {
completedObject[keys[idx]] = result[0];
} else {
completedObject[keys[idx]] = [...result];
}
if (invocationCount === tasks.length) {
callback(null, completedObject);
} else {
invokeInSeries(++i);
}
}
});
}
}
}
인자로 전달 받는 tasks(collection)의 비동기 함수를 위의 series와는 다르게 병렬로 실행합니다. 그리고 이전의 비동기 함수가 완료되기를 기다리지 않습니다. 모든 비동기 함수가 완료되면 결과를 callback에 반환합니다. 도중에 하나의 비동기 함수가 에러를 반환하면 callback을 즉각 실행합니다.
function parallel(tasks, callback) {
let invocationCount = 0;
let errorHappened = false;
if (Array.isArray(tasks)) {
const arrayResult = [];
for (let i = 0; i < tasks.length; i++) {
invokeAsyncFunction(i);
}
function invokeAsyncFunction(idx) {
tasks[idx](function handleAsyncResult(err, ...result) {
if (!errorHappened) {
if (err) {
callback(err);
errorHappended = true;
} else {
invocationCount++;
if (result.length === 1) {
arrayCompleted[idx] = result[0];
} else {
arrayCompleted[idx] = [...result];
}
if (invocationCount === tasks.length) {
callback(null, arrayCompleted);
}
}
}
});
}
} else {
const objectResult = {};
const keys = Object.keys(tasks).map(key => tasks[key]);
for (let i = 0; i < keys.length; i++) {
invokeAsyncFunction(i);
}
function invokeAsynFunction(idx) {
tasks[keys[idx]](function handleAsyncResult(err, ...result) {
if (!errorHappened) {
if (err) {
callback(err);
errorHappened = true;
} else {
invocationCount++;
if (result.length === 1) {
objectCompleted[keys[idx]] = result[0];
} else {
objectCompleted[keys[idx]] = [...result];
}
if (invocationCount === keys.length) {
callback(null, objectCompleted);
}
}
}
});
}
}
}
Compose는 인자로 전달 받은 함수를 새로 구성해 하나의 함수로 만듭니다. 인자로 전달 받은 함수 중 가장 마지막으로 전달 받은 함수가 먼저 실행되고, 그 결과값은 그 다음 함수의 인자가 됩니다.
만약 Compose의 인자로 f(), g(), h()를 받으면, 그 실행값은 f(g(h()))와 같이 됩니다. 그리고 실행이 모두 완료되면 최종 결과값을 callback에 전달합니다.
인자로 전달 받은 각 함수는 this의 값이 binding 된 상태로 실행할 수 있습니다.
function compose(...callbacks) {
let curFunction = callbacks.pop();
return function(...args) {
const callback = args[args.length - 1];
args = args.slice(0, args.length - 1);
curFunction(...args, handleAsyncFunction);
function handleAsyncFunction(err, ...result) {
if (err) {
callback(err);
return;
} else {
if (callbacks.length > 0) {
curFunction = callbacks.pop();
curFunction(...result, handleAsyncFunction);
} else {
callback(err, ...result);
}
}
}
};
}
Promisify는 node에 내장되어 있는 유틸 함수로 일반적인 노드의 비동기 처리 콜백 패턴을 프로미스 형태로 사용할 수 있게 해 줍니다.
export default function promisify(asyncFunc) {
return function(...args) {
return new Promise((resolve, reject) => {
asyncFunc(...args, function handleAsynResult(err, result) {
if (err) {
reject(err);
}
resolve(result);
});
});
};
}