본문 바로가기

프로그래밍 언어/JavaScript

JavaScript | 비동기 처리 | async & await 키워드

비동기 처리 발전과정 요약

현대의 프론트엔드 개발에서 비동기 프로그래밍은 필수적이다. 서비스는 사용자의 요청에 신속하게 응답하고, 동시에 백그라운드에서 데이터를 로드하고 처리하는 등의 복잡한 작업을 수행해야 하기 때문이다.

 

초기의 자바스크립트에서는 이러한 비동기 작업을 주로 콜백 함수를 통해 처리했다. 그러나 서비스의 규모가 커지고 로직이 복잡할수록 콜백 지옥이라는 부작용이 발생했으며, 이는 가독성 저하와 유지 보수 난이도 상승으로 이어졌다. 

 

이러한 문제를 해결하기 위해 ES6(ECMAscript 2015)에서 Promise가 도입되었다. Promise를 통해 비동기 작업 완료 실행될 작업을 연기하는 것이 수월해졌고(Promise 객체 리턴값을 변수에 담은 후에 나중에 활용하는 식으로), 또한 체이닝을 통해 앞서 말한 콜백 지옥을 효과적으로 잡을 수 있게 되었다. 

 

그러나 Promise도 마냥 직관적이라고 하기에는 조금 어렵다. 특히 에러 처리와 관련한 부분에서는 애로사항이 많았다고 한다. 때문에 비동기 처리와 관련한 코드 작성을 더욱 직관적으로 할 수 있도록, ES8(ECMAscript 2017)에서 asyncawait 문법이 도입되었다. 

 

async 함수

아래 간단한 예시를 통해 async 함수의 정의와 사용법에 대해 알아보자. 

function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms); // 딱히 전달해줄 인자가 없으면, 콜백함수에 바로 resolve 넣어도 됨.
    });
}

// async 함수는 Promise 객체를 리턴한다. 
async function helloAsync(){
    return delay(2000).then(() => {
        return "hello Async";
    });    
};

// 따라서 then(), catch() 등의 메서드를 사용할 수 있다. 
helloAsync().then((res) => {
    console.log(res);
})

 

async 키워드를 함수 선언부 앞에 붙이면, 해당 함수는 Promise 객체를 리턴한다. 

따라서 async 함수의 결과에 then, catch 등의 메서드를 그대로 사용할 수 있다. 

 

여기까지만 보면 '뭐가 다른가?' 싶은데, 

async 함수는 await 키워드와 함께 사용할 때 진짜 빛을 발한다.

 

async 함수: await 키워드와 함께 사용

function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms); // 딱히 전달해줄 인자가 없으면, 콜백함수에 바로 resolve 넣어도 됨.
    });
};

async function helloAsync(){
    await delay(2000);
    return "hello Async";
};

helloAsync().then((res) => {
    console.log(res);
});

 

가운데 helloAsync() 내부의 코드를 보면, 이전 대비 훨씬 직관적으로 읽힌다. 

 

await 키워드는 async 함수 내부에서만 사용할 수 있다. 

await 키워드가 앞에 붙은 비동기 작업은 마치 동기적인 것처럼 실행된다. 

(해당 비동기 작업이 완료된 후에야 바로 아래 코드가 실행될 수 있다.)

 

이는 Promise 체이닝 로직을 매우 직관적으로 보이게 하는 효과를 가져온다.

 

 

여기에 함수 실행 부분까지 async 함수로 대체해주면 다음과 같이 코드를 작성할 수 있다.

function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms); // 딱히 전달해줄 인자가 없으면, 콜백함수에 바로 resolve 넣어도 됨.
    });
};

async function helloAsync(){
    await delay(2000);
    return "hello Async";
};

async function main() {
    const res = await helloAsync();
    console.log(res);
}

main();

 

main() 함수로 진입 -> helloAsync() 에 await 걸렸네? -> helloAsync() 진입 -> delay()에 await 걸려있네? -> delay() 진입 ..

과 같은 식으로 코드를 분석하기 훨씬 편해졌다. 

 

마지막으로, 만일 비동기 작업이 실패할 경우를 대비하여 어떻게 에러처리를 하는지까지 알아보자. 

아래는 try/catch를 활용하여 에러처리까지 수행하는 최종 버전이다.

(사실 로직상 실패할 경우의 수는 없지만, 다른 비동기 작업이라고 가정하고 이해하자.)

function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms); // 딱히 전달해줄 인자가 없으면, 콜백함수에 바로 resolve 넣어도 됨.
    });
};

async function helloAsync() {
    try {
        await delay(2000);
        return "hello Async";
    } catch (error) {
        throw new Error('delay 함수에서 오류 발생');
    }
}

async function main() {
    try {
        const res = await helloAsync();
        console.log(res);
    } catch (error) {
        console.error('오류 발생:', error);
    }
}

main();