Callback Hell을 줄이기 위한 Node.js의 방법

콜백 헬(Callback Hell)이란?

function foo() {
    run1(function() {
        run2(function() {
            run3(function() {
                ...
            });
        });
    });
}

자바스크립트는 비동기 프로그램이라는 특성상 이벤트에 따라 받는 콜백이 복잡해지면 내부에서 지속적으로 콜백을 사용하게 되므로 코드 읽기가 아주 어려워지는 상황이 발생하게 된다. 이러한 상황을 콜백헬(Callback Hell) 이라고 한다. 이러한 콜백헬을 피하기 위해 대표적으로 async라이브러리가 있고 Promise 패턴, Generator 패턴 등이 있다.

Async

function foo() {
    async.waterfall([function (next) {
                run1(next);
            }, function (next) {
                run2(next);
            }, function (next) {
                run3(next);
            }, function (next) {
                ...
            }
        ]);
}

Promise 패턴

function foo() {
	run1Async.then(function () {
		return run2Async();
	}).then(function () {
		return run3Async();
	}).then(function () {
		...
	});
}

Generator 패턴

기존의 패턴보다 조금은 이해하기 어려운 Generator 패턴을 이해해야 db의 접근 문법을 완성할 수 있을것 같아서 자세히 살펴보겠다. 자바스크립트에 포함되어 사용하게 되는데 기존에 많은 언어에 도입되었던 패턴이다. 제너레이터를 사용하려면 function 대신에 function* 문법을 사용해야 한다. 테스트 코드를 보자. 크롬의 console에서 다음을 테스트 해보자. 

> function* increase() {
    for (var i = 0; i < 5; i++) {
      yield i;
    }
  }
  undefined
> var index = increase();
  undefined
> index.next();
  Object {value: 0, done: false}
> index.next();
  Object {value: 1, done: false}
> index.next();
  Object {value: 2, done: false}
> index.next();
  Object {value: 3, done: false}
> index.next();
  Object {value: 4, done: false}
> index.next();
  Object {value: undefined, done: true}

여기서 increase라는 이름으로 만든 것이 제너레이터고 제너레이터라는 의미로 function를 사용했다. 여기서 제너레이터를 할당한 indexindex.toString()를 실행해 보면 "[object Generator]"로 나와서 제너레이터임을 알 수 가 있다. 이 예제에서 제너레이터는 for문을 순회하면서 yield로 인덱스 값을 반환하게 되는데 제너레이터는 여기서 멈추고 next()를 호출될 때마다 다음 yield를 만날 때까지만 다시 실행을 하게 된다. next()를 실행할 때마다 yield의 값이 value에 담기고 done은 완료 여부를 나타낸다. 이 yield라는 표현은 어디서든 사용할 수 있다. 이 생성자는 함수의 어느 곳이든 일시 중단 시킬 수 있다. 예를 들어 foo(yield x, yield y) 와 같이 인자에서도 사용할 수 있다. 

generator는 함수처럼 보일 수 있으나 객체일 뿐이다. 따라서 .nextsend 등의 멤버를 호출하여 실행을 재개 할 수 있다. send는 값을 같이 전달 할 수 있다. 따라서 next()와 .send(null)은 같은 표현이 된다. 다음 코드를 보자.

function* foo(x) {
    yield x + 1;

    var y = yield null;
    return x + y;
}

next()와 send()의 사용 결과 이다. generator는 어떠한 raw value도 리턴하지 않는다. 다만 value와 done에 대한 프로퍼티를 리턴한다. 


var gen = foo(5);
gen.next(); // { value: 6, done: false }
gen.next(); // { value: null, done: false }
gen.send(8); // { value: 13, done: true }


yield에 대한 다른 예제를 살펴보자.

function* anotherGenerator(i) {
  yield i + 1;
  yield i + 2;
  yield i + 3;
}

function* generator(i) {
  yield i;
  yield* anotherGenerator(i);
  yield i + 10;
}

var gen = generator(10);

console.log(gen.next().value); // 10
console.log(gen.next().value); // 11
console.log(gen.next().value); // 12
console.log(gen.next().value); // 13
console.log(gen.next().value); // 20

 

Node.js에서의 제너레이터

자바스크립트의 최신 기능들도 브라우저에서는 대부분 크로스 브라우징 이슈때문에 사용하기 어려운데 제너레이터같은 기능은 특히 사용하기가 쉽지 않아 보인다. 하지만 V8에 들어간 것이기 때문에 Node.js에서는 사용할 수가 있다. 

코드의 적용

var fs = require('fs');

fs.readFile('./one.txt', 'utf-8', function(error, data1) {
  fs.readFile('./two.txt', 'utf-8', function(error, data2) {
    fs.readFile('./three.txt', 'utf-8', function(error, data3) {
       console.log(data1 + data2 + data3); // one two three
    });
  });
});

위와 같이 3개의 파일을 읽어서 내용을 이어붙이기를 할때, 위에서 언급한 callack hell 형태의 코드가 된다. 이것을 프로미스(Promise)라이브러리인 q를 사용하면 다음과 같이 작성할 수 있다.

var Q = require('q'),
    fs = require('fs');

function step1() {
  var deferred = Q.defer();
  fs.readFile('./one.txt', 'utf-8', function(error, data) {
    if (error) {
      deferred.reject(new Error(error));
    } else {
      deferred.resolve(data);
    }
  });
  return deferred.promise;
}
function step2(p) {
  var deferred = Q.defer();
  fs.readFile('./two.txt', 'utf-8', function(error, data) {
    if (error) {
      deferred.reject(new Error(error));
    } else {
      deferred.resolve(p + data);
    }
  });
  return deferred.promise;
}
function step3(p) {
  var deferred = Q.defer();
  fs.readFile('./three.txt', 'utf-8', function(error, data) {
    if (error) {
      deferred.reject(new Error(error));
    } else {
      deferred.resolve(p + data);
    }
  });
  return deferred.promise;
}

Q.fcall(step1)
 .then(step2)
 .then(step3)
 .then(function (value3) {
   console.log(value3)
 }, function (error) {
   console.log(error);
 })
 .done();

step1, step2, step3는 읽어오는 파일명만 다르고 완전히 같은 코드이지만 각 과정이 분리되어 있다는 의미로 따로 작성했고 앞에서 3개의 파일을 읽어오는 과정을 별도의 함수로 분리한 것이다. 이를 Q.fcall을 사용하면 위와 같이 순차적으로 실행되는 것처럼 코드를 작성할 수 있다. 여기에 제너레이터를 사용하면 다음과 같이 작성할 수 있다.

Q.async(function* () {
   try {
     var value1 = yield step1();
     var value2 = yield step2(value1);
     var value3 = yield step3(value2);
     console.log(value3);
   } catch (e) {
     console.log(e);
   }
 })().done();

Q.async에 제너레이터 함수를 전달했고 각 단계에서 yield를 사용했다. 앞에서 then으로 연결한 것보다 깔끔해 졌고 실제로 비동기 콜백으로 이어진 것이 아니라 순차적으로 실행되는 것으로 보인다. 그리고 이렇게 제너레이터를 썼을 때의 장점은 제너레이터가 오류를 던져주기 때문에 try - catch 구문을 그대로 사용할 수 있고 훨씬 자연스러운 로직을 작성할 수 있다.

co

nodejs와 promise의 패턴을 이용한 제네레이터로 넌블럭킹코드를 쉽게 넣어주는 도구이다. https://github.com/tj/co 에서 오픈소스를 볼 수 있다. 이전에는 thunk라는 것을 리턴했는데 v4에서는 프로미스를 리턴할 수 있게 되었다. 

co(function* () {
  var result = yield Promise.resolve(true);
  return result;
}).then(function (value) {
  console.log(value);
}, function (err) {
  console.error(err.stack);
});

기본적인 전체 사용 예제를 살펴보자.

 var co = require('co');

co(function *(){
  // yield any promise
  var result = yield Promise.resolve(true);
}).catch(onerror);

co(function *(){
  // resolve multiple promises in parallel
  var a = Promise.resolve(1);
  var b = Promise.resolve(2);
  var c = Promise.resolve(3);
  var res = yield [a, b, c];
  console.log(res);
  // => [1, 2, 3]
}).catch(onerror);

// errors can be try/catched
co(function *(){
  try {
    yield Promise.reject(new Error('boom'));
  } catch (err) {
    console.error(err.message); // "boom"
 }
}).catch(onerror);

function onerror(err) {
  // log any uncaught errors
  // co will not throw any errors you do not handle!!!
  // HANDLE ALL YOUR ERRORS!!!
  console.error(err.stack);
}

co는 yieldable 객체로 다음과 같은 것들을 지원하고 있다.

  • promises
  • thunks (functions)
  • array (parallel execution)
  • objects (parallel execution)
  • generators (delegation)
  • generator functions (delegation)

 

참고

 

 

 

youngdeok's picture

Language

Get in touch with us

"If you would thoroughly know anything, teach it to other."
- Tryon Edwards -