프로그래밍/Javascript

자바스크립트 비동기 프로그래밍 완벽 가이드: 콜백, 프로미스, 그리고 async/await

shimdh 2025. 2. 14. 11:50
728x90

비동기 프로그래밍은 현대 웹 개발에서 없어서는 안 될 중요한 개념입니다. 특히, 서버 요청, 데이터베이스 접근, 파일 처리 등의 작업을 수행하는 동안 프로그램이 멈추지 않고 원활하게 작동할 수 있도록 합니다. 이번 포스트에서는 자바스크립트에서 사용되는 비동기 프로그래밍의 핵심 개념인 콜백, 프로미스, 그리고 async/await을 깊이 있게 살펴보겠습니다.


🔹 비동기 프로그래밍이란?

비동기 프로그래밍(Asynchronous Programming)은 코드 실행이 블로킹되지 않고, 특정 작업이 완료될 때까지 기다리지 않도록 설계하는 방식입니다. 이 개념을 이해하려면 동기(Synchronous)와 비동기(Asynchronous)의 차이를 살펴볼 필요가 있습니다.

✅ 동기(Synchronous) vs. 비동기(Asynchronous)

동기 처리 비동기 처리
하나의 작업이 완료될 때까지 다음 작업이 대기함 여러 개의 작업이 동시에 진행될 수 있음
코드가 순차적으로 실행됨 긴 작업이 진행되는 동안 다른 코드도 실행됨
네트워크 요청이나 파일 읽기 같은 작업이 느려질 수 있음 긴 작업을 별도로 처리하며, UI나 다른 코드가 계속 실행됨

예제: 동기 코드

console.log("1. 시작");
console.log("2. 데이터 가져오는 중...");
console.log("3. 데이터 가져오기 완료");

출력 결과:

1. 시작
2. 데이터 가져오는 중...
3. 데이터 가져오기 완료

위 코드는 순차적으로 실행됩니다. 하지만 데이터베이스 조회나 서버 요청처럼 시간이 오래 걸리는 작업이 있다면, 이러한 방식은 프로그램을 멈추게 할 수 있습니다.

예제: 비동기 코드 (setTimeout 사용)

console.log("1. 시작");
setTimeout(() => {
    console.log("2. 데이터 가져오기 완료");
}, 2000);
console.log("3. 다음 작업 실행");

출력 결과:

1. 시작
3. 다음 작업 실행
2. 데이터 가져오기 완료 (2초 후)

이처럼 비동기 처리를 하면, 데이터가 로딩되는 동안 다른 작업을 계속할 수 있습니다.


🔹 콜백 함수(Callback)란?

초기 자바스크립트에서는 비동기 작업을 위해 콜백 함수를 사용했습니다. 콜백 함수는 특정 작업이 완료된 후 실행되는 함수입니다.

콜백 함수 예제

function fetchData(callback) {
    setTimeout(() => {
        callback("데이터를 가져왔습니다!");
    }, 2000);
}

fetchData((data) => {
    console.log(data);
});

출력 결과:

(2초 후) 데이터를 가져왔습니다!

콜백 방식은 간단하지만, 콜백이 중첩되면 코드가 복잡해지는 콜백 지옥(Callback Hell) 문제가 발생할 수 있습니다.

콜백 지옥 예제

fetchData((data) => {
    console.log(data);
    fetchData((data2) => {
        console.log(data2);
        fetchData((data3) => {
            console.log(data3);
        });
    });
});

위 코드는 중첩 구조가 많아질수록 유지보수가 어려워집니다.


🔹 프로미스(Promise)의 등장

콜백 지옥을 해결하기 위해 프로미스(Promise) 개념이 등장했습니다. 프로미스는 비동기 작업이 성공하거나 실패한 후 실행할 동작을 정의할 수 있는 객체입니다.

✅ 프로미스 기본 문법

const fetchData = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("데이터를 성공적으로 가져왔습니다!");
        }, 2000);
    });
};

fetchData()
    .then(data => console.log(data))
    .catch(error => console.error("오류 발생:", error));

출력 결과:

(2초 후) 데이터를 성공적으로 가져왔습니다!

Promise의 주요 메서드

  • .then() → 프로미스가 성공(resolve)했을 때 실행
  • .catch() → 프로미스가 실패(reject)했을 때 실행
  • .finally() → 성공, 실패 여부와 관계없이 실행

✅ 프로미스 체이닝(Promise Chaining)

fetchData()
    .then(data => {
        console.log(data);
        return fetchData();
    })
    .then(data2 => {
        console.log(data2);
    })
    .catch(error => console.error("오류 발생:", error));

위처럼 .then()을 여러 개 연결하여, 비동기 작업을 체계적으로 구성할 수 있습니다.


🔹 async/await: 현대적인 비동기 처리

async/await은 ES2017(ES8)에서 도입된 문법으로, 프로미스를 더 쉽게 다룰 수 있도록 도와줍니다.

✅ async/await 기본 문법

async function getData() {
    const data = await fetchData();
    console.log(data);
}

getData();

await 키워드

  • await을 사용하면 프로미스가 해결될 때까지 기다립니다.
  • await 키워드는 async 함수 내부에서만 사용할 수 있습니다.

✅ async/await vs. Promise 체이닝 비교

Promise 체이닝

fetchData()
    .then(data => {
        console.log(data);
        return fetchData();
    })
    .then(data2 => console.log(data2))
    .catch(error => console.error("오류 발생:", error));

async/await 방식

async function getData() {
    try {
        const data1 = await fetchData();
        console.log(data1);

        const data2 = await fetchData();
        console.log(data2);
    } catch (error) {
        console.error("오류 발생:", error);
    }
}

getData();

async/await 방식은 동기 코드처럼 읽히기 때문에 가독성이 뛰어나며, 디버깅도 쉽습니다.


🔹 async/await의 에러 처리

비동기 코드에서 에러 처리는 필수적입니다. async/await에서는 try/catch 구문을 사용하여 쉽게 예외 처리를 할 수 있습니다.

async function getDataWithErrorHandling() {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (error) {
        console.error("오류 발생:", error);
    }
}

만약 여러 개의 비동기 작업이 있다면, 각각의 await에 대해 try/catch를 사용하는 것이 좋습니다.


🔹 결론

  • 콜백 함수는 비동기 처리의 기본 개념이지만, 중첩될 경우 유지보수가 어려움
  • Promise를 사용하면 비동기 처리를 더 체계적으로 할 수 있지만, .then() 체이닝이 길어질 경우 복잡해질 수 있음
  • async/await는 프로미스를 더욱 직관적이고 깔끔하게 다룰 수 있도록 해줌
  • try/catch를 활용하면 안정적인 에러 처리가 가능함
728x90