IT_Tech_AI

자바스크립트 비동기 처리, 콜백 지옥에서 탈출한 나의 6개월 여정

kanez 2025. 10. 17. 07:41
반응형

콜백 지옥, 정말 지옥이었다

6개월 전, 나는 자바스크립트 비동기 처리를 제대로 이해하지 못한 채 프로젝트를 시작했다. 콜백 함수를 중첩하다 보니 코드가 옆으로 끝없이 늘어나는 '콜백 지옥'에 빠졌고, 디버깅만 3일이 걸렸다. 그때부터 시작된 나의 비동기 처리 학습 여정을 공유한다.

첫 번째 실패: 콜백 함수로 API 3개 연달아 호출하기

내 첫 프로젝트는 사용자 정보를 가져오고, 그 사용자의 주문 내역을 조회한 뒤, 각 주문의 상세 정보를 불러오는 웹 앱이었다. 당시 나는 콜백 함수만 알고 있었고, 이렇게 코드를 짰다:

getUserInfo(userId, function(user) {
    getOrders(user.id, function(orders) {
        getOrderDetails(orders[0].id, function(details) {
            getShippingInfo(details.shippingId, function(shipping) {
                console.log(shipping);
                // 여기서부터 코드가 오른쪽으로 계속...
            });
        });
    });
});

이 코드의 문제점을 3일 후에야 깨달았다. 에러가 어디서 발생했는지 추적이 불가능했고, 각 단계마다 에러 처리를 따로 해야 했다. 코드 리뷰에서 선배 개발자가 한마디 했다. "이거 콜백 지옥이라고 부릅니다. Promise 써보세요."

자바 스크립트 콜백 지옥

Promise가 내 코드를 구해준 순간

Promise를 처음 배울 때는 솔직히 개념이 어려웠다. "대기(pending), 이행(fulfilled), 거부(rejected)" 같은 용어들이 낯설었다. 하지만 직접 리팩토링을 시작하자 모든 게 명확해졌다.

getUserInfo(userId)
    .then(user => getOrders(user.id))
    .then(orders => getOrderDetails(orders[0].id))
    .then(details => getShippingInfo(details.shippingId))
    .then(shipping => console.log(shipping))
    .catch(error => console.error('에러 발생:', error));

코드가 세로로 흐르면서 읽기 쉬워졌고, .catch() 하나로 모든 에러를 처리할 수 있었다. 가장 큰 변화는 디버깅 시간이었다. 3일 걸리던 작업이 30분으로 줄었다.

💡 실전 팁: Promise 체인에서 중간 데이터가 필요할 때는 클로저를 활용하거나 Promise.all()로 병렬 처리하는 게 효율적이다. 나는 이걸 모르고 불필요한 변수를 외부에 선언했다가 메모리 누수를 경험했다.

Async/Await을 쓰고 나서 달라진 3가지

자바스크립트 async await 문법 비교

Promise만으로도 충분하다고 생각했는데, Async/Await을 배우고 나니 세상이 달라 보였다. 동기 코드처럼 작성하면서도 비동기로 동작한다는 게 마법 같았다.

async function fetchUserData(userId) {
    try {
        const user = await getUserInfo(userId);
        const orders = await getOrders(user.id);
        const details = await getOrderDetails(orders[0].id);
        const shipping = await getShippingInfo(details.shippingId);
        
        return shipping;
    } catch (error) {
        console.error('데이터 로딩 실패:', error);
        throw error;
    }
}

1. 코드 가독성이 극적으로 개선됐다

변수 선언과 사용이 자연스러워져서 팀원들이 내 코드를 이해하는 시간이 절반으로 줄었다. 코드 리뷰 코멘트도 "이게 뭐하는 코드야?"에서 "이 부분 최적화 가능할 것 같아요"로 바뀌었다.

2. 에러 처리가 직관적으로 바뀌었다

try-catch 블록으로 에러를 처리하니, 기존 자바스크립트 경험을 그대로 활용할 수 있었다. Promise의 .catch()보다 훨씬 익숙하고 명확했다.

3. 조건문과 반복문을 자유롭게 쓸 수 있게 됐다

Promise 체인에서는 if문이나 for문을 쓰기 까다로웠다. Async/Await을 쓰니 일반 동기 코드처럼 작성할 수 있었다.

async function processOrders(userId) {
    const orders = await getOrders(userId);
    
    for (const order of orders) {
        if (order.status === 'pending') {
            await processPayment(order.id);
        }
    }
}

실전에서 겪은 에러 핸들링 실수들

비동기 처리를 배우면서 가장 많이 실수한 부분이 에러 핸들링이었다. 특히 이 세 가지 실수는 지금도 가끔 저지른다.

실수 1: async 함수에서 try-catch를 빠뜨림

// ❌ 잘못된 코드 - 에러가 잡히지 않음
async function loadData() {
    const data = await fetchAPI(); // 에러 발생 시 앱이 멈춤
    return data;
}

// ✅ 올바른 코드
async function loadData() {
    try {
        const data = await fetchAPI();
        return data;
    } catch (error) {
        console.error('API 호출 실패:', error);
        return null; // 기본값 반환
    }
}

실제로 배포 환경에서 API가 다운됐을 때, 이 실수 때문에 전체 앱이 멈춘 적이 있다. 사용자들에게 에러 메시지도 보여주지 못하고 하얀 화면만 띄워서 엄청 욕먹었다.

실수 2: Promise.all()에서 하나만 실패해도 전체 실패

여러 API를 동시에 호출할 때 Promise.all()을 썼는데, 하나라도 실패하면 나머지 성공한 데이터도 못 받는다는 걸 몰랐다. Promise.allSettled()를 알게 된 후 문제가 해결됐다.

// Promise.allSettled() 사용
const results = await Promise.allSettled([
    fetchUserData(),
    fetchPosts(),
    fetchComments()
]);

// 각 결과를 개별적으로 처리
results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
        console.log(`API ${index} 성공:`, result.value);
    } else {
        console.error(`API ${index} 실패:`, result.reason);
    }
});

실수 3: await을 빠뜨려서 Promise 객체를 받음

이건 정말 바보 같은 실수인데, await을 안 붙여서 데이터 대신 Promise 객체를 받은 적이 여러 번 있다. 타입스크립트를 쓰면 이런 실수를 컴파일 단계에서 잡아준다.

병렬 처리 vs 순차 처리, 언제 뭘 써야 할까

처음에는 무조건 await을 연달아 써서 순차적으로 처리했다. 그런데 속도 측정을 해보니 엄청나게 느렸다. 서로 독립적인 API 호출인데도 순차적으로 기다리고 있었던 것이다.

// ❌ 느린 코드 (약 6초 소요)
async function loadDashboard() {
    const userData = await fetchUser();      // 2초
    const postsData = await fetchPosts();    // 2초
    const statsData = await fetchStats();    // 2초
}

// ✅ 빠른 코드 (약 2초 소요)
async function loadDashboard() {
    const [userData, postsData, statsData] = await Promise.all([
        fetchUser(),
        fetchPosts(),
        fetchStats()
    ]);
}

실제 프로젝트에서 대시보드 로딩 시간을 6초에서 2초로 줄였더니, 사용자 이탈률이 30% 감소했다. 구글 애널리틱스로 확인한 실제 데이터다.

📊 언제 뭘 쓸까?
- 병렬 처리(Promise.all): 서로 독립적인 API 호출, 속도 중요
- 순차 처리(await 연달아): 이전 결과가 다음 호출에 필요할 때
- 혼합 사용: 일부는 병렬, 일부는 순차로 최적화

6개월 후 내 코드가 달라진 모습

비동기 처리를 제대로 이해하고 나니, 코드 품질이 전반적으로 향상됐다. 같은 기능을 구현하는데 걸리는 시간이 절반으로 줄었고, 버그 발생 빈도도 확연히 낮아졌다.

변화 전후 비교

항목 6개월 전 현재
기능 구현 시간 4-5일 2-3일
디버깅 시간 3일 30분
코드 리뷰 코멘트 평균 15개 평균 3개
대시보드 로딩 시간 6초 2초

가장 큰 변화는 자신감이다. 이제는 복잡한 비동기 로직도 두려움 없이 작성할 수 있고, 후배 개발자들에게도 자신 있게 가르쳐줄 수 있다.

💡 마지막 조언: 비동기 처리는 이론보다 실전이 중요하다. 작은 프로젝트라도 직접 콜백, Promise, Async/Await을 각각 써보면서 차이를 체감해야 한다. 나도 책만 보다가는 이해가 안 됐지만, 직접 코드를 짜고 실수하면서 배웠다.

자주 묻는 질문 (FAQ)

Q1. 콜백, Promise, Async/Await 중 뭘 배워야 하나요?

순서대로 다 배워야 합니다. 레거시 코드에는 콜백이 많고, 많은 라이브러리가 Promise를 반환하며, 최신 코드는 Async/Await을 쓰기 때문입니다. 하지만 실전에서는 Async/Await을 주로 사용하게 될 겁니다.

Q2. Promise.all()과 Promise.allSettled()의 차이는?

Promise.all()은 하나라도 실패하면 전체가 실패하고, Promise.allSettled()는 성공/실패 여부와 관계없이 모든 결과를 반환합니다. 여러 API를 호출할 때 일부가 실패해도 나머지 데이터를 받아야 한다면 allSettled()를 쓰세요.

Q3. async 함수에서 return하면 Promise가 반환되나요?

네, async 함수는 항상 Promise를 반환합니다. 일반 값을 return해도 자동으로 Promise.resolve()로 감싸집니다. 그래서 async 함수를 호출할 때는 await을 붙이거나 .then()을 사용해야 합니다.

Q4. try-catch로 잡히지 않는 에러가 있나요?

있습니다. 비동기 콜백 내부에서 발생한 에러는 try-catch로 못 잡습니다. 이럴 때는 해당 비동기 함수를 async/await으로 바꾸거나, Promise의 .catch()를 사용해야 합니다.

Q5. for 루프 안에서 await을 쓰면 느리지 않나요?

네, 순차적으로 처리되어 느립니다. 배열의 각 항목을 병렬로 처리하려면 Promise.all()과 map()을 함께 사용하세요: await Promise.all(items.map(item => processItem(item)))

Q6. 실무에서 가장 많이 쓰는 패턴은?

제 경험상 async/await + try-catch + Promise.all() 조합이 90% 이상입니다. API 호출이나 DB 쿼리는 거의 이 패턴으로 처리합니다.

Q7. 타입스크립트를 쓰면 도움이 되나요?

엄청 도움됩니다. Promise의 타입을 명시하면 await을 빠뜨리거나, 잘못된 데이터를 사용하는 실수를 컴파일 단계에서 잡을 수 있습니다. 저는 타입스크립트 도입 후 비동기 관련 버그가 70% 줄었습니다.

Q8. 비동기 처리를 배우는 가장 좋은 방법은?

직접 API를 호출하는 작은 프로젝트를 만들어보세요. 날씨 API나 영화 정보 API 같은 공개 API를 사용해서 데이터를 받아오고, 에러 처리도 해보고, 로딩 상태도 구현해보면서 체득하는 게 가장 빠릅니다.

🔗 함께 읽으면 좋은 글

자바스크립트 비동기 처리를 제대로 이해하려면 네트워크와 웹의 작동 원리도 알아두면 좋습니다:

반응형