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
- 배열요소(item)을 하나하나 돌면서 에러가 발생하면 doneCallback(error)을 일으킨다.
- doneCallback(error)이 일어나면 each의 세번째 매개변수 callback 함수에 에러를 전달한다.
- 에러가 발생해도 each는 모든 배열 요소를 돈다. (?? 요거 확인해봐야함)
- 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
매개변수
- arr - 반복 수행될 배열 객체
- iterator(item, callback) - 배열 arr의 각 원소 item에 대해 적용될 함수로 실행을 완료하면 에러(에러가 없을 경우 null)와 item의 변형값과 함께 callback(err, transformed)를 호출한다.
- callback(err, results) - 이 메인 callback은 모든 iterator iterator 함수 실행이 완료되면 호출되거나 에러가 발생하면 호출된다. 이때 results는 배열 arr의 원소 item에 대해 변형한 값을 원소로 가지고 있는 배열이다.
예제
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
매개변수
- tasks - 수행할 함수를 가지고 있는 배열 또는 객체로, 각각의 함수는 에러(에러가 없을 경우 null)와 결과 값과 함께 callback(err, results)을 호출한다.
- callback(err, results) - 이 선택적인 callback은 모든 함수가 수행되고 나면 호출된다. 이 함수는 배열 또는 객체 results에 tasks의 함수들의 콜백에 전달된 값들이 저장되어 있다.
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
매개변수
- tasks - 수행할 함수를 가지고 있는 배열 또는 객체로, 각각의 함수는 에러(에러가 없을 경우 null)와 결과 값과 함께 callback(err, result)을 호출해야 한다.
- callback(err, results) - 이 선택적인 callback은 모든 함수가 수행되고 나면 호출된다. 이 함수는 배열 또는 객체 results에 tasks의 함수들의 콜백에 전달된 값들이 저장되어 있다.
예제
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
매개변수
- test- true(루프를 정지시키고자 하는 경우) 또는 false(루프를 계속 하려면 false) 중 하나를 반환하는 함수이다.
- iteratee(callback) - 비동기 함수
- callback(err, result) - 끝나고 호출되는 callback
Waterfall
매개변수
- tasks - 수행할 함수를 가지고 있는 배열의 각각의 함수는 에러(에러가 없을 경우 null)와 다음함수에 전달할 결과 값들과 함께 callback(err, result1, result2, ...)을 호출해야 한다.
- callback(err, results) - 이 선택적인 callback은 모든 함수가 수행되고 나면 호출된다. 이 함수의 선택적인 인자 results에는 tasks이 마지막 함수의 결과 값이 전달된다.
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
'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 |