Skip to content

Instantly share code, notes, and snippets.

@disjukr
Last active April 5, 2020 02:35
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save disjukr/313105cbca8dcbc9bd98 to your computer and use it in GitHub Desktop.
Save disjukr/313105cbca8dcbc9bd98 to your computer and use it in GitHub Desktop.

Promise, async / await

안녕하세요, 이번 아는만큼 세미나에서는 블로킹과 논블로킹의 차이, 그리고 논블로킹의 콜백 지옥에 대해서 알아본 뒤, 콜백 지옥을 탈출할 수 있도록 돕는 Promise 객체와 async / await 문법을 살펴보겠습니다.

블로킹, 논블로킹

function blocking_wait(sec, callback) {
    var start_time = +new Date;
    while (+new Date < (start_time + sec * 1000));
    callback();
}
console.log('1초 기다려 볼까요?');
blocking_wait(1, function () {
    console.log('1초가 지났어요!');
});
function non_blocking_wait(sec, callback) {
    setTimeout(callback, sec * 1000);
}
console.log('1초 기다려 볼까요?');
non_blocking_wait(1, function () {
    console.log('1초가 지났어요!');
});

위쪽은 블로킹, 아래쪽은 논블로킹으로 작성된 코드입니다. 둘 다 1초 기다린 후, 1초가 지났어요!를 출력합니다.

차이점

blocking_wait이 돌아가는 동안은 while 루프가 실행흐름을 잡고 놓아주지 않기 때문에 다른 이벤트 루프가 처리될 수 없습니다. blocking_wait(1, callback)을 실행하면 callback이 호출되기 전까지는 -- 그러니까 1초 동안은 -- 사용자가 페이지를 스크롤 해도, 링크를 클릭해도 브라우저가 아무런 반응을 하지 않는 것이죠. 이렇게 실행흐름을 잡고 놓아주지 않는 것을 블로킹이라고 합니다. 반면에 non_blocking_wait(1, callback)을 실행하면 callback이 아직 호출되지 않았어도, 사용자가 페이지를 스크롤해서 아래에 무슨 내용이 있는지 확인해볼 수 있고, 링크를 클릭해서 다음 페이지로 넘어갈 수도 있습니다.

코드를 블로킹으로 작성하게 되는 이유

지금 웹오피스 프로젝트를 살펴보면 꽤 많은 동기 ajax 요청이 있습니다. ajax 요청을 동기로 날리면 서버에서 응답을 주기 전까지는 브라우저 탭이 얼어버리게 되죠. 이미지 업로드를 위해서 서버에 요청을 날리는 동안 플래시의 클릭 이벤트 문맥이 끊기지 않게 하기 위해서 라던지 등, 어쩔 수 없이 그렇게 짜게 된 부분도 몇 군데 있다고는 들었지만, 지금 사용중인 모든 동기 ajax 요청들이 전부 그런 사정으로 인해 작성된 것은 아닙니다. 저는 그 것이 콜백 지옥을 피하고 싶기 때문에, 웹페이지가 잠시 멈추는 것을 감수하고서 동기 ajax 요청을 작성한 것이라고 생각하고 있습니다.

콜백 지옥

사실 제가 처음에 보여준 blocking_wait은 동기 함수의 장점을 잠시 감추기 위해 callback 인자를 받아서, sec초 만큼 기다린 뒤에 실행될 로직을 무조건 callback에 기술해야하는 것처럼 작성해놨습니다.

하지만 그럴 필요가 없습니다. blocking_wait은 실행 흐름을 잡고 놓아주지 않으니, 콜백을 작성하는 대신 blocking_wait(1)을 호출하는 코드 바로 아래에 1초 뒤에 실행될 코드를 적어도 의도한 대로 작동하겠죠.

blocking_wait(1);
console.log('1초가 지났어요!');
blocking_wait(1);
console.log('2초가 지났어요!');
blocking_wait(1);
console.log('3초가 지났어요!');

위 코드는 blocking_wait를 아무리 호출하더라도, 같은 블록 안에서 다음에 실행되어야 할 코드를 쉽게 찾을 수 있는 장점이 있습니다.

반면 non_blocking_wait에서는 위와 같이 작성하면, 1초가 지났어요!라는 문구가 실제로 코드 실행후 1초 지나고나서 출력되는 대신, 코드를 실행함과 동시에 출력되기 때문에, 원하는 시점에 문구를 출력하기 위해서는 콜백을 걸어주어야만 합니다. 따라서 논블로킹에서는 한 작업이 끝난 다음에 실행되어야 하는 작업 또한 논블로킹인 경우 들여쓰기가 더욱 깊어지게 되고, 결국 코드의 가독성이 현저하게 떨어지게 됩니다.

non_blocking_wait(1, function () {
    console.log('1초가 지났어요!');
    non_blocking_wait(1, function () {
        console.log('2초가 지났어요!');
        non_blocking_wait(1, function () {
            console.log('3초가 지났어요!');
        });
    });
});

Promise

프로미스 객체는 es6에 새로 들어온 자바스크립트 스펙으로. 미래에 존재할 거라고 예상되는 값을 나타내는 값입니다. 값으로써 존재하기 때문에, 비동기 함수에서 콜백을 받는 대신 동기 함수처럼 프로미스 객체를 곧바로 반환하게 만들 수 있습니다.

프로미스 객체를 반환하는 wait 함수를 구현해보겠습니다. Promise 생성자는 resolvereject를 인자로 받는 함수를 인자로 받습니다. resolve 인자는 특정 조건을 만족했을 경우, reject 인자는 에러가 발생했을 경우 호출할 함수입니다:

function promise_wait(sec) {
    return new Promise(function (resolve, reject) {
        setTimeout(resolve, sec * 1000);
    });
}

반환된 프로미스 객체는 thencatch 메서드를 갖고 있습니다. thenresolve됐을 경우, catchreject됐을 경우에 호출될 함수를 인자로 받습니다.

프로미스를 반환하는 promise_wait 함수를 사용해서, 아까와 같이 연달아 호출하는 코드에 대해 다음과 같이 작성할 수 있습니다:

promise_wait(1).then(function () {
    console.log('1초가 지났어요!');
    return promise_wait(1);
}).then(function () {
    console.log('2초가 지났어요!');
    return promise_wait(1);
}).then(function () {
    console.log('3초가 지났어요!');
});

프로미스의 then, catch 체인을 활용하면 들여쓰기가 더욱 깊어지지 않기 때문에 코드의 가독성을 해치지 않을 수 있게 됩니다.

하지만 프로미스가 완벽한 것은 아닙니다. 에러 처리를 위해 try...catch 문을 사용할 수 없게 되고(그대신 catch 체인을 활용해야 합니다), blocking_wait를 사용했을 때는 쉽게 작성할 수 있는, 다음과 같은 코드를 작성하기가 까다롭습니다:

var wait_functions = [
    blocking_wait.bind(this, 1),
    blocking_wait.bind(this, 1),
    blocking_wait.bind(this, 1)
];
for (var i = 0; i < wait_functions.length; ++i) {
    wait_functions[i]();
    console.log((i + 1) + '초가 지났어요!');
}

함수가 들어있는 배열을 돌면서 이전 항목의 실행이 종료되면 다음 항목을 호출하고 싶을 때, 블로킹 함수들의 배열은 평상시처럼 for 문을 돌면서 각 항목을 호출시켜주면 되지만, 프로미스를 반환하는 함수들의 배열의 경우에는 바로 for문을 사용할 수 없고, Array.prototype.reduce를 돌려서 처리하는 식으로 우회해야 합니다:

var wait_functions = [
    promise_wait.bind(this, 1),
    promise_wait.bind(this, 1),
    promise_wait.bind(this, 1)
];
wait_functions.reduce(function (prev, curr, i) {
    return prev.then(function () {
        return curr().then(function () {
            console.log((i + 1) + '초가 지났어요!');
        });
    });
}, Promise.resolve());

아무래도 기존 문법에 대응하는 함수형 프로그래밍 기법을 따로 배워야 하기 때문에 프로미스만으로는 아직 콜백 지옥에서 완전히 벗어났다고 볼 수 없습니다.

async, await

아직은 표준이 아닌 es7의 초안 스펙인 async 키워드와 await 연산자에 대해서 알아보겠습니다. 함수를 async 키워드를 붙여서 선언할 수 있습니다. async 키워드가 붙은 함수는 프로미스 객체를 반환합니다. 함수 안에서 값을 반환하면 그 값이 반환되는 프로미스 객체에서 resolve되고, 함수 안에서 예외가 발생하면 에러가 reject되는 것입니다.

await 연산자는 async 함수 블록 안에서만 사용할 수 있는 특별한 단항연산자입니다. 우항의 값이 프로미스 객체인 경우, 프로미스가 resolvereject될 때까지 함수 블록의 평가를 멈추고 기다립니다. 얼핏 블로킹 로직과도 비슷해 보이지만, await 연산자는 논블로킹으로 처리되기 때문에 기다리는 동안 다른 이벤트 루프가 돌 수 있습니다(!) resolve되면 값을 반환하고, reject되면 에러를 던지는 식입니다. 우항이 프로미스가 아닌 경우에는 그 값을 바로 반환합니다.

await 연산자의 특성 덕분에, 프로미스를 반환하는 비동기 함수를 동기 함수처럼 호출할 수 있게 되었습니다:

async function main() {
    await promise_wait(1);
    console.log('1초가 지났어요!');
    await promise_wait(1);
    console.log('2초가 지났어요!');
    await promise_wait(1);
    console.log('3초가 지났어요!');
}
main();

for문, try...catch문과도 섞어쓸 수 있습니다:

async function main() {
    var wait_functions = [
        promise_wait.bind(this, 1),
        promise_wait.bind(this, 1),
        promise_wait.bind(this, 1)
    ];
    for (var i = 0; i < wait_functions.length; ++i) {
        await wait_functions[i]();
        console.log((i + 1) + '초가 지났어요!');
    }
}
main();

기다리는 동안 다른 이벤트 루프도 돌 수 있습니다:

async function main() {
    setTimeout(function () {
        console.log('짠!');
    });
    await promise_wait(1000);
    console.log('이건 1000초나 지나서야 찍히겠지');
}
main();
@tnraro
Copy link

tnraro commented Jul 6, 2015

맨 마지막 코드 하이라이팅이 빠졌어요!

@disjukr
Copy link
Author

disjukr commented Jul 6, 2015

헐 그렇군여 고쳤습니당

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment