Waylog Blog

자바스크립트의 비동기 처리: Callback 지옥에서 Async/Await 천국까지

JavaScript

자바스크립트는 싱글 스레드(Single Thread) 언어입니다. 즉, 한 번에 하나의 일만 처리할 수 있습니다. 그런데 어떻게 수많은 네트워크 요청과 사용자 입력을 동시에 처리하는 것처럼 보일까요? 그 비결은 바로 **이벤트 루프(Event Loop)**와 **비동기 삼형제(Callback, Promise, Async/Await)**에 있습니다.

1. 태초에 Callback이 있었다

초기에는 비동기 작업이 끝난 후 실행할 함수를 인자로 넘겨주었습니다.

getData(function(a) {
  getMoreData(a, function(b) {
    getFinalData(b, function(c) {
      console.log(c);
    });
  });
});

이것이 그 유명한 **콜백 지옥(Callback Hell)**입니다. 가독성은 나락으로 가고, 에러 처리는 거의 불가능에 가깝습니다.

2. 구세주 Promise의 등장 (ES6)

2015년, Promise가 등장하며 비동기 처리가 우아해졌습니다. 성공(resolve)과 실패(reject)를 표준화된 객체로 다룰 수 있게 되었고, 체이닝(Chaining)을 통해 깊이(Depth)를 줄였습니다.

getData()
  .then(a => getMoreData(a))
  .then(b => getFinalData(b))
  .then(c => console.log(c))
  .catch(err => console.error(err));

3. 완성형 Async/Await (ES8)

Promise도 훌륭하지만, 여전히 .then()이 꼬리를 무는 형태는 동기 코드와 이질감이 있었습니다. Async/Await는 비동기 코드를 마치 동기 코드처럼 작성하게 해줍니다.

async function main() {
  try {
    const a = await getData();
    const b = await getMoreData(a);
    const c = await getFinalData(b);
    console.log(c);
  } catch (err) {
    console.error(err);
  }
}

가독성이 압도적으로 좋아졌고, try/catch 문을 사용하여 동기/비동기 에러를 동일한 흐름으로 처리할 수 있게 되었습니다.

4. 주의할 점: 병렬 처리

await을 남발하면 병목이 생길 수 있습니다. 서로 의존성이 없는 요청이라면 Promise.all을 사용하여 병렬로 처리해야 성능을 높일 수 있습니다.

// ❌ 나쁜 예 (직렬)
const user = await getUser();
const posts = await getPosts(); // getUser가 끝날 때까지 대기

// ✅ 좋은 예 (병렬)
const [user, posts] = await Promise.all([getUser(), getPosts()]);

비동기는 자바스크립트의 심장입니다. 이를 자유자재로 다루는 것이 중급 개발자로 가는 관문입니다.