비동기 처리: 전체 통합



0. 왜 비동기 처리가 필요한가?

JavaScript는 기본적으로 동기적으로 작동합니다. 즉, 코드가 위에서 아래로 순차적으로 실행됩니다. 이러한 동기적 실행은 다음과 같은 문제를 일으킬 수 있습니다.

  • 오래 걸리는 작업이 실행되는 동안 웹 페이지의 UI가 멈추게 됩니다. (UI 멈춤)
  • 네트워크 요청이나 파일 읽기 같은 작업이 오래 걸리면 사용자는 웹 페이지가 응답하지 않는다고 느낄 수 있습니다. (사용자 경험 저하)
  • 동기적 코드가 계속해서 차례로 실행되므로, 병렬로 실행할 수 있는 작업도 순차적으로 실행되어 성능이 저하됩니다. ( 성능 저하)

간단히 말해 동기적으로 작동하면 계산되는 사이 화면이 멈추는 등의 문제가 생길 수 있지만, 비동기 처리를 하면 복잡한 계산이 처리되는 사이 계산이 완료되길 기다리지않고 다음 작업을 할 수 있습니다. 이로 인해 UI가 멈추지 않고, 사용자 경험이 향상되며, 성능이 개선됩니다.



1. 필요한 이유, 코드로 보기

// 1. 동기적으로 데이터를 처리
function processData(data) {
    console.log('데이터를 동기적으로 처리 중입니다...');
    // 오랜 시간이 걸리는 계산 예시
    for (let i = 0; i < 1000000000; i++) {
    }
    console.log('데이터를 동기적으로 처리 완료했습니다.');
    return `처리된 데이터: ${data}`;
}


// 2. 비동기적으로 데이터를 처리 (for문을 Promise 내부로 이동)
function processData(data) {
    console.log('데이터를 비동기적으로 처리 중입니다...');

    return new Promise((resolve, reject) => {

        setTimeout(() => { // 3초 후에 작업 시작
            console.log('데이터를 비동기적으로 처리 완료했습니다.');

            // 오래 걸리는 계산(for문)
            console.log('오래 걸리는 계산을 시작합니다...');
            for (let i = 0; i < 1000000000; i++) {
            }
            console.log('오래 걸리는 계산을 완료했습니다.');

            resolve(`처리된 데이터: ${data}`);

        }, 3000); 
    });
}

// processData 함수 호출
async function processData_async() {
    console.log('비동기 처리를 시작합니다.');
    try {
        const result = await processData('예시 데이터');
        console.log(result);
    } catch (error) {
        console.error('비동기 처리 중 오류 발생:', error);
    }
}

processData_async();

--------------------------------------------------------
// 동기 처리 결과
데이터를 동기적으로 처리 중입니다...
// (오래 걸리는 계산으로 인해 멈춤)
데이터를 동기적으로 처리 완료했습니다.
처리된 데이터: 예시 데이터

--------------------------------------------------------
// 비동기 처리 결과
비동기 처리를 시작합니다.
데이터를 비동기적으로 처리 중입니다...
// (3초, 이때 다른 코드 처리하여 출력함)
데이터를 비동기적으로 처리 완료했습니다.
오래 걸리는 계산을 시작합니다...
오래 걸리는 계산을 완료했습니다.
처리된 데이터: 예시 데이터

--------------------------------------------------------



2. 비동기 처리 방법


1) 콜백 함수 (Callback Function)

(1) 개념

콜백 함수는 다른 함수의 인수로 전달되어 특정 작업이 완료된 후 실행되는 함수입니다. 비동기 작업이 끝난 후에 어떤 작업을 수행하도록 콜백 함수를 전달합니다. 콜백 함수는 비동기 작업이 완료되었을 때 호출되어 그 결과를 처리합니다.

(2) 실행 순서

  • 주 함수가 실행됩니다.
  • 콜백 함수를 인수로 받는 비동기 함수가 호출됩니다.
  • 비동기 함수는 작업이 완료되면 콜백 함수를 호출합니다.
  • 콜백 함수가 실행됩니다.

(3) 예제 코드

function goMart() {
    console.log('마트에 가서 어떤 음료를 살지 고민한다..');
}

function pickDrink(callback) {
    setTimeout(function() {
        console.log('고민 끝');
        const product = '제로콜라';
        const price = 3000;
        callback(product, price);
    }, 3000);
}

function pay(product, price) {
    console.log(`상품명: ${product} // 가격: ${price}`);
}

goMart();
pickDrink(pay);

(4) 분석

  • goMart 함수가 호출되어 “마트에 가서 어떤 음료를 살지 고민한다..”라는 메시지가 출력됩니다.
  • pickDrink 함수가 호출됩니다. 이 함수는 3초 후에 내부의 setTimeout 콜백 함수를 실행합니다.
  • setTimeout 콜백 함수에서는 “고민 끝”이라는 메시지를 출력하고, product 변수에 ‘제로콜라’, price 변수에 3000을 할당합니다.
  • pickDrink 함수의 콜백으로 전달된 pay 함수가 호출됩니다. 이때 productprice 인자로 ‘제로콜라’와 3000이 전달됩니다.
  • pay 함수가 실행되어 “상품명: 제로콜라 // 가격: 3000”이라는 메시지가 출력됩니다.

(5) 장단점

  • 장점: 간단하고 직관적입니다. 작은 비동기 작업에는 적합합니다.
  • 단점: 콜백 지옥(Callback Hell)으로 불리는 중첩 구조가 생기기 쉬워 가독성이 떨어지고, 코드가 복잡해질 수 있습니다.


2) Promise

(1) 개념

Promise는 비동기 작업의 성공 또는 실패를 처리하는 객체입니다. then 메서드로 작업이 성공했을 때의 결과를 처리하고, catch 메서드로 실패했을 때의 에러를 처리합니다. Promise는 작업이 성공(resolve)하거나 실패(reject)하는 경우를 설정하여 처리합니다.

(2) 실행 순서

  • Promise 객체가 생성됩니다.
  • 비동기 작업이 실행됩니다.
  • 작업이 성공하면 resolve가 호출되고, 실패하면 reject가 호출됩니다.
  • then 또는 catch 메서드로 결과를 처리합니다.

(3) 예제 코드

function goMart() {
    console.log('마트에 가서 어떤 음료를 살지 고민한다..');
}

function pickDrink() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('고민 끝');
            const product = '제로콜라';
            const price = 3000;
            if (product) {
                resolve({ product, price });
            }

            // reject 예시, 현재 코드에서는 product가 항상 있으므로 굳이 필요없음.
            // else {
            //     reject(new Error('제품을 찾을 수 없습니다.')); 
            // }
        }, 3000);
    });
}

function pay({ product, price }) {
    console.log(`상품명: ${product} // 가격: ${price}`);
}

goMart();
pickDrink()
    .then(pay)
    .catch(function(err) {
        console.error(err);
    });

(4) 분석

  • goMart 함수가 호출되어 “마트에 가서 어떤 음료를 살지 고민한다..”라는 메시지가 출력됩니다.
  • pickDrink 함수가 호출됩니다. 이 함수는 새로운 Promise를 반환합니다. 이 함수는 setTimeout을 통해 3초 후에 비동기 작업을 시작합니다.
  • 3초 후 setTimeout의 콜백 함수가 실행됩니다. “고민 끝”이라는 메시지가 출력되고, product 변수에 ‘제로콜라’, price 변수에 3000을 할당합니다.
  • pickDrink 함수의 내부에서 resolve가 호출되어 Promise가 성공 상태가 됩니다.
  • then 메서드에 전달된 pay 함수가 호출되며, productprice를 인자로 받아 “상품명: 제로콜라 // 가격: 3000”이라는 메시지가 출력됩니다.
  • Promise가 실패하지 않았으므로 catch 블록은 실행되지 않습니다.

(5) 장단점

  • 장점: 가독성이 좋고 체인 형식으로 비동기 작업을 처리할 수 있습니다. 비동기 작업을 단계적으로 연결할 수 있어 복잡한 작업을 쉽게 처리할 수 있습니다.
  • 단점: 여전히 콜백 지옥을 완전히 피하지 못할 수 있으며, 많은 thencatch를 사용하면 코드가 길어질 수 있습니다.


3) async/await

(1) 개념

async/await는 Promise를 더 간결하고 동기적으로 보이게 작성할 수 있는 문법입니다. async 키워드는 함수 앞에 붙여 해당 함수가 Promise를 반환하도록 하고, await 키워드는 Promise가 해결될 때까지 기다립니다. await 키워드는 async 함수 내에서만 사용할 수 있습니다.

(2) 실행 순서

  • async 함수가 호출됩니다.
  • 함수 내에서 await 키워드로 비동기 작업을 기다립니다.
  • 비동기 작업이 완료되면 다음 코드가 실행됩니다.

(3) 예제 코드

function goMart() {
    console.log('마트에 가서 어떤 음료를 살지 고민한다..');
}

function pickDrink() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('고민 끝');
            const product = '제로콜라';
            const price = 3000;
            resolve({ product, price });
        }, 3000);
    });
}

function pay({ product, price }) {
    console.log(`상품명: ${product} // 가격: ${price}`);
}

async function exec() {
    goMart();
    try {
        const result = await pickDrink();
        pay(result);
    } catch (err) {
        console.error(err);
    }
}

exec();

// 아래 코드처럼 더 직관적으로 작성도 가능함.
// async function exec() {
//     try {
//         goMart();
//         await pickDrink();
//         pay();
//     }
//     catch(err) {
//         console.error(err);
//     }
// }

// exec();

(4) 분석

  • exec 함수는 async 키워드로 정의되어 있습니다. 이 함수는 Promise를 반환하는 비동기 함수가 됩니다.
  • exec 함수가 호출되면 먼저 goMart 함수가 실행되어 “마트에 가서 어떤 음료를 살지 고민한다..”라는 메시지를 출력합니다.
  • await 키워드가 pickDrink 함수 앞에 붙어 있으므로 이 함수가 반환하는 Promise가 해결될 때까지 기다립니다. 3초 후 “고민 끝”이라는 메시지가 출력되고, productprice가 설정된 객체가 반환됩니다.
  • await 뒤의 Promise가 해결되면 pay 함수가 호출되어 “상품명: 제로콜라 // 가격: 3000”이라는 메시지를 출력합니다.
  • try...catch 블록을 사용하여 비동기 작업 중 발생할 수 있는 에러를 처리합니다. 이 예제에서는 reject가 호출되지 않으므로 catch 블록은 실행되지 않습니다.

(5) 장단점

  • 장점: 가독성이 매우 좋고, 동기 코드처럼 작성할 수 있습니다. 비동기 작업의 흐름을 쉽게 이해할 수 있습니다.
  • 단점: 최신 문법이므로 오래된 브라우저에서는 지원되지 않을 수 있습니다.