Node.js Async & Await를 활용하여 콜백 탈출하기

Javascript로 프로그래밍을 할 때 가장 골치인 것이 비동기 콜백 함수를 사용할 때이다. 비동기 함수가 유용할 때도 있지만, 크롤링 등의 작업을 할 때는 순차적으로 프로세스를 진행해야하는데 비동기 함수로 인해 코드 리딩이 어려워진다. Node.js 버전 7부터는 Async/Await 기능이 추가되어 이러한 비동기 콜백 문제를 직관적으로 해결 할 수 있게 되었다.

관련 키워드: Node.js, Async/Await, 비동기 콜백, 콜백 지옥

### Async / Await 문법

Async / Await 문법은 ES7에서 새롭게 지원하는 문법으로, Node.js v7에서부터 지원된다. 현재 LTS 버전은 6 버전이지만 크롤러와 같은 단일 프로그램을 작성할 때에는 Stable 버전인 7로 사용하는 것을 추천한다. 현재 브라우저 등에서는 ECMA6는 지원 중이지만 7이 지원되려면 시간이 한참 걸릴 것 같다.

Async / Await 를 사용하면, 기존 async 라이브러리나 Promise를 사용하지 않고도 비동기 콜백 지옥을 효과적으로 해결 할 수 있다. 물론 Async/Await 를 사용하려면 Promise 함수가 사용되기 때문에 사전에 Promise 사용 방법을 이해해야한다.

  • http://proinlab.com/archives/2086

Promise를 잘 모른다면 위의 글을 먼저 읽는 것을 추천한다.

Await를 위한 Promise 함수 선언

Await를 사용하기 위해서는 먼저 Promise 함수를 정의하여야한다. setTimeout 함수로 비동기 함수 2개를 만들어보자.

'use strict';

let asyncFunction1 = (message)=> new Promise((resolve)=> {
    setTimeout(()=> {
        console.log('fn-1', message);
        resolve('fn-1');
    }, 1000);
});

let asyncFunction2 = (message)=> new Promise((resolve)=> {
    setTimeout(()=> {
        console.log('fn-2', message);
        resolve('fn-2');
    }, 500);
});

위의 함수는 각각 1000ms, 500ms 후에 메세지를 출력하고 결과를 리턴하는 함수이다. 먼저 기존의 방식으로 비동기 함수를 호출해보자.

function notAsyncMain () {
    asyncFunction1('hello');
    asyncFunction2('world');
}

notAsyncMain();

위와 같이 단순 호출할 경우 결과는 아래와 같다.

fn-2 world
fn-1 hello

결과를 보면 두 함수가 병렬로 동작하여 2번째 함수가 먼저 출력을하고 1번째 함수가 출력을한다. 이 함수를 순차적으로 실행하기 위해서는 Promise를 사용하여 코드를 구성해야한다.

function notAsyncMain () {
    asyncFunction1('hello').then((data)=> {
        console.log(data);
        return asyncFunction2('world');
    }).then((data)=> {
        console.log(data);
    });
}

notAsyncMain();

위의 코드를 실행해보면 Promise 함수의 결과를 받아서 처리 할 수 있고 순차적으로 결과가 출력되는 것을 확인 할 수 있다.

fn-1 hello
fn-1
fn-2 world
fn-2

Async 함수 선언

Promise를 사용하면 비동기 함수를 순차적으로 사용 할 수 있지만, 코드의 직관성이 떨어지고 함수가 많아지면 편집이 어려워진다. 이를 해결하기 위해서 최신 문법에서는 Async 문법을 만들어서 코드를 효율적으로 짜는 것이 가능해졌다.

async function asyncMain () {
    let data = await asyncFunction1('hello');
    console.log(data);
    data = await asyncFunction2('world');
    console.log(data);
}

asyncMain();

Async 함수는 위와 같이 정의된다. await 문법은 Async 함수 안에서만 사용이 가능하고, async 내에서라고 해도 함수 안에 선언된 경우 사용이 불가하다.

async function asyncMain () {
    let fn = ()=> {
        let data = await asyncFunction1('hello');
        return data;
    };

    let result = fn();
    console.log(result);
}

asyncMain();

예를 들면, 위와 같은 코드는 동작할 수 없다. fn 이라는 함수 안에서 await 문법은 문법적 오류가 발생하며 아래와 같은 에러 로그가 출력된다.

/Users/proin/workspace/example/await/index.js:45
        let data = await asyncFunction1('hello');
                         ^^^^^^^^^^^^^^
SyntaxError: Unexpected identifier
    at createScript (vm.js:53:10)
    at Object.runInThisContext (vm.js:95:10)
    at Module._compile (module.js:543:28)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)
    at Function.Module._load (module.js:439:3)
    at Module.runMain (module.js:605:10)
    at run (bootstrap_node.js:423:7)
    at startup (bootstrap_node.js:147:9)

이러한 구조를 실행하기 위해서는 fn 함수 또한 async 함수로 만들어야한다.

async function asyncMain () {
    async function fn () {
        let data = await asyncFunction1('hello');
        return data;
    };

    let result = await fn();
    console.log(result);
}

asyncMain();

async 함수 또한 생성되면 Promise 함수로 정의된다. 따라서 실행시 동기 시키려면 await를 앞에 붙여주어야한다.

결론

Promise 개념을 이해한 후 Async / Await 문법을 잘 사용하면 순차적 작업이 필요한 단일 프로그램을 효율적으로 작성할 수 있다. 기존 다른 언어의 문법과 같이 await를 사용하면 순차적이고 직관적으로 보이게 되므로 익숙한 방식으로 비동기 문제 해결이 가능하다.

하지만 Node.js 7 버전이 아니라면 사용이 안되기 때문에 현재 LTS로 제공되는 6버전에서는 사용이 안되고, 라이브러리로 만들었을 때에는 오류를 발생시키는 원인이 될 수 있다.

또한 콜백 지옥은 피할 수 있지만, 기존 콜백의 병렬 프로세싱과 같은 장점 또한 사용이 안되므로 사용 목적에 따라 유연하게 사용해야한다. (무작정 사용하게 되면 성능 저하의 원인이 될 수 있다. 예를 들면, 웹 서버를 구축할 때 비동기 함수를 await로 사용한다면, 사용자가 동시에 접속하여 병목 현상이 발생 할 때 대응하기가 더 힘들다.)

댓글 남기기