Node.js 8에서 async/await 적용하며 겪은 콜백 지옥 탈출기

배경

회사 API 서버가 Node.js 6 기반이었는데, 9월에 8.x LTS가 나오면서 업그레이드를 진행했다. 가장 큰 변화는 async/await이 정식 지원된다는 점이었다. 기존 코드는 콜백과 Promise가 뒤섞여 있어서 가독성이 좋지 않았다.

기존 코드의 문제

사용자 정보를 가져와서 권한을 체크하고 데이터를 조회하는 로직이었다.

function getUserData(userId, callback) {
  db.findUser(userId, (err, user) => {
    if (err) return callback(err);
    
    checkPermission(user.id, (err, hasPermission) => {
      if (err) return callback(err);
      if (!hasPermission) return callback(new Error('No permission'));
      
      db.findData(user.id, (err, data) => {
        if (err) return callback(err);
        callback(null, data);
      });
    });
  });
}

전형적인 콜백 지옥이었다. 에러 처리도 매번 반복되고 들여쓰기도 깊어졌다.

async/await 전환

먼저 DB 레이어를 Promisify했다. util.promisify를 쓰면 간단했다.

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

const findUserAsync = promisify(db.findUser);
const checkPermissionAsync = promisify(checkPermission);
const findDataAsync = promisify(db.findData);

async function getUserData(userId) {
  const user = await findUserAsync(userId);
  const hasPermission = await checkPermissionAsync(user.id);
  
  if (!hasPermission) {
    throw new Error('No permission');
  }
  
  const data = await findDataAsync(user.id);
  return data;
}

코드가 훨씬 읽기 쉬워졌다. 에러는 try-catch로 한 곳에서 처리할 수 있었다.

주의할 점

한 가지 실수했던 부분은 Express 라우터에서 async 함수의 에러를 제대로 처리하지 못한 것이었다. Express는 async 함수의 reject를 자동으로 잡아주지 않는다.

// 문제가 있는 코드
app.get('/api/user/:id', async (req, res) => {
  const data = await getUserData(req.params.id); // 에러 발생 시 처리 안됨
  res.json(data);
});

// 래퍼 함수로 해결
const asyncHandler = fn => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/api/user/:id', asyncHandler(async (req, res) => {
  const data = await getUserData(req.params.id);
  res.json(data);
}));

결과

주요 API 엔드포인트 20여 개를 전환했다. 코드 라인 수는 30% 정도 줄었고, 무엇보다 읽기 쉬워졌다. Node.js 8 LTS 덕분에 프로덕션에 안심하고 적용할 수 있었다.