Node.js 8 LTS에서 async/await 도입 후기

배경

회사 API 서버를 Node.js 6에서 8 LTS로 업그레이드하면서 async/await 문법을 본격 도입했다. 기존 콜백 패턴과 Promise 체이닝이 뒤섞인 코드가 많아 유지보수가 어려웠는데, 이번 기회에 정리했다.

기존 코드 문제점

function getUserData(userId, callback) {
  db.findUser(userId, (err, user) => {
    if (err) return callback(err);
    db.findPosts(user.id, (err, posts) => {
      if (err) return callback(err);
      db.findComments(posts[0].id, (err, comments) => {
        if (err) return callback(err);
        callback(null, { user, posts, comments });
      });
    });
  });
}

전형적인 콜백 지옥이었다. 에러 핸들링도 매번 반복되고, 들여쓰기가 깊어질수록 로직 파악이 힘들었다.

async/await로 전환

async function getUserData(userId) {
  try {
    const user = await db.findUser(userId);
    const posts = await db.findPosts(user.id);
    const comments = await db.findComments(posts[0].id);
    return { user, posts, comments };
  } catch (error) {
    throw error;
  }
}

동기 코드처럼 읽히면서도 논블로킹으로 동작한다. try/catch로 에러를 일괄 처리할 수 있어 코드가 훨씬 간결해졌다.

병렬 처리

순차 처리가 필요 없는 경우 Promise.all을 조합했다.

async function getMultipleData(userId) {
  const [user, settings, notifications] = await Promise.all([
    db.findUser(userId),
    db.findSettings(userId),
    db.findNotifications(userId)
  ]);
  return { user, settings, notifications };
}

세 개의 쿼리가 병렬로 실행되어 응답 속도가 개선되었다.

주의사항

  • forEach 안에서 await을 쓰면 병렬 실행되지 않는다. for...of나 map + Promise.all 사용
  • 에러 핸들링을 빼먹으면 unhandledRejection이 발생하므로 반드시 try/catch
  • Express 미들웨어에서 사용 시 에러를 next()로 전달해야 함

결과

팀 전체가 async/await으로 통일하면서 코드 리뷰 시간이 줄었다. 신규 입사자도 코드 흐름을 빠르게 파악할 수 있게 되었다.