JavaScript 비동기 처리: Promise와 async/await 비교

배경

레거시 코드베이스에 콜백이 4~5단계로 중첩된 부분이 많았다. ES6로 마이그레이션하면서 Promise로 전환했고, 최근 Node 8에서 async/await가 정식 지원되면서 일부를 다시 리팩토링했다.

Promise 방식

function getUserData(userId) {
  return fetchUser(userId)
    .then(user => fetchPosts(user.id))
    .then(posts => fetchComments(posts[0].id))
    .then(comments => {
      return { comments };
    })
    .catch(error => {
      console.error('Error:', error);
      throw error;
    });
}

Promise 체이닝은 콜백보다 훨씬 낫지만, 중간에 변수를 참조해야 할 때 클로저나 Promise.all을 써야 해서 복잡했다.

async/await 방식

async function getUserData(userId) {
  try {
    const user = await fetchUser(userId);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    return { user, posts, comments };
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

동기 코드처럼 읽히고, 중간 변수 접근이 자유롭다. 에러 핸들링도 try/catch로 통일할 수 있어서 직관적이었다.

병렬 처리 주의점

// 순차 실행 (느림)
const user = await fetchUser(id);
const config = await fetchConfig(id);

// 병렬 실행 (빠름)
const [user, config] = await Promise.all([
  fetchUser(id),
  fetchConfig(id)
]);

await를 연속으로 쓰면 순차 실행된다. 독립적인 요청은 Promise.all로 묶어야 성능 이슈가 없다.

결론

새로운 비동기 코드는 async/await로 작성하고 있다. Promise는 여전히 유용하지만, 가독성 면에서 async/await가 명확히 우위였다. 다만 브라우저 지원을 위해 Babel 트랜스파일은 필수다.