[JavaScript] async/await 문법

async 함수

async 키워드는 함수 선언 시 function 키워드 앞에 위치하며, 해당 함수를 비동기 함수로 정의합니다.

  • 암묵적 프라미스 반환: async 함수는 명시적으로 프라미스를 반환하지 않더라도, 항상 프라미스를 반환합니다. 함수가 일반 값을 반환하는 경우, JavaScript 엔진은 해당 값을 Promise.resolve(value) 로 래핑하여 이행 상태(resolved)의 프라미스로 자동 변환합니다.
  • await 키워드 활성화: async 함수 내부에서는 await 키워드를 사용하여 비동기 작업의 완료를 대기하고, 결과를 동기적으로 처리할 수 있습니다.
// async 키워드를 사용하여 비동기 함수 선언
async function asynchronousFunction() {
    // 일반 값 반환 (프라미스로 자동 래핑)
    return "Async 작업 완료";
}

// async 함수 호출은 항상 프라미스를 반환
asynchronousFunction().then(result => {
    console.log(result); // 출력: Async 작업 완료
});

await 표현식

async 함수 내부에서만 사용될 수 있으며, 프라미스 기반 비동기 연산의 결과를 동기적으로 처리하는 핵심적인 역할을 담당합니다.

  • 프라미스 처리 대기: await 키워드를 만나면, JavaScript 엔진은 해당 프라미스가 이행(resolve)되거나 거부(reject)될 때까지 async 함수의 실행을 일시 중단합니다. 이는 마치 동기 코드의 실행 흐름이 블로킹(blocking) 되는 것처럼 보이지만, 실제로는 엔진이 이벤트 루프를 통해 다른 작업을 처리할 수 있도록 양보(yield)합니다.
  • 이행 값 반환 또는 예외 던지기: 대기 중인 프라미스가 이행되면, await 표현식은 프라미스의 이행 값(resolved value)을 반환하고, async 함수의 실행이 재개됩니다. 반대로 프라미스가 거부되면, await 표현식은 거부 이유(rejection reason)를 예외(exception)로 던집니다. 이는 동기 코드의 throw 문과 유사하게 동작하며, try...catch 블록을 통해 예외를 처리할 수 있습니다.
async function fetchDataAndProcess() {
    try {
        // fetch API를 사용하여 비동기 데이터 요청
        const response = await fetch('https://api.example.com/data'); 
        
        // await 키워드는 response.json() 프라미스가 이행될 때까지 대기
        const jsonData = await response.json(); 

        console.log("Fetch 데이터:", jsonData);
        return jsonData; // 최종 결과 반환

    } catch (error) {
        console.error("데이터 Fetch 실패:", error);
        // 예외 처리: 에러 로깅, 폴백 값 반환 등
        return { error: error.message }; 
    }
}

fetchDataAndProcess().then(result => {
    console.log("Async 함수 결과:", result);
});

async/await 와 프라미스 체이닝의 비교

  • 가독성: async/await 는 비동기 코드를 동기 코드와 유사한 선형적인 구조로 표현하여 코드의 가독성증가합니다.
  • 에러 핸들링: async/await 는 동기 코드의 예외 처리 방식과 동일한 try...catch 블록을 사용하여 비동기 에러를 처리할 수 있도록 합니다.
  • 디버깅 용이성: async/await 함수 내에서는 일반적인 동기 코드처럼 단계별 디버깅이 가능합니다.
// Promise.then 체이닝 방식 (복잡하고 가독성 저하)
function fetchDataChaining() {
    return fetch('https://api.example.com/data')
        .then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            return response.json();
        })
        .then(jsonData => {
            console.log("Data:", jsonData);
            return jsonData;
        })
        .catch(error => {
            console.error("Error:", error);
            return { error: error.message };
        });
}

// async/await 방식 (간결하고 가독성 우수)
async function fetchDataAsyncAwait() {
    try {
        const response = await fetch('https://api.example.com/data');
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        const jsonData = await response.json();
        console.log("Data:", jsonData);
        return jsonData;
    } catch (error) {
        console.error("Error:", error);
        return { error: error.message };
    }
}

async/await 와 Promise.all 의 조합

async/await 는 Promise.all 과 함께 사용하여 병렬적인 비동기 작업을 효율적으로 관리합니다. Promise.all 은 여러 개의 프라미스를 동시에 실행하고, 모든 프라미스가 이행될 때까지 기다린 후, 결과를 배열로 반환하는 기능을 제공합니다. await 와 함께 사용하면, 병렬 작업의 결과를 동기적인 방식으로 순차적으로 처리할 수 있습니다.

async function fetchMultipleData() {
    try {
        // Promise.all 을 사용하여 병렬적으로 데이터 fetching
        const [data1, data2, data3] = await Promise.all([
            fetch('https://api.example.com/data1').then(res => res.json()),
            fetch('https://api.example.com/data2').then(res => res.json()),
            fetch('https://api.example.com/data3').then(res => res.json())
        ]);

        console.log("Data 1:", data1);
        console.log("Data 2:", data2);
        console.log("Data 3:", data3);

        return { data1, data2, data3 }; // 통합된 결과 반환

    } catch (error) {
        console.error("데이터 Fetch 중 에러 발생:", error);
        return { error: error.message };
    }
}

fetchMultipleData().then(combinedData => {
    console.log("Combined Data:", combinedData);
});

async/await 에러 핸들링 심층 분석

async/await 는 프라미스 거부(rejection)를 예외(exception)로 변환하여 동기적인 try...catch 블록으로 에러를 처리할 수 있도록 합니다. await 표현식이 거부된 프라미스를 만나면, 해당 거부 이유가 throw 되어 catch 블록으로 제어 흐름이 이동합니다.

async function errorHandlingExample() {
    try {
        // 의도적으로 거부(reject)되는 프라미스 생성
        await Promise.reject(new Error("강제 에러 발생!")); 

    } catch (error) {
        console.error("try...catch 블록에서 에러 핸들링:", error.message);
        // 에러 복구 시도, 폴백 값 반환, 사용자 알림 등 
    }
}

errorHandlingExample();

만약 async 함수 내에서 try...catch 블록을 사용하지 않고 프라미스 거부가 발생하면, 해당 거부는 unhandledrejection 이벤트를 발생시킵니다.

async function unhandledRejectionExample() {
    // try...catch 블록 없이 프라미스 거부 발생
    await Promise.reject(new Error("처리되지 않은 거부!")); 
}

window.addEventListener('unhandledrejection', event => {
    console.error("전역 unhandledrejection 핸들러:", event.reason);
    // 애플리케이션 전역 차원의 에러 로깅 및 모니터링
});

unhandledRejectionExample(); // .catch() 핸들러 없이 async 함수 호출