• async 모듈을 통한 비동기 제어 :: 마이구미
    Nodejs 2017. 3. 18. 22:11
    반응형

    비동기 관련 최신글 async/await - http://mygumi.tistory.com/328


    이번 글은 Node.js 에서 비동기를 제어하기 위한 방법 중 Async 모듈을 다뤄본다.

    비동기 제어는 비동기 프로그래밍을 하다보면 접할 수 있는 문제점들이 있기에 필요하다.

    예를 들어, ajax, db query, request 등 비동기로 처리되다보니 콜백지옥을 맛보기도 한다.


    위의 말이 이해가 가지 않더라도,

    아래 예시를 통해 조금 더 자세히 다룰테니 이번 글의 요지를 알 수 있으니 끝까지 읽어보길 바란다.


    다음의 코드를 보자.


    app.get('/user/:userId', function(req, res) { var locals = {}; var userId = req.params.userId; db.get('users', userId, function(err, user) { locals.user = { name: user.name, email: user.email, bio: user.bio }; });

    db.query('posts', {userId: userId}, function(err, posts) { locals.posts = posts; }); res.render('user-profile', locals); });


    userId를 통해 2번의 DB 접근 후 페이지를 렌더하는 흐름의 코드이다.

    순수하게 본다면, 위 코드의 실행 완료 순서는 get -> query -> render 라고 생각할 수 있다.

    하지만 비동기 프로그래밍을 다뤄봤다면, 위와 같은 순서가 아니라는 것을 알 수 있다.

    그렇기에 원하는 결과가 나오지 않을 수 있다.


    사실상 위 코드로는 순서는 매번 다르기 때문에 아무도 모른다.

    그래서 이를 해결하기 위해서 아래와 같이 코드를 작성한다.

    실제로 많이 사용되는 패턴이지만, 비효율적인 패턴이라고 불리는 안티패턴(anti-pattern)이다.


    app.get('/user/:userId', function(req, res, next) { var locals = {}; var userId = req.params.userId; var callbackCounter = 0; db.get('users', userId, function(err, user) { locals.user = { name: user.name, email: user.email, bio: user.bio }; callbackCounter++; if (callbackCounter == 2) { res.render('user-profile', locals); } }); db.query('posts', {userId: userId}, function(err, posts) { locals.posts = posts; callbackCounter++; if (callbackCounter == 2) { res.render('user-profile', locals); } }); });


    count를 통해 체크한 후 원하는 완료 시점에 페이지를 render 하는 코드이다.

    보다시피 굉장히 비효율적으로 보인다.

    만약 db 접근 추가할 경우 그만큼 중첩된 코드가 추가되어야하고, 에러 대해서도 문제가 많다.


    if (callbackCounter == 3) { res.render('user-profile', locals); } if (callbackCounter == 4) { res.render('user-profile', locals); } ........


    이러한 문제점들을 async 모듈을 통해 해결할 수 있다.

    async 모듈에는 크게 3가지 메소드를 활용할 수 있다. (parallel, series, forEach)


    3가지 모두 비동기 처리를 위한 역할같다.

    역할은 같지만, 필요한 경우에 따라 사용하기 위해 제공되는 것이라 생각하면 된다.

    간단한 용어로 표현하자면, 아래와 같다.


    • parallel - 병렬 처리
    • series - 직렬 처리
    • forEach - 반복문


    자세한 건 예시를 통해 다뤄보겠다.


    app.get('/user/:userId', function(req, res, next) { var locals = {}; var userId = req.params.userId; async.parallel([ //Load user function(callback) { db.get('users', userId, function(err, user) { if (err) return callback(err); locals.user = { name: user.name, email: user.email, bio: user.bio }; callback(); }); }, //Load posts function(callback) { db.query('posts', {userId: userId}, function(err, posts) { if (err) return callback(err); locals.posts = posts; callback(); }); } ], function(err) { // Load user, Load posts 완료된 시점 if (err) return next(err); res.render('user-profile', locals); }); });


    async.parallel을 보면 인자가 2가지가 있다.

    첫번째 인자인 배열에 호출할 비동기 처리들을 넣는다. 

    (객체 형태도 가능하다. 조금 뒤에 다른 목적으로 다루겠다.)

    두번째 인자에는 배열 안에 있는 함수들이 완료가 되면 실행되는 함수이다.

    그 결과, 원하는 비동기 처리들이 끝나면 render를 할 수 있게 되었다.


    parallel는 해석 그대로 병렬이다.

    배열 안의 함수들이 병렬로 처리된다.

    그렇기에 배열 안의 함수들의 최종 완료 시점은 알 수 있지만, 각 함수의 완료 시점은 알 수 없다.


    예를 들어, 우리는 Load user를 완료한 후 Load posts가 실행되도록 하고 싶다고 가정한다.

    parallel을 통해서는 이것이 가능하지 않다는 것이다.


    이것은 async.series 를 활용할 수 있다.


    app.get('/user/:userId', function(req, res, next) { var locals = {}; var userId = req.params.userId; async.series([ //Load user function(callback) { db.get('users', userId, function(err, user) { if (err) return callback(err); locals.user = { name: user.name, email: user.email, bio: user.bio }; callback(); }); }, //Load user 완료 후 Load posts 실행됨. function(callback) { db.query('posts', {userId: userId}, function(err, posts) { if (err) return callback(err); locals.posts = posts; callback(); }); } ], function(err) { // Load user, Load posts 완료된 시점 if (err) return next(err); res.render('user-profile', locals); }); });


    원래 코드는 count를 통해 render할 시점을 체크했다.

    async 모듈을 활용하여 코드의 간결성과 유지보수 등 많은 측면에서 효율적이게 되었다.


    async.forEach 는 말 그대로 반복문처럼 활용하기에 최적화되어있다.


    app.delete('/messages/:messageIds', function(req, res, next) { var messageIds = req.params.messageIds.split(','); async.forEach(messageIds, function(messageId, callback) { db.delete('messages', messageId, callback); }, function(err) { if (err) return next(err); res.json({ success: true, message: messageIds.length+' message(s) was deleted.' }); }); });


    위와 같이 반복하여 비동기 처리를 할 경우에 forEach를 쓰면 효율적이다.


    한가지 더 유용한 팁이 있다.

    async 를 사용하는 가장 큰 이유는 비동기 처리이다.

    하지만 코드를 짜다보면, 각 함수들의 반환값을 이용할 경우가 존재한다.


    예를 들어, Load user 함수의 반환값과 Load posts의 반환값을 얻고 싶다는 것이다.

    이 경우는 callback의 인자를 통해 넘기면 가능하다.


    app.get('/user/:userId', function(req, res, next) { var locals = {}; var userId = req.params.userId; async.parallel([ function(callback) { db.get('users', userId, function(err, user) { ......... callback(null, user); }); }, function(callback) { db.query('posts', {userId: userId}, function(err, posts) { .......... callback(null, posts); }); } ], function(err, results) { ......... // 배열 형태로 반환. results[0] => Load user, results[1] => Load posts console.log(results); res.render('user-profile', locals); }); });


    조금 더 명확한 코드와 유지보수를 위해서는 배열 대신 객체 형태로 표현하면 훨씬 좋다.


    app.get('/user/:userId', function(req, res, next) { var locals = {}; var userId = req.params.userId; async.parallel({ user: function(callback) { db.get('users', userId, function(err, user) { ......... callback(null, user); }); }, post: function(callback) { db.query('posts', {userId: userId}, function(err, posts) { .......... callback(null, posts); }); } }, function(err, results) { ......... // 배열 형태로 반환. results['user'] => Load user, results['post'] => Load posts console.log(results); res.render('user-profile', locals); }); });


    마지막 팁?으로 ES6를 통해 표현하면 훨씬 간결한 코드를 볼 수 있다.


    async.parallel([ callback => db.save('xxx', 'a', callback), callback => db.save('xxx', 'b', callback) ], err => { if (err) throw err console.log('Both a and b are saved now') })


    결과적으로 본인의 목적에 따라 많은 메소드를 제공하고 있으니 선택하여 사용하면 된다.

    아래 링크를 참고하여 작성한 글로, 자세한 건 아래 링크를 참고하길 바란다.


    참고 URL

    http://www.sebastianseilund.com/nodejs-async-in-practice


    https://github.com/caolan/async/issues/431

    반응형

    댓글

Designed by Tistory.