Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Q promise로 차례대로 비동기작업 실행하기

Node를 많이 사용하지만 Promise를 많이 쓰는 편은 아니다. 물론 I/O가 많은 서버개발을 하다 보면 비동기로 인해서 코드가 엉망이 되는 문제가 꽤 많기도 하고 Callback hell에 빠지는 경우도 종종 있지만, 보통은 Callback을 선호하는 편이다. 일부 특수한 케이스에서 Promise가 해결해주는 부분이 있긴 하지만 전체적으로는 Callback에 비해서 Promise가 뭔가 크게 해결해 준다고 생각해 본 적은 거의 없다. 보통은 Promise가 필요할 때 q를 사용하지만, 일부 범위에만 한정해서 쓰고 있다가 이번에 Sequelize를 쓰다 보니 Promise를 많이 사용하게 됐다.

Promise에서 불만인 부분이 코어에서 Promise를 쓰기 시작하면 다른 부분까지 전파된다는 점인데 이러다 보니 Promise로 로직을 해결해야 하는 부분이 나타났다. 비동기로 I/O를 다룰 때 가장 어려운 경우 중 하나가 동기처럼 순서가 있다거나 어떤 종료 시점을 확인해야 한다거나 할 때인데 이번에 비슷한 경우가 발생해서 Promise로 이를 해결하는 코드를 작성해 봤다.

상황은 간단하다. 아이템이 동적인 배열이 있고 이 배열을 하나씩 꺼내서 비동기로 작업을 해야 한다. 각 작업은 연결되어 있으므로 비동기이지만 차례대로 발생해야 한다. 배열 전체를 동시에 작업하고 완료 처리하면 된다면 Q.all()등을 사용하면 되지만 이번엔 차례대로 처리해야 했다. 다행히 q의 문서에 순서대로 처리하는 예시가 나와 있었다.

var Q = require('q');

var list = [
 {id: 1, name: 'name1'},
 {id: 2, name: 'name2'},
 {id: 3, name: 'name3'},
 {id: 4, name: 'name4'},
 {id: 5, name: 'name5'}
];

var asyncJob = function(i) {
  var deferred = Q.defer();
  setTimeout(function() {
    console.log(list[i]);
    deferred.resolve(i+1);
  }, 1000);
  return deferred.promise;
};

list.reduce(function(prev, curr) {
  return prev.then(asyncJob);
}, Q.resolve(0))
.then(function() {
  console.log('completed');
}); 

asyncJob은 비동기 작업을 시뮬레이트하기 위한 임시 함수로 내부에서 setTimeout을 사용했으므로 복잡한 비동기 작업이 있더라도 이 안에서 작성하면 된다. 기본적으로 Array.prototype.reduce를 사용해서 차례대로 처리하는 방식인데 then()으로 넘겨서 다음 작업으로 하나씩 처리하게 된다. 여기서는 reduce의 방식을 이용해서 파라미터로 배열의 인덱스를 전달해서 하나씩 증가하면서 배열의 다음 아이템을 처리하도록 했다.

이를 실행하면 다음과 같이 1초마다 로그를 출력하면서 차례대로 실행이 된다.

{ id: 1, name: 'name1' }
{ id: 2, name: 'name2' }
{ id: 3, name: 'name3' }
{ id: 4, name: 'name4' }
{ id: 5, name: 'name5' }
completed

관련해서 내용을 찾아보다가 Stackoverflow에서 비슷한 답변을 보게 되었다. 여기서는 재귀를 통한 상태머신을 사용하고 있었는데 이러한 접근이 좀 더 그럴듯해 보여서 위 답변을 참고해서 재작성을 해보았다.

var Q = require('q');

var list = [
 {id: 1, name: 'name1'},
 {id: 2, name: 'name2'},
 {id: 3, name: 'name3'},
 {id: 4, name: 'name4'},
 {id: 5, name: 'name5'}
];

var asyncJob = function(v, a) {
  var deferred = Q.defer();
  setTimeout(function() {
    console.log(v);
    deferred.resolve(1);
  }, 1000);
  return deferred.promise;
};

function next(idx) {
  if (idx > list.length -1) { return Q(true); }
  return asyncJob(list[idx]).then(function(result) {
    return next(idx+ 1);
  });
}

next(0).then(function() { 
  console.log('completed');
});

next()에 초깃값(여기서는 인덱스값)을 전달하고 비동기 작업을 수행하면서 재귀 호출을 하면서 배열이 끝나면 종료하도록 처리했다. 이 코드를 실행하면 결과는 앞과 같게 나온다.

2015/02/16 03:43 2015/02/16 03:43