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를 반환하는 경우의 동작을 헷갈려하는 경우가 있었다.