본문 바로가기

JavaScript/ETC

async.js

Node.js 프로그래밍을 하다보면 여러 단계로 중첩된 콜백함수 처리에 대한 문제를 겪게 된다. 이를 "Callback Hell" 이라고 부른다. 여러겹의 중첩된 비동기 콜백 함수 문제는 최초 콜백 함수 내부에서 콜백함수를 호출하고, 그 콜백함수에서 또 다시 콜백 함수를 호출하고, 이런식으로 콜백함수가 계속 중첩되는 상황에서 발생한다. 콜백함수의 중첩단계가 늘어날수록 코드는 지저분해지고 결국엔 가독성에 심각한 문제를 갖게되어 관리 불가능의 상태가 될 수도 있다. Node.JS의 써드파티 모듈 라이브러리들중 중첩 콜백 문제를 처리하기 위한 다양한 비동기 제어 모듈들이 있는데 그 중 가장 많이 활용되고 있는 모듈이 Async 모듈이다.


Async에 대한 Reference Site에 접속해 보면 크게 3종류로 기능이 나뉘어져 있는 것을 알 수 있다.

  •  Collections: Collection 객체, 예를 들어 배열과 같은 데이터를 조회하며 비동기적으로 해야할 작업이 있을 경우 주로 사용한다.
  •  Control flow : 제어흐름을 조절하는 함수들로 위에서 언급한바와 같이 기존의 함수로 구현할 경우 Callback-Hell이 발생할 수 있는 작업 흐름을 이 함수들로 구현할 경우 Callback-Hell 없이 좀 더 간단하게 구현할 수 있다.
  •  Utils: 기타 이외의 부가적인 기능들

Collections

  • each
  • every
  • filter
  • map
  • reduce

Controll flow

  • parallel
  • series
  • until
  • waterfall


each

async.each는 비동기 프로그래밍 방식으로 배열 객체의 원소에 대해 반복 함수를 수행할 수 있도록 기능을 제공한다.


async.each(arr, iterator, callback)


배열 arr의 각각 원소에 대해 병렬로 iterator 함수를 적용한다. 배열로부터 각 원소를 전달받아 iterator가 호출되며 iterator 실행이 끝나면 자신의 callback이 호출된다. 만일 iterator가 자신의 callback에 에러를 전달하면 each함수의 메인 callback이 에러와 함께 호출된다. 이 함수는 병렬로 시작되기 때문에 iterator가 순서대로 실행되는 것을 보장하지는 않는다.


매개변수

  • arr - 반복 수행될 배열 객체
  • iterator(item, callback) - 배열 arr의 각 원소 item에 대해 적용될 함수로 실행을 완료하면 callback(err)을 호출한다. 에러가 없을 경우 callback은 인자없이 호출되거나 명시적으로 null 인자를 전달해야 한다.
  • callback - 이 메인 callback은 모든 iterator 함수 실행이 완료되면 호출되거나 에러가 발생하면 호출된다.

아래 코드는 async.each를 이용하여 배열의 원소 객체 각각에 대해 square 함수를 실행하고 마지막으로 콜백함수를 실행한다. 이때 만일 배열의 원소가 숫자 타입이 아닐 경우 square 함수의 콜백을 에러와  함께 호출한다.


예제 - 1

var async = require('async');

// [1] each

function square(item, doneCallback){

if(typeof item != "number"){

doneCallback(new Error(item+' != number type'));

return;

}

console.log(item*item);

doneCallback(null);

}

// 1,2,3,4가 순서대로 실행되는 것이 아니라 동시에 실행되서 콜백을 먼저 타는애가 먼저 출력된다.

async.each([1,2,3,4], square, function(err){

// squre함수에 배열을 던져서 모든 작업이 끝날때 호출되는 Callback함수

if(err) console.log(err.message);

else console.log('Finish');

});

flow 

  1. 배열요소(item)을 하나하나 돌면서 에러가 발생하면 doneCallback(error)을 일으킨다.
  2. doneCallback(error)이 일어나면 each의 세번째 매개변수 callback 함수에 에러를 전달한다.
  3. 에러가 발생해도 each는 모든 배열 요소를 돈다. (?? 요거 확인해봐야함)
  4. callback 함수의 에러는 최근의 에러로 덮혀 씌워진다. 

예제 - 2 (API)

// openfiles가 파일 이름의 배열이고, savefile은 파일의 수정된 내용을 저장하는 함수라고 가정한다.

async.each(openFiles, saveFile, function(err){
// if any of the saves produced an error, err would equal that error
});

// openFiles가 파일 이름의 배열이라고 가정하면
async.each(openFiles, function(file, callback) {

// 여기서 파일 작업을 수행한다
console.log('Processing file ' + file);

if( file.length > 32 ) {
console.log('This file name is too long');
callback('File name too long');
} else {
// 여기서 파일 처리를 위해 작업한다.
console.log('File processed');
callback();
}
}, function(err) {
// 만약 어떤 파일 처리에서 오류가 발생한다면, 에러는 그 오류와 같을 것이다.
if( err ) {
// interation 중 하나라도 오류가 발생하면 모든 과정이 중지 될 것이다.
console.log('A file failed to process');
} else {
console.log('All files have been processed successfully');
}
});

each.js

export function each(arr, iterator, callback) {
var error = null

for (var i = 0; i < arr.length; i++) {
iterator(arr[i], (e) => {
if(e) {
error = e
}
})

if(error) break // 확인 필요

}

callback(error)

}


every


예제

every([4,2,8,16,19,20,44], function(number, callback) {
if(number % 2 === 0) {
callback(null, true);
} else{
callback(null, false);
}
}, function(err, result) {
console.log(err, result)
});

filter


예제

filter([4,2,8,16,19,20,44], function(number, callback) {
if(number % 2 === 0) {
callback(null, true);
} else {
callback(null, false);
}
}, function(err, result) {
console.log(err, result)
});

map

async.map은 비동기 프로그래밍 방식으로 배열 객체의 원소에 대해 반복함수를 수행하고 마지막으로 배열 결과 객체를 얻을 수 있도록 하는 기능을 제공한다.

async.map(arr, iterator, callback)

배열 arr의 각각의 원소를 매핑하여 새로운 배열을 생성한다. 배열로부터 각 원소를 전달받아 iterator가 호출되며 iterator 실행이 끝나면 자신의 callback이 호출된다. 이 callback은 에러와 배열 원소 2개의 인자를 갖는다. 만일 iterator가 자신의 callback에 에러를 전달하면 map 함수의 메인 callback이  에러와 함께 호출된다. 이 함수는 병렬로 실행되기 때문에 iterator가 순서대로 실행되는 것을 보장하지 않지만, callback에 전달되는 결과배열은 본래 배열 arr의 순서와 동일하다.


매개변수

  • arr - 반복 수행될 배열 객체
  • iterator(item, callback) - 배열 arr의 각 원소 item에 대해 적용될 함수로 실행을 완료하면 에러(에러가 없을 경우 null)와 item의 변형값과 함께 callback(err, transformed)를 호출한다.
  • callback(err, results) - 이 메인 callback은 모든 iterator iterator 함수 실행이 완료되면 호출되거나 에러가 발생하면 호출된다. 이때 results는 배열 arr의 원소 item에 대해 변형한 값을 원소로 가지고 있는 배열이다.
아래 예제에서 async.map()은 배열 객체에 대해 두번째 매개변수로 전달된 iterator함수를 각각의 원소에 대해 실행한 결과를 생성해 콜백에 전달한다. 이때 사용한 uuid 써드파티 모듈은 rfc4122 UUID 생성을 위한 v4() 함수를 제공한다.


예제

map(arr,
function(item, callback){
callback(null, {'id':item,'uuid':
[Math.floor((Math.random() * 100) + 1),
Math.floor((Math.random() * 100) + 1),
Math.floor((Math.random() * 100) + 1)]
})
},
function(err,result){
if(err) console.log(err);
else console.log(result);
}
);

map.js

export function map(arr, iterator, callback) {
var resultArray = []
var error = null
for(var i = 0; i < arr.length; i++) {
iterator(arr[i], function (err, result){
error = err
resultArray.push(result)
})
}
callback(error, resultArray)
}


reduce

예제

reduce([1,2,3], 1, function(memo, item, callback) {
callback(null, memo * item)
}, function(err, result) {
console.log(err,result)
});

Parallel

async.parallel()은 태스크의 병렬 실행이 필요할 때 사용하는 흐름제어 함수로 태스크 실행은 서로 독립적으로 진행된다.

async.parallel(tasks, [callback])

task 배열에 있는 함수들을 서로 상관없이 병렬로 실행하며, 이 함수들 중 어느 하나가 자신의 콜백에 에러를 전달하면 더이상 나머지 함수들은 실행되지 않고, 즉시 callback이 에러와 함께 호출된다. 에러가 없고 모든 함수가 실행을 마치면 결과 배열과 함께 callback이 호출된다. (나머지 함수 실행되는데 ? callback만 호출안됨)

배열 대신 객체를 사용할 경우엔 각각의 프로퍼티를 함수로 정의해야 하며, 마지막 callback에 전달할 인자를 배열 대신 객체로 전달한다.

매개변수

  • tasks - 수행할 함수를 가지고 있는 배열 또는 객체로, 각각의 함수는 에러(에러가 없을 경우 null)와 결과 값과 함께 callback(err, results)을 호출한다.
  • callback(err, results) - 이 선택적인 callback은 모든 함수가 수행되고 나면 호출된다. 이 함수는 배열 또는 객체 results에 tasks의 함수들의 콜백에 전달된 값들이 저장되어 있다.
아래 예제는 async.parallel()을 이용한 비동기 프로그래밍의 병렬 처리 제어 코드 예제이다. 배열의 원소인 task#() 함수를 병렬로 호출하며, 각각 task#()함수 마지막에 배열 results에 저장될 값과 함께 callback()를 호출한다. 이때 에러가 발생할 경우 여러 정보를 callback의 첫번째 인자값으로 넘겨준다.


parallel([
function task1(callback){
setTimeout(function(){
console.log("task1");
// 콜백 함수로 에러로그와 result인 "one"을 전달하면 콜백함수는 이를 배열에 저장한다.
callback(null, "one");
},1000);
},
function task2(callback){
setTimeout(function(){
console.log("task2");
// 콜백 함수로 에러로그와 result인 "one"을 전달하면 콜백함수는 이를 배열에 저장한다.
callback(null,"two");
}, 3000);
},
function task3(callback){
setTimeout(function(){
console.log("task3");
// 콜백 함수로 에러로그와 result인 "one"을 전달하면 콜백함수는 이를 배열에 저장한다.
callback(null, "three");
}, 2000);
}],
// Callback함수(기존 작업이 모두 종료되어야만 호출된다.)
function(err,results){
if(err){
return console.log(err);
}
console.log(results);
}
);

parallel.js

// todo: taskArray가 배열 또는 객체로 input
// todo: resultArray가 taskArray의 타입과 같아야 됨

export function parallel(taskArray, callback) {
var error = null
var resultArray = []

function getResult(callbackFunc) {
for (var i = 0; i < taskArray.length; i++) {
taskArray[i](function(err, result) {
callbackFunc(err, result)
})
}
}

getResult(function(err, result) {
if(err) error = err
resultArray.push(result)

if(err || (!error && (taskArray.length === resultArray.length))) callback(err, resultArray)

})

}

Series

async.series()는 태스크의 순차적 실행이 필요할 때 사용하는 흐름제어 함수이다.

async.seires(tasks, [callback])

순차적으로 tasks배열에 있는 함수들을 실행하며, 이 함수들 중 어느 하나가 자신의 콜백에 에러를 전달하면 더 이상 나머지 함수들은 실행되지 않고 즉시 callback이 에러와 함께 호출된다. 에러가 없고 모든 함수가 실행을 마치면 결과 배열과 함께 callback이 호출된다. 배열 대신 객체를 사용할 경우엔 각각의 프로퍼티를 함수로 정의해야 하며, 마지막 callback에 전달할 인자를 배열 대신 객체로 전달해야 한다.

매개변수

  • tasks - 수행할 함수를 가지고 있는 배열 또는 객체로, 각각의 함수는 에러(에러가 없을 경우 null)와 결과 값과 함께 callback(err, result)을 호출해야 한다.
  • callback(err, results) - 이 선택적인 callback은 모든 함수가 수행되고 나면 호출된다. 이 함수는 배열 또는 객체 results에 tasks의 함수들의 콜백에 전달된 값들이 저장되어 있다.
아래 예제는 async.series을 이용한 비동기 프로그래밍의 순차 처리 제어 코드 예제이다. 배열의 원소인 task#()함수를 순차적으로 호출하며, task#() 함수 마지막에 배열 results에 저장될 값과 함께 callback()를 호출한다. 이때 에러가 발생할 경우 에러 정보를 callback의 첫번째 인자값으로 넘겨준다.

예제

series([
function task1(callback){
setTimeout(function(){
console.log("task1");
// 콜백 함수로 에러로그와 result인 "one"을 전달하면 콜백함수는 이를 배열에 저장한다.
callback(null, "one");
},1000);
},
function task2(callback){
setTimeout(function(){
console.log("task2");
// 콜백 함수로 에러로그와 result인 "one"을 전달하면 콜백함수는 이를 배열에 저장한다.
callback(new Error("Error"),"two");
}, 3000);
},
function task3(callback){
setTimeout(function(){
console.log("task3");
// 콜백 함수로 에러로그와 result인 "one"을 전달하면 콜백함수는 이를 배열에 저장한다.
callback(null, "three");
}, 2000);
}],
// Callback함수(기존 작업이 모두 종료되어야만 호출된다.)
function(err,results){
if(err){
return console.log(err);
}
console.log(results);
}
);

series.js

// todo: taskArray가 배열 또는 객체로 input
// todo: resultArray가 taskArray의 타입과 같아야 됨

export function series(taskArray, callback) {
var error = null
var resultArray = []
var index = 0


function getResult(i, callbackFunc) {
if(error) return
taskArray[i](function(err, result) {
callbackFunc(err, result)
})
}
function callbackFunc(err, result) {
if(err) error = err
resultArray.push(result)

if(err || (!error && (index === taskArray.length - 1))) callback(err, resultArray)

index = index < taskArray.length - 1 ? index + 1 : null

if(index) getResult(index, callbackFunc)
}
getResult(index, callbackFunc)
}


Until

async.until()은 태스크가 true로 돌아올 때 까지 반복한다. 중단되었을 때 콜백 또는 오류가 발생한다. 콜백에 오류가 전달되고, 모든 함수가 수행되고 나면 호출된다

매개변수

  • test- true(루프를 정지시키고자 하는 경우) 또는 false(루프를 계속 하려면 false) 중 하나를 반환하는 함수이다.
  • iteratee(callback) - 비동기 함수
  • callback(err, result) - 끝나고 호출되는 callback

Waterfall

async.waterfall()은 태스크의 순차적 실행이 필요할 때 사용하는 흐름제어 함수로 async.series()와 다르게 이전 태스크의 결과를 다음 태스크 함수의 인자로 전달할 수 있다.

async.waterfall(tasks, [callback])

순차적으로 tasks 배열에 있는 함수를 실행하며, tasks  배열에 있는 다음 함수에 결과를 인자로 전달한다. 이 함수들 중 어느 하나가 자신의 콜백에 에러를 전달하면 더이상 나머지 함수들은 실행되지 않고 즉시 callback이 에러와 함께 호출된다.

매개변수

  • tasks - 수행할 함수를 가지고 있는 배열의 각각의 함수는 에러(에러가 없을 경우 null)와 다음함수에 전달할 결과 값들과 함께 callback(err, result1, result2, ...)을 호출해야 한다.
  • callback(err, results) - 이 선택적인 callback은 모든 함수가 수행되고 나면 호출된다. 이 함수의 선택적인 인자 results에는 tasks이 마지막 함수의 결과 값이 전달된다.
아래 예제는 async.waterfall()를 이용한 비동기 프로그래밍의 순차처리 제어 코드이다. 배열의 원소인 task#()함수를 순차적으로 호출하며, 다음 task#() 함수를 콜백으로 호출해서 결과 값들을 콜백 함수의 인자들로 전달한다. 마지막 task#()함수에서는 최종 결과를 생성하여 results에 저장될 값과 함께 메인의 callback()를 호출한다.

waterfall([
function task1(callback){
// 첫번째 작업 시작
setTimeout(function(){
console.log("task1");
// 에러가 없으므로 null과 두번째 작업함수에서 전달받을 매개변수 1,2를 함께 리턴
callback(null, 1, 2);
},1000);
},
function task2(arg1, arg2, callback){
// task1로부터 1,2를 전달받아 다시 덧셈과 곱셈을 하여 리턴
setTimeout(function(){
console.log("task2");
callback(null, arg1 + arg2, arg1 * arg2);
},3000);
},
function task3(arg1, arg2, callback){
// task2로부터 3,2를 전달받아 배열에 넣고 callback함수로 매개변수로 담아 리턴
setTimeout(function(){
console.log("task3");
var arr = [];
arr.push(arg1);
arr.push(arg2);
callback(null, arr);
},2000);
}
],
function(err,result){
if(err){
return console.log(err);
}
// task3으로부터 2개 원소가 있는 배열을 전달받음
console.log(result);
}
);


waterfall.js


// todo: taskArray가 배열 또는 객체로 input
// todo: resultArray가 taskArray의 타입과 같아야 됨

export function waterfall(taskArray, callback) {

var error = null
var resultArgumentsArray = []
var index = 0

function getResult(i, argumentsArray, callbackFunc) {
if(error) return

argumentsArray.push(callbackFunc)
taskArray[i].apply(null, argumentsArray)
}
function callbackFunc(err) {
if(err) error = err

if(err || (!error && (index === taskArray.length - 1))) {
resultArgumentsArray.pop()
callback(err, resultArgumentsArray)
}

index = index < taskArray.length - 1 ? index + 1 : null

resultArgumentsArray = []

if(index) {
for(var i = 1; i < arguments.length; i++) resultArgumentsArray.push(arguments[i])

getResult(index, resultArgumentsArray, callbackFunc)
}
}
getResult(index, resultArgumentsArray, callbackFunc)
}



출처 : http://avilos.codes/server/nodejs/node-js-async-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC/



git : https://github.com/Jeonsol/AsyncJS

async.pdf



'JavaScript > ETC' 카테고리의 다른 글

불변성  (0) 2019.04.03
Object.entries()  (0) 2019.03.29
자바스크립트 비동기 처리와 콜백함수  (0) 2019.03.07
this  (0) 2019.03.05
렉시컬 스코핑  (0) 2019.03.05