Node.js 8 Async/Await를 실무에 도입하며

배경

팀 프로젝트가 Node.js 6에서 8 LTS로 업그레이드되면서 async/await를 본격적으로 사용할 수 있게 되었다. 기존 코드베이스는 콜백과 Promise가 혼재되어 있어 가독성이 떨어졌고, 특히 순차적인 비동기 처리가 필요한 부분에서 코드가 복잡했다.

기존 코드의 문제

사용자 인증 후 데이터를 조회하고 로그를 남기는 로직이 Promise chain으로 되어 있었다.

function getUserData(userId) {
  return authenticateUser(userId)
    .then(user => {
      return fetchUserProfile(user.id);
    })
    .then(profile => {
      return enrichProfileData(profile);
    })
    .then(enrichedData => {
      return logActivity(userId, 'profile_view')
        .then(() => enrichedData);
    })
    .catch(err => {
      console.error('Error:', err);
      throw err;
    });
}

Async/Await로 전환

동일한 로직을 async/await로 변경하니 훨씬 직관적이었다.

async function getUserData(userId) {
  try {
    const user = await authenticateUser(userId);
    const profile = await fetchUserProfile(user.id);
    const enrichedData = await enrichProfileData(profile);
    await logActivity(userId, 'profile_view');
    return enrichedData;
  } catch (err) {
    console.error('Error:', err);
    throw err;
  }
}

주의할 점

1. try-catch 누락

async 함수 내에서 await를 사용할 때 try-catch를 빼먹으면 unhandled rejection이 발생한다. Express 미들웨어에서 특히 조심해야 한다.

// 위험한 패턴
app.get('/user/:id', async (req, res) => {
  const data = await getUserData(req.params.id); // 에러 시 크래시
  res.json(data);
});

// 안전한 패턴
app.get('/user/:id', async (req, res, next) => {
  try {
    const data = await getUserData(req.params.id);
    res.json(data);
  } catch (err) {
    next(err);
  }
});

2. 병렬 처리 놓치기

순차적으로 await를 쓰면 불필요하게 느려진다.

// 느림 (6초)
const user = await fetchUser(id);  // 3초
const posts = await fetchPosts(id); // 3초

// 빠름 (3초)
const [user, posts] = await Promise.all([
  fetchUser(id),
  fetchPosts(id)
]);

결론

팀 전체가 async/await 패턴으로 전환하기로 했고, ESLint에 관련 룰도 추가했다. 콜백 헬은 이제 옛날 이야기가 될 것 같다. 다만 기존 Promise 기반 코드와의 호환성, 에러 핸들링 일관성은 코드 리뷰 때마다 체크하고 있다.