Node.js는 비동기 처리 방식을 사용한다는 특징이 있고, 이 특징 덕분에 빠른 속도를 낼 수 있습니다. 하지만 비동기 처리 방식에 익숙하지 않다면, 아래의 코드가 어떻게 작동될지 이해하기 어려울 수 있습니다.

let message;

setTimeout(function() {
	message = 'Hello World!'
}, 1000);

console.log(message);

일반적인 프로그래밍 언어라면, message라는 변수를 만들고, 1초 후에 message에 “Hello World!”를 넣고, 그 후에 message를 출력할 것이라고 예측할 수 있을 것 입니다.

하지만, 위 코드를 실행시키면 실행하자 마자 undefined가 출력됩니다.

그 이유는 프로그램이 실행될때 setTimeout가 끌날 때 까지 기다려주는 것이 아니라, setTimeout 실행 후, 바로 그 아랫 줄의 console.log를 실행하기 때문입니다. message에 “Hello World!”가 들어가는 것은 이미 console.log가 실행된 후입니다.

이러한 코드의 실행 방식이 비동기 처리 방식입니다. 이런 상황에서 원하는 방식으로 코드를 짜기 위해 Callback, Promise, async / await 를 사용합니다.

Callback

가장 기본적인 방법은 callback 함수를 사용하는 것 입니다.

function delay(callback) {

    setTimeout(function() {

        callback();

    }, 1000);

}

console.log('Before');

delay(function() {

    console.log('Finished');

});

console.log('After');

우선, 비동기 작업이 끝난 후에 실행할 코드를 어떤 함수로 만듭니다. (위의 경우에는 익명 함수)

비동기로 처리되는 부분이 있는 함수인 delaycallback이라는 파라미터를 갖고 있습니다. callback에 아까 만들어둔 함수를 넣어주면, 비동기 작업이 끝났을 때 그 함수를 호출하여 원하는 코드를 실행할 수 있습니다. 이렇게 작업이 끝났을 때 호출할 함수를 콜백 함수라고 합니다.

사실 위의 코드는 굉장히 간단한 예시입니다. setTimeout 함수도 파라미터로 콜백 함수를 받고 있는데, setTimeout의 콜백 함수에서는 callback()만을 호출합니다. 이 경우에는 굳이 새로운 익명 함수를 만들 필요 없이, 아래와 같이 callback을 그대로 넘겨줘도 됩니다.

function delay(callback) {

    setTimeout(callback, 1000);

}

Promise

console.log('Before');

new Promise(function (resolve) {

    setTimeout(function() {
        
        resolve('Finished');

    }, 1000);
  
}).then(function(result) {

    console.log(result);

});

console.log('After');
function delay() {
    return new Promise(function (resolve) {

        setTimeout(function() {
            
            resolve('Finished');
    
        }, 1000);
      
    });
}

console.log('Before');

delay().then(function(result) {
    
    console.log(result);

});

console.log('After');

Promise는 여러 가지 방법으로 사용할 수 있습니다. 위의 예시는 new Promise()를 통해 새로운 Promise를 만들고 바로 실행하는 방식이고, 아래의 예시는 return new Promise()를 반환하는 함수를 만들어두고, 그 함수를 호출하는 방식입니다.

Promise는 실행된 후, 비동기 처리 방식을 따라 끝날 때 까지 기다리지 않고 그 아랫줄의 코드를 바로 실행시킵니다. Promise가 종료된 후에 실행할 코드는 Promise 뒤에 이어지는 then() 안에 함수 형태로 작성해주시면 됩니다. 이때, Promise가 resolve() 를 통해 반환한 값을 파라미터로 받을 수 있습니다. (Promise는 값을 반환할 때 return 대신 resolve를 이용합니다.)

Promise 에러 핸들링

아래 예시는 Promise에서 에러가 발생할 때 처리하는 방법입니다.

new Promise(function (resolve, reject) {

    setTimeout(function() {

        reject('Catched');

    }, 1000);
  
}).then(function(result) {
    
    console.log(result);

}).catch(function(result) {

    console.log('Error ' + result);

});

Promise 안에서 에러가 발생했을 대는 resolve() 대신 reject()를 이용해서 값을 반환하고, Promise 뒤에 이어지는 catch() 를 붙히고 그 안에 함수 형태로 에러가 발생했을 때 실행할 코드를 작성합니다. 이 함수도 reject()가 반환한 값을 파라미터로 받을 수 있습니다.

Promise chaining

Promise에서는 아래와 같이 then chaining을 통해 then을 여러 번 연결할 수 있습니다.

new Promise(function (resolve) {

    setTimeout(function() {
            
        resolve(10);
    
    }, 1000);
      
}).then(function(result) {

    return result * 10;

}).then(function(result) {

    return result * 10;

}).then(function(result) {

    console.log(result);

});

then을 여러 번 연결 했을 때, 다음 then은 이전 then의 반환 값을 파라미터로 받습니다.

async / await

async와 await을 이용하면 비동기 처리 방식을 해결하기 위해 chaining 하던 방식을 사용하지 않고, 코드를 아래로 쭉 쓰는 방식을 사용할 수 있습니다.

함수를 선언할 때 async 함수로 선언하면, 그 함수 안에서는 await을 사용할 수 있습니다.

function delay() {
    return new Promise(function (resolve) {

        setTimeout(function() {
            
            resolve('Finished');
    
        }, 1000);
      
    });
}

async function run() {

    console.log('Before');

    const result = await delay();

    console.log(result);

    console.log('After');

}

run();

위의 코드에서는 run 함수를 async 함수로 선언하고, delay 함수를 호출할 때 await 키워드를 붙혀 실행했습니다. await이 붙은 Promise는 기존의 비동기 처리 방식과 다르게, 그 Promise가 끝날 때 까지 기다린 후에 다음 줄을 실행합니다.

위 코드의 결과는 기존 예시들과는 다르게 Before, Finished, After 순서로 출력됩니다. 비동기 처리 방식이 아니라 순서대로 실행되는 방식을 원한다면, 비동기로 실행되는 부분을 Promise로 만들어두고 async와 await을 이용해서 코드를 짤 수 있습니다.

aync 함수의 반환형

function delay() {
    return new Promise(function (resolve) {

        setTimeout(function() {

            resolve('Finished');

        }, 1000);

    });
}

async function run() {

    console.log('Before');

    const result = await delay();

    console.log(result);

    console.log('After');

}

async function runWrapper() {

    console.log('Before run');

    await run();

    console.log('After run');

}

runWrapper();

이 코드는 기존 코드에서 run 함수 이전과 이후에 다른 코드를 실행시키는 예시입니다.

run은 aync 함수이기 때문에, 반환형이 Promise입니다.

즉, 이미 aync 함수로 만들어둔 코드를 await하고 싶다면 new Promise()를 이용해 새로운 Promise를 만들 필요 없이 그대로 이용하면 됩니다.

async / await 에러 핸들링

아래 예시는 async와 await을 이용할 때 에러를 처리하는 코드입니다.

function delay() {
    return new Promise(function (resolve, reject) {

        setTimeout(function() {

            reject('Catched');
    
        }, 1000);
      
    });
}

async function run() {

    console.log('Before');

    try {

        let result = await delay();

        console.log(result);

    } catch(error) {

        console.log('Error ' + error);

    }

    console.log('After');

}

run();

에러가 발생할 수 있는 Promise를 호출할 때는 에러 처리 코드를 작성해야 합니다. await을 이용해 Promise를 호출하는 코드를 try, catch로 감싸주면 에러가 발생하더라도 안전한 코드를 작성할 수 있습니다.

에러가 발생한 경우, try 안에 있던 코드를 건너뛰고 catch 안의 코드를 실행합니다. 이때 catch의 파라미터로는 reject() 가 반환한 값이 들어갑니다.