Skip to main content

Command Palette

Search for a command to run...

Javascript 비동기 처리에 대한 정리

콜백(Callback), 프로미스(Promise), async/await의 차이점을 알아보자

Updated
4 min read
Javascript 비동기 처리에 대한 정리

이 글에서는 자바스크립트의 비동기 처리 수단 세가지에 대해 알아보고, 각자가 어떤 장단점과 한계점을 가졌는지 정리합니다.

세 줄 요약

  • 자바스크립트 비동기 처리: 자바스크립트는 비동기 처리를 위해 콜백, 프로미스, async/await를 제공합니다. 콜백은 간단하지만 "콜백 지옥"을 유발할 수 있고, 프로미스는 이를 개선했지만 복잡할 수 있으며, async/await는 가독성이 좋지만 제어권 상실 및 자원 누수 위험이 있습니다.

  • 프로미스와 async/await의 문제: async/await 사용 시 비동기 작업 완료까지 함수 실행이 중단되어 제어권 상실 및 자원 누수가 발생할 수 있으며, 프로미스 처리 여부도 보장되지 않습니다.

  • 주의 사항: 각 방식은 "콜백 지옥"이나 "await 사건의 지평선" 같은 단점이 있으므로, 이를 이해하고 상황에 맞는 비동기 처리 방식을 선택하는 것이 중요합니다.

세 가지 비동기 처리 방식 정리 및 장단점

1. 콜백(Callback) 함수

  • 특징: 가장 기본적인 비동기 처리 방식. 함수를 다른 함수의 인자로 넘겨주고, 어떤 이벤트가 발생한 후 해당 함수가 나중에 호출됩니다.

  • 장점: 간단한 비동기 처리, 함수 주입을 통한 제어 역전, 오래된 라이브러리/프레임워크와 호환 용이, 낮은 학습 곡선이 있습니다.

  • 단점: 중첩된 콜백으로 인해 코드의 복잡도가 증가하는 '콜백 지옥' 이 발생 할 수 있습니다.

  • 사용 예시:

function fetchData(callback) {
    setTimeout(() => {
        callback('데이터');
    }, 1000);
}

fetchData(data => {
    console.log(data); // '데이터'
});

2. 프로미스(Promise)

  • 특징: 콜백 지옥의 해결책으로 등장했습니다. 비동기 작업의 최종 성공 또는 실패를 나타내는 객체입니다.

  • 장점: 연속된 비동기 작업을 체이닝을 통해 깔끔하게 표현 가능합니다. 작업 체인 내에서의 오류를 catch()를 통해 쉽게 제어 할 수 있고, Promise.all 등을 활용해서 병렬처리도 지원합니다.

  • 단점: 잘못 사용하면 여전히 콜백 지옥을 유발할 수 있습니다.

  • 사용 예시:

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('데이터');
        }, 1000);
    });
}

fetchData().then(data => {
    console.log(data); // '데이터'
}).catch(error => {
    console.error(error);
});

3. async/await

  • 특징: 프로미스를 기반으로 한 비동기 처리 패턴입니다. Promise의 활용할 시의 보일러플레이트 코드를 입력 안해도 되게 해 주는 문법적 설탕(Syntactic sugar)입니다.

  • 장점: 코드의 가독성을 대폭 향상 해줍니다. try/catch 블록을 사용한 에러 처리가 가능해서 좀 더 익숙한 방식의 에러 처리가 가능합니다.

  • 단점:

  • 사용 예시:

async function fetchData() {
    try {
        const response = await fetch('https://api.example.com/data');
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.error(error);
    }
}

fetchData();

여기서 볼 수 있듯이, 콜백 -> 프로미스 -> async/await로 넘어오면서 비동기 처리의 복잡성과 가독성 문제가 점점 개선되고 있습니다. async/await는 내부적으로 프로미스를 사용하긴 하지만, 코드를 동기식으로 보이게 하여 더 이해하기 쉽고, 관리하기 편리한 형태로 작성할 수 있게 해줍니다.

Promise와 async/await는 그럼 무조건 좋을까요?

답은 당연히 그렇지 않습니다. 지금은 많이 알려진 심각한 단점이 있습니다.

제어권의 상실

  • async/await를 사용하면, await 키워드가 사용된 비동기 작업이 완료될 때까지 함수의 실행이 멈추게 됩니다. 이는 ‘작업이 완료되지 않으면 함수가 영원히 중단될 수 있음’을 의미합니다. 즉, 프로그램의 나머지 부분으로 제어권이 돌아가지 않을 수 있습니다.

자원 누수의 위험

  • 메모리라는 제한된 자원의 누수가 축적될 수 있는 위험이 존재합니다. 하단의 예시에서는 간단한 락(lock) 메커니즘을 사용하여, work 함수의 실행 전에 락을 획득하고 실행 후에 락을 해제하는 방식으로 자원 관리를 시도해보겠습니다.
async function acquireLock() {
    // 임의의 락 획득 로직을 가정
    console.log("Lock acquired");
    return { id: "lockId" }; // 실제 구현에서는 락 객체를 반환할 것
}

function releaseLock(lock) {
    // 임의의 락 해제 로직을 가정
    console.log(`Lock ${lock.id} released`);
}

async function protect(work) {
    let lock = await acquireLock();
    try {
        await work();
    } finally {
        releaseLock(lock);
    }
}

function work() {
    return new Promise((resolve, reject) => {
        // 임의의 비동기 작업을 가정
        setTimeout(() => {
            console.log("Work done"); // <- ⚠️ 여기서 에러가 발생하면?
            resolve();
        }, 2000); // 2초 후 작업 완료
    });
}

protect(work);
  • 예로 들어진 protect 함수에서 볼 수 있듯이, work()에서 반환된 Promise가 임의의 문제로 resolve 되지 않으면, protect() 함수는 "await 사건의 지평선"을 넘어서고 재개되지 않게 됩니다. 이는 초기에 획득한 자원이 영원히 해제되지 않음을 의미하며, 이러한 자원 누수는 시스템에 심각한 문제를 일으킬 수 있습니다.

비동기 작업 처리에 대한 보장이 없음

  • Promise가 언제 처리될지, 심지어 처리될지 여부조차도 보장할 수 없습니다. 이는 코드가 필요한 설정, 동작 수행, 그리고 해제 작업을 안전하게 완료하는 것을 어렵게 만듭니다.

그래서 이 문제를 해결하려면 어떻게 해야할까요? 내용이 너무 길어질 수 있어서 좋은 글 링크를 소개해 드리겠습니다. 자바스크립트 await 사건의 지평선

이러한 Promise와 async/await에 있는 문제들은 콜백에는 해당되지 않는 문제입니다. 콜백은 비동기 작업이 완료될 때 실행되는 함수를 제공하지만, Promiseasync/await처럼 실행 흐름을 멈추거나 "await 사건의 지평선" 같은 개념에 직접적으로 묶여 있지 않습니다.

하지만 콜백을 사용할 때도 비동기 작업의 완료를 관리하는 복잡성, 에러 처리의 어려움 등 다른 종류의 문제가 발생할 수 있으므로 여전히 좋은 선택지는 아닙니다.

마치며

위에서 소개 드린 것처럼 콜백(Callback), 프로미스(Promise), async/await는 비동기 작업을 처리하는 방법들이며, 각기 다른 장단점을 가지고 있습니다. 콜백은 가장 기본적이지만 콜백 지옥으로 인한 복잡성이 문제가 될 수 있고, 프로미스와 async/await는 가독성과 에러 처리를 개선하지만, 제어권 상실과 자원 누수의 위험이 있습니다. 이들 방법은 비동기 처리를 용이하게 하지만, 각각의 사용 시 주의가 필요합니다.

Javascript 비동기 처리에 대한 정리