IT/언어

[JavaScript] async/await 입문

개발자 두더지 2023. 3. 5. 21:49
728x90

일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 있을 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.

 

시작전에


 JavaScript의 async/await에 관련된 포스트로, 다음의 사람을 대상으로한다.

  • 지금까지 $.Deferred()나 Promise등으로 비동기 처리를 쓴 적이 있지만, async/awit에 대해서 잘 모른다.
  •  $.Deferred()나 Promise등의 비동기 처리 작성법보다 더 간단하게 쓸 수 있는 방법이 있다면 알고싶다.
  • 지금까지의 비동기 처리 작성법에 비교해서 좋은 점을 잘 모르겠다.

 $.Deferred()나 Promise등으로 비동기 처리를 작성했던 경험이 있는 것을 전제로하므로, 비동기 처리 자체에 대한 설명을 이번에 생략한다.

 기재된 이용 예의 코드는 Chrome의 콘솔 상에 움직이므로, 콘솔 상에서 실행하여 동작을 확인해보면, 더욱 이해하기 쉬울 것이라고 생각한다.

 

 

이번 포스트에서 사용하고 있는 용어 정리


Promise 반환한다.

Promise 오브젝트를 반환하는 것.

// Promise를 반환
return new Promise((resolve, reject) => {

});

 

Promise의 결과를 반환한다.

Promise의 resolve 혹은 reject를 실행하는 것.

return new Promise((resolve, reject) => {
    // Promise의 결과를 반환
    resolve('resolve!!');
});

 

resolve 한다.

Promise의 reslove를 실행하는 것.

return new Promise((resolve, reject) => {
    // succes!!를 resolve한다.
    resolve('succes!!');
});

 

reject한다.

Promise의 reject를 실행하는 것.

return new Promise((resolve, reject) => {
    // err!!를 reject한다.
    reject('err!!');
});

 

 

async/await 이란?


 async이란?

 비동기 함수를 정의하는 함수 선언이다. 아래와 같이 함수의 전에 async를 선언하는 것으로 비동기 함수(async function)를 정의할 수 있다.

async function sample() {}

 

async function(async로 선언한 함수)는 무엇을 하는가?

  • async function은 호출되면 Promise를 반환한다.
  • async function가 값을 return한 경우, Promise는 반환값을 resolve한다.
  • async function가 예외나 무언가의 값을 throw한 경우 그 값을 reject한다.

 말뿐만으로는 잘 이해하기 힘드므로 예시를 살펴보자. 아래는 async function가 Promise를 반환해, 값을 resolve 혹은 reject하는가를 확인하기위한 예이다. 참고로, 이렇게 이용하는 경우는 거의 없지만, async function가 어떤 동작을 하는지를 확인하기 위해 기재한 코드이다.

// resolve!!를 return하고 있으므로, 이 값이 resolve된다.
async function resolveSample() {
    return 'resolve!!';
}

// resolveSample가 Promise를 반환해, resolve!!가 resolve되므로,
// then()이 실행되므로, 콘솔에 resolve!!가 표시된다.
resolveSample().then(value => {
    console.log(value); // => resolve!!
});


// reject!!를 throw하고 있으므로, 그 값이 reject된다.
async function rejectSample() {
    throw new Error('reject!!');
}

// rejectSample가 Promise를 반환해, reject!!가 reject되므로,
// catch()가 실행되어 콘솔에 reject!!가 표시된다.
rejectSample().catch(err => {
    console.log(err); // => reject!!
});


// resolveError는 async function가 아니므로 Promise를 반환된다.
function resolveError() {
    return 'resolveError!!';
}

// resolveError는 Promise를 반환하지 않으므로, 에러가 발생해 움직이지 않는다
// Uncaught TypeError: resolveError(...).then is not a function
resolveError().then(value => {
    console.log(value);
});

 위 처럼 async function가 Promise를 반환해, 값을 resolve 혹은 reject하고 있는 것을 알 수 있다. 그리고 async function 하나만 이용하는 예이지만, await와 병행해서 이용하는 경우가 많아 "async를 이용한다고 하면 await도 반드시 이용해야한다"라고 써 있는 포스트 글이 많다.

 

await이란?

async function내에서 Promise의 결과(resolve, reject)를 반환할 때까지 대기하는 (처리를 일시 중시하는) 연산자이다. 아래와 같이 함수 전에 await를 지정하는 것으로 그 함수의 Promise 결과가 반환될 때까지 대기한다.

async function sample() {
    const result = await sampleResolve();
    
    // sampleResolve()의 Promise결과가 반환될 때까지 아래는 실행되지 않는다.
    console.log(result);
}

 

await는 무엇을 하나?

  • await를 지정한 함수의 Promise의 결과가 반환될 때까지 async fuction내의 처리를 일시 정지한다.
  • 결과가 반환되면 async fuction내의 처리를 재개한다.

 await는 async fuction내에서가 아니면 이용되지 않으므로, async/await의 이용 예를 살펴보자.

 

async/await의 이용예

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value * 2);
        }, 2000);
    })
}

/**
 * sampleResolve()를 await하고 있으므로, Promise의 결과가 돌아올때까지 처리가 일시중지된다.
 * 이번의 경우, 2초 후에 resolve(10)이 반환되어, 그 후의 처리(return result + 5;)가 재개된다.
 * result에는 resolve된 10가 저장되어 있으므로, result + 5 = 15가 return반환된다.
 */
async function sample() {
    const result = await sampleResolve(5);
    return result + 5;
}

sample().then(result => {
    console.log(result); // => 15
});

 위 코드의 처리를 Promise의 구문으로 쓰면 다음과 같다.

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value * 2);
        }, 2000);
    })
}

function sample() {
    return sampleResolve(5).then(result => {
        return result + 5;
    });
}

sample().then(result => {
    console.log(result); // => 15
});

 두개를 비교하면 async/await의 쪽이 깔끔한 것을 알 수 있다. 보다 복잡한 경우도 async/await를 이용한 경우가 더욱 깔끔하게 쓸 수 있으므로 몇 가지 예를 소개하도록 하겠다.

 

 

Promise와 async/await의 비교 예시


연속된 비동기 처리

Promise 구문

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 1000);
    })
}

function sample() {
    let result = 0;

    return sampleResolve(5)
        .then(val => {
            result += val;
            return sampleResolve(10);
        })
        .then(val => {
            result *= val;
            return sampleResolve(20);
        })
        .then(val => {
            result += val;
            return result;
        });
}

sample().then((v) => {
    console.log(v); // => 70
});

async/await 구문

await를 이용하면 then()으로 처리를 연결하지 않아도 연계해서 비동기 처리를 쓸 수 있다.

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 1000);
    })
}

async function sample() {
    return await sampleResolve(5) * await sampleResolve(10) + await sampleResolve(20);
}

async function sample2() {
    const a = await sampleResolve(5);
    const b = await sampleResolve(10);
    const c = await sampleResolve(20);
    return a * b + c;
}

sample().then((v) => {
    console.log(v); // => 70
});

sample2().then((v) => {
    console.log(v); // => 70
});

 for를 이용한 반복 비동기 처리도 쓸 수 있다.

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 1000);
    })
}

async function sample() {
    for (let i = 0; i < 5; i += 1) {
        const result = await sampleResolve(i);
        console.log(result);
    }

    return '루프가 끝났다.'
}

sample().then((v) => {
    console.log(v); // => 0
                    // => 1
                    // => 2
                    // => 3
                    // => 4
                    // => 루프가 끝났다.
});

array.reduce()도 이용할 수 있다.

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 1000);
    })
}

async function sample() {
    const array = [5, 10, 20];
    const sum = await array.reduce(async (sum, value) => {
      return await sum + await sampleResolve(value) * 2;
    }, 0);

    return sum;
}

sample().then((v) => {
    console.log(v); // => 70
});

 연속 비동기 처리는 처리를 순서대로 할 필요가 없는 한 이용해서는 안 되므로 주의해야한다. 예를 들어, 이미지를 비동기로 읽어들이는 경우 위와 같은 처리라면 1개의 이미지 읽어들이기가 끝날 때 까지 다음 이미지의 읽어들이기가 시작되지 않는다.

 그러므로 모든 이미지의 읽어들이기에 꽤 시간이 걸린다. 이미지는 연속하여 읽어들이기가 필요하지 않으므로 후술할 비동기 처리로 읽어들여야한다.

 

병렬 비동기 처리

Promise 구문

function sampleResolve(value) {
  return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 2000);
    })
}

function sampleResolve2(value) {
  return new Promise(resolve => {
        setTimeout(() => {
            resolve(value * 2);
        }, 1000);
    })
}

function sample() {
    const promiseA = sampleResolve(5);
    const promiseB = sampleResolve(10);
    const promiseC = promiseB.then(value => {
        return sampleResolve2(value);
    });
    
    return Promise.all([promiseA, promiseB, promiseC])
        .then(([a, b, c]) => {
            return [a, b, c];
        });
}

sample().then(([a, b, c]) => {
    console.log(a, b, c); // => 5 10 20
});

async/await 구문

 Promise.all에서도 await를 이용할 수 있으므로 아래와 같이 작성할 수 있다.

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 2000);
    })
}

function sampleResolve2(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value * 2);
        }, 1000);
    })
}

async function sample() {
    const [a, b] = await Promise.all([sampleResolve(5), sampleResolve(10)]);
    const c = await sampleResolve2(b);
    
    return [a, b, c];
}

sample().then(([a, b, c]) => {
    console.log(a, b, c); // => 5 10 20
});

 array.map()도 이용할 수 있다.

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 2000);
    })
}

async function sample() {
    const array =[5, 10, 15];
    const promiseAll = await Promise.all(array.map(async (value) => {
        return await sampleResolve(value) * 2;
  }));

    return promiseAll;
}

sample().then(([a, b, c]) => {
    console.log(a, b, c); // => 10 20 30
});

 

예외 처리(에러 바인딩)

Promise 구문

function throwError() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            try {
                throw new Error('에러가 있었다.');
                resolve('에러가 없었다.');
            } catch(err) {
                reject(err);
            }
        }, 1000);
    });
}

function errorHandling() {
    return throwError()
        .then((result) => {
            return result;
        })
        .catch((err) => {
            throw err;
        });
}

errorHandling().catch((err) => {
    console.log(err); // => 에러가 있었다.
});

async/await 구문

 await를 이용하면 비동기 처리의 try catch를 작성할 수 있다.

function throwError() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            try {
                throw new Error('에러가 있었다.')
                resolve('에러가 없었다.');
            } catch(err) {
                reject(err);
            }
        }, 1000);
    });
}

async function errorHandling() {
    try {
        const result = await throwError();
        return result;
    } catch (err) {
        throw err;
    }
}

errorHandling().catch((err) => {
    console.log(err); // => 에러가 있었다.
});

 


참고자료

https://ja.javascript.info/async-await

https://qiita.com/soarflat/items/1a9613e023200bbebcb3

728x90