Node.js 8 async/await로 콜백 지옥 탈출하기

문제 상황

회원 가입 API에서 중첩된 콜백이 5단계까지 깊어지면서 유지보수가 어려워졌다. 에러 처리도 각 콜백마다 흩어져 있어 일관성이 없었다.

// 기존 코드
app.post('/signup', (req, res) => {
  validateUser(req.body, (err, user) => {
    if (err) return res.status(400).json({error: err});
    checkDuplicate(user.email, (err, exists) => {
      if (err) return res.status(500).json({error: err});
      if (exists) return res.status(409).json({error: 'exists'});
      hashPassword(user.password, (err, hash) => {
        if (err) return res.status(500).json({error: err});
        saveUser({...user, password: hash}, (err, saved) => {
          if (err) return res.status(500).json({error: err});
          sendWelcomeEmail(saved.email, (err) => {
            if (err) console.error('Email failed:', err);
            res.json({success: true, user: saved});
          });
        });
      });
    });
  });
});

해결 방법

Node.js 8.9 LTS가 출시되면서 async/await를 프로덕션에 도입하기로 결정했다. 콜백 기반 함수들을 Promise로 래핑하고 async 함수로 재작성했다.

const {promisify} = require('util');

const validateUserAsync = promisify(validateUser);
const checkDuplicateAsync = promisify(checkDuplicate);
const hashPasswordAsync = promisify(hashPassword);
const saveUserAsync = promisify(saveUser);
const sendWelcomeEmailAsync = promisify(sendWelcomeEmail);

app.post('/signup', async (req, res) => {
  try {
    const user = await validateUserAsync(req.body);
    const exists = await checkDuplicateAsync(user.email);
    
    if (exists) {
      return res.status(409).json({error: 'exists'});
    }
    
    const hash = await hashPasswordAsync(user.password);
    const saved = await saveUserAsync({...user, password: hash});
    
    sendWelcomeEmailAsync(saved.email).catch(err => {
      console.error('Email failed:', err);
    });
    
    res.json({success: true, user: saved});
  } catch (err) {
    res.status(500).json({error: err.message});
  }
});

결과

코드 가독성이 크게 개선되었고, try-catch로 에러 처리를 일관되게 할 수 있게 되었다. util.promisify를 사용하니 기존 콜백 함수들을 쉽게 Promise로 변환할 수 있었다.

다만 팀원들이 async/await에 익숙하지 않아 Promise 체이닝과의 차이, 에러 처리 방식 등을 공유하는 시간이 필요했다. 특히 await 없이 Promise를 반환하는 경우의 동작을 헷갈려하는 경우가 있었다.