프론트엔드 개발에서 비동기 처리는 자주 사용되는 패턴입니다. 자바스크립트는 비동기 처리를 지원하기 위해 콜백, 프로미스, async/await와 같은 다양한 방법을 제공합니다. 하지만 이러한 비동기 처리 방식은 개발자들 사이에서 여러 오해를 불러일으키고 있습니다. 이번 글에서는 자바스크립트 비동기 처리에서 발생할 수 있는 오해와 그 해결법에 대해 알아보겠습니다.

1. 콜백 지옥

가장 흔한 오해 중 하나는 콜백을 중첩해서 사용할 경우 코드가 복잡해지고 가독성이 떨어진다는 것입니다. 이를 종종 '콜백 지옥'이라고 부릅니다. 다음은 콜백 지옥의 간단한 예입니다.

function firstTask(callback) {
    setTimeout(() => {
        console.log("첫 번째 작업 완료");
        callback();
    }, 1000);
}

function secondTask(callback) {
    setTimeout(() => {
        console.log("두 번째 작업 완료");
        callback();
    }, 1000);
}

function thirdTask() {
    setTimeout(() => {
        console.log("세 번째 작업 완료");
    }, 1000);
}

firstTask(() => {
    secondTask(() => {
        thirdTask();
    });
});

이 코드에서 firstTask, secondTask, thirdTask를 콜백으로 서로 연결하면서 가독성이 떨어지는 것을 볼 수 있습니다. 이 문제를 해결하기 위해 프로미스를 사용할 수 있습니다.

2. 프로미스의 이해 부족

프로미스는 비동기 작업의 완료(혹은 실패)를 나타내는 객체입니다. 그러나 프로미스를 사용할 때도 잘못 이해하고 사용하는 경우가 많습니다. 예를 들어, 프로미스가 항상 성공적으로 완료된다고 생각할 수 있지만, 실제로는 실패할 수도 있습니다.

const task = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const success = Math.random() > 0.5; // 50% 확률로 성공
            if (success) {
                resolve("작업 성공");
            } else {
                reject("작업 실패");
            }
        }, 1000);
    });
};

task()
    .then((result) => {
        console.log(result);
    })
    .catch((error) => {
        console.error(error);
    });

위의 코드에서 task 함수는 50% 확률로 성공하거나 실패합니다. 프로미스를 사용할 때는 항상 catch를 사용해 에러를 처리하는 것이 중요합니다.

3. async/await의 사용

async/await는 비동기 코드를 더 간결하고 읽기 쉽게 만들어줍니다. 대부분의 개발자들이 async/await을 사용하면서도 여전히 비동기 처리에 대한 기본 개념을 간과하는 경우가 많습니다. async 함수는 항상 프로미스를 반환하며, 그 결과를 기다리기 위해 await를 사용할 수 있습니다.

const asyncTask = async () => {
    try {
        const result = await task();
        console.log(result);
    } catch (error) {
        console.error(error);
    }
};

asyncTask();

위의 예제에서 asyncTask 함수는 async로 정의되었고, 내부에서 await를 사용하여 task 함수의 결과를 기다립니다. 에러가 발생할 가능성이 있기 때문에 try/catch를 사용하여 적절히 처리하고 있습니다.

4. 비동기 처리를 위한 병렬 처리

비동기 작업이 여러 개 있을 때, 이를 병렬로 처리하고 싶을 때가 많습니다. Promise.all()을 사용하면 여러 프로미스를 동시에 실행할 수 있습니다. 하지만 모든 프로미스가 성공해야 결과를 반환하므로, 각 프로미스의 성공 여부에 따라 다른 처리를 하고 싶다면 Promise.allSettled()를 사용하는 것이 좋습니다.

const task1 = () => new Promise(resolve => setTimeout(() => resolve("작업 1 완료"), 1000));
const task2 = () => new Promise((_, reject) => setTimeout(() => reject("작업 2 실패"), 500));
const task3 = () => new Promise(resolve => setTimeout(() => resolve("작업 3 완료"), 1500));

Promise.allSettled([task1(), task2(), task3()])
    .then(results => {
        results.forEach((result, index) => {
            if (result.status === "fulfilled") {
                console.log(작업 ${index + 1} 성공: ${result.value});
            } else {
                console.error(작업 ${index + 1} 실패: ${result.reason});
            }
        });
    });

위의 코드에서는 Promise.allSettled를 사용하여 모든 비동기 작업의 결과를 확인합니다. 각각의 작업이 성공했는지 실패했는지를 확인할 수 있어 유용합니다.

마무리

비동기 처리는 프론트엔드 개발에서 필수적인 기술입니다. 그러나 이와 관련된 다양한 오해는 개발 과정에서 혼란을 초래할 수 있습니다. 콜백 지옥, 프로미스의 이해 부족, async/await의 잘못된 사용, 비동기 작업의 병렬 처리에 대한 이해 부족 등은 모두 비동기 처리에서 발생할 수 있는 대표적인 오해들입니다. 이 글을 통해 비동기 처리에 대한 보다 명확한 이해와 올바른 사용법을 습득하시길 바랍니다.