Javascript

JavaScript의 async, await에 대해 깊이 이해하기

드리프트2 2024. 7. 1. 21:36

서론

async/await와 Promise 개념에 대해 완벽하게 이해하지 못했다고 느껴, 좀 더 명확히 파악하기 위해 몇 가지 실험을 진행해 보았습니다.

 

이 글에서는 그 실험들을 통해 얻은 인사이트를 여러분과 공유하고자 합니다.

실험 방법

다음의 네 가지 함수를 사용해 실험을 진행합니다.

실험에 사용할 함수 정의

// Promise를 반환하는 async 함수
async function a() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('a 실행');
            console.timeLog();
            resolve();
        }, 1000);
    });
}

// Promise를 반환하지만 async가 아닌 함수
function b() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('b 실행');
            console.timeLog();
            resolve();
        }, 1500);
    });
}

// Promise를 반환하지 않는 async 함수
async function c() {
    setTimeout(() => {
        console.log('c 실행');
        console.timeLog();
    }, 1700);
}

// 수수께끼의 함수 X
function x() {
    console.log('x 실행');
    console.timeLog();
}

 

각 함수의 특징은 다음과 같습니다.

함수명 로그 출력까지의 시간 async 여부 Promise 반환 여부
a 1000ms 있음 반환
b 1500ms 없음 반환
c 1700ms 있음 반환하지 않음
x 0ms 없음 반환하지 않음

 

위 함수들을 await를 사용하거나 then을 사용하여 실행해보고 결과를 기록합니다.

실험 결과

다양한 방법으로 함수를 실행해본 결과입니다.

아무것도 추가하지 않은 경우

// a는 async가 있고 Promise를 반환하는 함수입니다.
a();
// > time:0 Promise {<pending>}
// time:1000 a 실행

// b는 async가 없고 Promise를 반환하는 함수입니다.
b();
// > time:0 Promise {<pending>}
// time:1500 b 실행

// c는 async가 있지만 Promise를 반환하지 않는 함수입니다.
c();
// > time:0 Promise {<fulfilled>: undefined}
// time:1700 c 실행

// x는 async도 없고 Promise도 반환하지 않는 함수입니다.
x();
// time:0 x 실행
// > time:0 undefined

 

결과를 정리하면 다음과 같습니다.

함수명 반환값 값이 반환되는 시간
a Promise {} 0ms
b Promise {} 0ms
c Promise {: undefined} 0ms
x undefined 0ms

 

then이나 await를 사용하지 않고 실행했을 경우, 값이 반환되는 시간(=처리가 완료되는 시간)은 거의 0입니다.

 

a와 b는 완전히 같은 결과였습니다.

 

c는 a와 b와 반환 값이 달랐습니다.

then을 사용한 경우

// a는 async가 있고 Promise를 반환하는 함수입니다.
a().then(x);
// > time:0 Promise {<pending>};
// time:1000 a 실행
// time:0 x 실행

// b는 async가 없고 Promise를 반환하는 함수입니다.
b().then(x);
// > time:0 Promise {<pending>};
// time:1500 b 실행
// time:0 x 실행

// c는 async가 있지만 Promise를 반환하지 않는 함수입니다.
c().then(x);
// time:0 x 실행
// > time:0 Promise {<fulfilled>: undefined}
// time:1700 c 실행

 

여기까지의 결과를 정리하면 다음과 같습니다.

함수명 반환값 로그의 출력 순서
a Promise {} a → x
b Promise {} b → x
c Promise {: undefined} x → c

 

a, b는 이들 함수가 실행을 마친 후에 x가 실행되었습니다.

 

c는 실행을 마치기 전에 x가 실행되었습니다.

 

또한, 이전과 마찬가지로 a와 b의 실험 결과는 변함이 없었습니다.

 

로그의 출력 순서 측면에서 c만이 실행 완료를 기다리지 않는 것처럼 보입니다.

 

다음은 여러 함수를 then으로 실행해 보겠습니다.

// a와 b를 실행
a().then(b).then(x);
// > time:0 Promise {<pending>};
// time:1000 a 실행
// time:1500 b 실행
// time:0 x 실행

// 실행 순서: a → b → x
// 위의 의미: a가 실행을 마친 후 b가 실행을 시작하고, b가 실행을 마친 후 x가 실행을 시작했습니다.
// b와 c를 실행
b().then(c).then(x);
// > time:0 Promise {<pending>};
// time:1500 b 실행
// time:0 x 실행
// time:1700 c 실행

// 실행 순서: b → c = x
// 위의 의미: b가 실행을 마친 후, c와 x가 동시에 실행을 시작했습니다.
// c와 b의 실행 순서를 바꿔 실행
c().then(b).then(x);
// > time:0 Promise {<pending>};
// time:1500 b 실행
// time:0 x 실행
// time:200 c 실행

// 실행 순서: c = b → x
// x는 b의 실행이 끝난 후에 실행되었습니다.
// a, b, c를 실행
a().then(b).then(c);
// > time:0 Promise {<pending>};
// time:1000 a 실행
// time:1500 b 실행
// time:1700 c 실행

// 실행 순서: a → b → c
// a, c, b를 실행
a().then(c).then(b);
// > time:0 Promise {<pending>};
// time:1000 a 실행
// time:1500 b 실행
// time:200 c 실행

// 실행 순서: a → c = b

 

정리하면 다음과 같습니다.

실행한 함수 실행 순서
a().then(b).then(x) a → b → x
b().then(c).then(x) b → c = x
c().then(b).then(x) c = b → x
a().then(b).then(c) a → b → c
a().then(c).then(b) a → c = b

 

c의 경우에는 =가 들어가 있습니다.

 

즉, c.then(x)를 해도 x는 c의 처리가 끝나기 전에 실행되어 버립니다.

 

참고로 c는 async를 붙이고 있지만 아무것도 반환하지 않는 함수입니다(그러나 async가 붙어 있기 때문에 실제로는 Promise가 반환됩니다).

 

내부에서 setTimeout을 사용하는 경우, 단순히 async를 함수에 붙였다고 해서 처리가 끝날 때까지 기다려 주지 않는 것 같습니다.

await를 사용한 경우

이제는 await를 사용하여 함수를 실행해 보겠습니다.

// a를 실행
await a();
// time:1000 a 실행
// > time:0 undefined

// b를 실행
await b();
// time:1500 b 실행
// > time:0 undefined

// c를 실행
await c();
// > time:0 undefined
// time:1700 c 실행

// x를 실행
await x();
// time:0 x 실행
// > time:0 undefined

 

결과를 정리하면 다음과 같습니다.

함수명 마지막 로그가 출력되는 시간 값이 반환되는 시간
a 1000ms 1000ms
b 1500ms 1500ms
c 1700ms 0ms
x 0ms 0ms

 

a와 b는 값이 반환되기까지 시간이 걸리는 반면, c는 거의 0초 만에 반환됩니다.

 

역시 async를 단순히 붙였을 때는 실행을 기다려 주지 않는 것 같습니다.

결론

실험 결과를 모두 정리하고자 합니다.

 

전제로, 함수 선언의 맨 앞에 async를 붙이면, 그 함수는 Promise를 반환하게 됩니다.

async가 붙으면 Promise를 반환합니다

// async 없음
function notAsync() {
    return 'aaa';
}
notAsync(); // 'aaa'

// async 있음
async function hasAsync() {
    return 'bbb';
}
hasAsync(); // Promise {<fulfilled>: 'bbb'}

효과가 없는 예

먼저, Promise를 반환하는 함수에 await를 추가하면, 함수는 Promise의 결과를 반환하게 됩니다.

 

Promise를 반환하지 않는 함수에 await를 추가하면, 마치 await가 없는 것처럼 함수가 실행됩니다.

 

이 때, 특별한 에러 등은 발생하지 않습니다.

// Promise를 반환하지 않음
function notAsync() {
    return 'aaa';
}
await notAsync(); // 'aaa', 이 await는 마치 없는 것 같음

// Promise를 반환함
async function hasAsync() {
    return 'bbb';
}
await hasAsync(); // 'bbb', 이 await는 Promise를 확인함

 

예를 들어, setTimeout은 Promise를 반환하지 않기 때문에, await setTimeout(() => { 함수 }, 1000)처럼 사용해도, 그 다음에 작성된 처리는 setTimeout 내의 처리가 끝나기 전에 실행됩니다.

// setTimeout에 await를 사용해도 효과 없음
async function test() { // await가 들어간 함수에는 async가 필요합니다.
    await setTimeout(() => { // 이 await는 효과가 없습니다.
        console.log('setTimeout 내');
    }, 1000);
    console.log('setTimeout 후'); // 이게 먼저 출력됩니다.
}

test();
// time:0 setTimeout 후
// > time:0 Promise {<fulfilled>: undefined}
// time:1000 setTimeout 내

 

그리고, 단순히 async를 붙였다고 해서 await가 효과를 발휘하지 않는 경우도 있습니다.

 

예를 들어, 내부에 await를 포함하지 않는 함수에 async를 붙여도 처리 완료를 기다려 주지 않습니다.

// 내부에 await가 없어서 async를 붙여도 효과 없음
async function test() { // 이 async는 효과 없음
    setTimeout(() => {
        console.log('setTimeout 내');
    }, 1000);
}

await test(); // async가 없는 것과 동일한 상태이므로, 이 await는 효과 없음
console.log('test 후'); // 먼저 출력됩니다.
// time:0 test 후
// > time:0 undefined
// time:1000 setTimeout 내

효과가 있는 예

그렇다면 어떤 경우에는 효과가 있을까요?

 

결국 내부에서 Promise를 반환하는 함수라면 어떤 것이든 좋습니다.

 

예를 들어, 특정 시간만큼 대기하는 다음 함수는 setTimeout을 포함하고 있지만, Promise를 반환하고 resolve를 호출하기 때문에 효과가 있습니다.

// 특정 시간 동안 대기하는 함수
function sleep(sec) { // async가 있든 없든 같습니다.
    return new Promise((resolve, reject) => { // Promise를 반환합니다.
        setTimeout(() => { resolve() }, sec); // sec 밀리초 후에 resolve
    });
}

await sleep(1000);
console.log('sleep 후'); // 1000밀리초 후에 출력됩니다.

 

또한, 처음부터 Promise를 반환하는 함수에 대해서도 사용할 수 있습니다.

 

예를 들어 fetch 등이 있습니다.

// await에 효과가 있는 예
const response = await fetch('url');

async와 await에 효과가 있는 예

// url에서 json을 반환하는 함수, 특별한 사용도는 없습니다.
async function getJson(url) {
    const res = await fetch(url); // fetch는 Promise를 반환합니다.
    return res.json();
}

// 사용할 때는 이런 식으로
const { sample } = await getJson('url');

결론

Promise는 매우 유용합니다.

 

이를 사용하면 이전에 할 수 없었던 것들을 할 수 있게 됩니다.

 

또한 async/await를 사용하면 코드를 대폭 줄일 수 있으며, 코드의 중첩도 줄일 수 있습니다.