티스토리 뷰
노드의 강점은 그 홈페이지에서 찾아볼 수 있는데 아래와 같이 정의되어 있다.
Node.js 는 이벤트 기반, 논 블로킹 I/O 모델을 사용해 가볍고 효율적입니다.
하지만 이 강력함으로 인해 개발자는 곤욕을 치루게 되는데 그 중 하나가 콜백지옥이다. 콜백지옥이 발생하는 근본적인 이유는 노드의 비동기를 해결하고자 할 때 중첩 콜백이 이어지기 때문인데 왜 콜백을 중첩해서 사용해야 할까? 이 문제에 대해 나는 "우리 개발자 뇌가 아직 동기적으로 코드를 이해하려고 하기 때문" 이라고 이야기한다. 노드를 더 깊게 잘 이해하려면 비동기에 대한 이해를 높이고 중첩 콜백을 풀어야겠다.
각설하고 노드는 비동기에 특화되어 있는 플랫폼이다보니 무조건 동기적으로 처리해야만 하는 코드를 풀어내야 할 때 난항을 겪게 된다. 예를들어 데이터베이스에서 정보를 읽어오고 그 데이터로 뭔가 액션을 취해야 한다면 이는 절차적으로 처리가 되어야 하는 부분이다. 하지만 데이터베이스에서 정보를 읽어올 수 있도록 제공되는 모듈 자체가 대부분 비동기로 동작하다보니 다음 액션을 이어서 처리하는데 무리가 있겠다. 아무튼 이런 문제를 해결할 수 있는 방법으로 async 모듈이 있고 여기서는 async 모듈에서 제공되는 waterfall 을 통해 비동기를 교통정리 해보자.
연습문제 실습
실전 상황은 다음과 같다. 이 간단한 문제가 풀리면 더 깊게 이어지는 콜백도 같은 원리로 쉽게 풀린다.
- Redis 에서 foo KEY 를 읽어온다.
- 읽어온 KEY 값을 bar 에 넣는다.
자, 우선 아래와 같이 심플한 코드가 있다. Redis 에서 데이터를 가져오는 코드인데 foo 라는 string type 의 KEY 에는 firstString 이라는 값이 들어있다.
const redis = require( "redis" ),
client = redis.createClient();
client.on( "error", (err) => {
console.log( "Error " + err );
});
client.get( "foo", ( err, reply ) => {
if( err ) throw( err );
console.log( reply ); // firstString
client.quit();
});
console.log( "File End" );
이제 가져온 데이터를 bar 라는 다른 Redis KEY 에 넣어볼텐데 일반적인 프로그래밍 방식이라면 코드는 아래와 같은 형상일 것이다. 위의 코드와 달라진 부분은 몇 라인 안된다.
const redis = require( "redis" ),
client = redis.createClient();
client.on( "error", (err) => {
console.log( "Error " + err );
});
var fooVal = ''
client.get( "foo", ( err, reply ) => {
if( err ) throw( err );
console.log( reply ); // firstString
fooVal = reply;
});
if( fooVal )
{
console.log( 'fooVal: ', fooVal );
client.set( "bar", fooVal, redis.print );
}
console.log( "File End" ); // File End
client.quit();
과연 정상동작할까? 결과는 모두가 예상 가능한 것 처럼 정상동작하지 않는다.
$ node run.js
File End
firstString
$
이유가 뭘까? Redis 에서 값을 가져오는 과정이 비동기로 동작하다보니 fooVal 에 값이 세팅되기 전에 이미 코드는 if( fooVal ) 조건을 검사하고 run.js 프로세스를 종료해버린다. 그럼 foo 에서 가져온 값을 bar 로 세팅하기 위한 쉬운 방법은 없는걸까? 여기 async.waterfall 이 있다. 우선 모듈을 사용하기 위해 async 를 설치하자.
$ npm install async
그리고 코드를 수정해주면 되는데 async.waterfall 의 기본 사용법은 다음과 같다.
공식문서: https://caolan.github.io/async/docs.html#waterfall
waterfall 안에 작업 함수를 직접 구현하는 방식
async.waterfall([
function(callback) {
callback(null, 'one', 'two');
},
function(arg1, arg2, callback) {
// arg1 now equals 'one' and arg2 now equals 'two'
callback(null, 'three');
},
function(arg1, callback) {
// arg1 now equals 'three'
callback(null, 'done');
}
], function (err, result) {
// result now equals 'done'
});
waterfall 안에 함수 이름만 달아놓고 원형은 밖에 제공하는 방식
async.waterfall([
myFirstFunction,
mySecondFunction,
myLastFunction,
], function (err, result) {
// result now equals 'done'
});
function myFirstFunction(callback) {
callback(null, 'one', 'two');
}
function mySecondFunction(arg1, arg2, callback) {
// arg1 now equals 'one' and arg2 now equals 'two'
callback(null, 'three');
}
function myLastFunction(arg1, callback) {
// arg1 now equals 'three'
callback(null, 'done');
}
우리 예제는 작업 함수의 크기가 길지 않으니 첫 번째 방식으로 구현을 해보자. waterfall 안에서 해야할 작업이 많아지면 해당 코드의 목적을 한눈에 알아보기 쉽게하기 위해 두번째 방식이 선호될 수 있다.
const redis = require( "redis" ),
client = redis.createClient();
const async = require( "async" );
client.on( "error", (err) => {
console.log( "Error " + err );
});
var fooVal = ''
async.waterfall(
[
function( cb )
{
client.get( "foo", ( err, reply ) => {
if( err ) throw( err );
console.log( reply ); // firstString
cb( null, reply );
});
},
function( fooVal, cb )
{
console.log( 'fooVal: ', fooVal ); // fooVal: firstString
client.set( "bar", fooVal, redis.print ); // Reply: OK
cb( null );
}
],
function( err, obj )
{
if( err ) throw( err );
client.quit();
});
console.log( "File End" ); // File End
실행결과는 다음과 같다.
$ node run.js
File End
firstString
fooVal: firstString
Reply: OK
$
waterfall 을 통해 작업을 간단하게 순차처리 하였다. 여기서 주의해야하는 점이 하나 있다면 연결한 Redis 세션을 종료하거나 프로세스를 종료하는 process.exit() 와 같은 코드는 위치 선정을 잘 해야 한다. waterfall 은 분명 작업 ( task ) 에 대해서 절차적으로 처리하지만 waterfall 자체가 비동기로 동작하기 때문에 뒤쪽에 process.exit() 가 존재하면 프로그램은 비정상적으로 동작할 것이다. 아래와 같은 코드는 분명 문제가 있다. 꼭 주의하도록 하자.
async.waterfall(
task
, function( err, obj )
{
if( err ) throw( err );
client.quit();
});
process.exit();
마무리
async.waterfall 을 통해 비동기 처리에 대해 교통정리를 해보았다. 여기서는 따로 다루지 않았지만 async 에는 waterfall 이외에도 다양한 모듈이 제공되니 참고하면 좋다. 아울러 노드를 잘 다룬다는 것은 결국 비동기의 처리 과정을 이해한다는 의미와도 같은데 그러기 위해서는 그 내부 구조인 libuv 까지 내려가봐야 하지 않을까 생각해본다. 사실 waterfall 이나 series, parallel 등에 대한 글은 인터넷에 너무 많지만 이 정보의 홍수 속에서 이 글이 누군가에게는 도움이 될 수 있기를 바란다.
'개발 > Node.js' 카테고리의 다른 글
npm install 에러 - MacOS (2) | 2020.03.26 |
---|---|
노드를 더 우아하게. nvm 이야기 (0) | 2018.06.07 |
콜백(callback) 개념 이해하기 (0) | 2018.03.26 |
pm2 모듈을 부트 스크립트로 등록하기 (0) | 2018.03.16 |
노드를 더 우아하게. npm 이야기 (0) | 2018.03.08 |
- Total
- Today
- Yesterday