async/await로 자바스크립트에서 여러 Functions를 제대로 체이닝 해보기


원문 : https://nikodunk.com/how-to-chain-functions-with-await-async/

필자는 electrade를 빌드하고 운영하면서 친구들이 진행하는 프로젝트를 도와준 적이 있다. 최근에는, Craiglist 스타일의 익명 이메일 전송 기능을 서버리스(serverless) Google Firebase Functions(AWS Lambda, MS Azure Functions 등과 비슷하다.)로 개발하고 싶어졌다. .then() 콜백을 이용해서 비동기 처리를 쉽게 설계할 수 있겠다는 생각이 들었지만, async/await을 이용해서 더 가독성 있고 깔끔한 코드를 짜기로 결심했다. Functions의 다중 체이닝을 다루는 여러 아티클을 읽어보았지만, 대부분은 MSDN에서 그대로 퍼온 미완성의 예제코드여서 도움이 되지 않았다. 그런 예제 중에서는 async/await으로 디버그 하기 힘든 함정들이 있었는데, 필자는 거의 모든 함정들에 빠져본 것 같다. 그래서 지금부터는 완성된 코드를 사용해서 필자가 배운 것을 설명하려고 한다.

여기 잘 동작하는 Functions 다중 체이닝 코드가 있다. 이 코드는 모든 async 함수들이 resolve되길 기다렸다가 결과를 전송한다. 그동안 했던 가장 큰 실수는 다음과 같다.

  1. async function myFunction(){ <your code here> }처럼 정의한 모든 함수는 자동적으로 함수의 코드 전체(<your code here>)를 new Promise로 감싸고, return xresolve(x)로 바꾼다. 그러므로, await을 통해서 호출(let y = await myFunction())해야 한다. 그렇지 않으면 실제로 기다려지지않는다.
  2. 추가적으로, Cloud Functions에서는 반드시 res.send()를 통해서 응답을 보내야 한다. 그렇지 않으면, function이 이를 실패로 간주해서 해당 요청을 재실행 한다. 프로미스 내의 모든것이 정상적으로 실행되지 않는다면, 그 프로미스는 취소될 것이다.

아래의 코드를 설명하면 다음과 같다.

  • 일반 함수인 getFieldsFromRequest()extractCourseIdFromEmailAddress()는 아무런 문제가 없다.
  • getEmailOfCourseWithCourseId() 는 비동기 함수이고, 코스의 이메일 주소를 Firetore로부터 가져온다. Firestore 에서 가져오는 일이 얼마나 오래 걸릴 지 알 수 없으므로, 이 함수는 비동기 함수로 되어있다. 가져오기가 완료되면 다음 2개의 함수의 실행에 필요한 courseEmail을 반환(혹은 resolve)할 것이다.
  • 다음의 두 함수 saveToCloudFirestore()sendEmailInSendgrid()는 반드시 getEmailOfCourseWithCourseId()가 실행되어 courseEmail을 반환한 뒤에 실행되어야 한다. courseEmailundefined인 상태로 실행된다면 모든 것이 물거품이 될 것이다. 앞서 설명한 getEmailOfCourseWithCourseId()함수를 기다려서(await) courseEmail을 넘겨야 하므로, 이 함수들은 그 비동기 동작이 완료되길(프로미스가 resolve되길) 기다릴 것이고, 완료된 뒤에 각 함수들이 실행될 것이다.
  • 마지막으로, saveToCloudFirestore()sendEmailInSendgrid()가 실행되고 각자의 반환값을 반환한 다음에 res.send()가 실행되어야 한다. 그렇지 않으면, 작업이 완료되기 전에 Cloud Functions 전체가 중단될 것이다. 이 문제를 해결하기 위해 saveToCloudFireStore()sendEmailInSendgrid()의 응답(그들이 반환하는 것들)을 변수로 저장한다. 이 변수들은 단순히 해당 비동기 작업이 완료되었음 을 나타내기 위해서 사용된다. 이를 통해 .then()을 대체하기 위해 쓴 것이다. 두 변수가 모두 반환(두 프로미스가 모두 resolve)되길 기다렸다가 모두 완료되면, 그 변수들로 res.send()를 실행한다.
  • 가독성을 더 좋게 하기 위해 try/catch 블록을 모두 제거했다. 이 예제 코드를 실행하려 할 때는 반드시 추가해야 한다. 서버로 비동기 요청시 반드시 에러를 처리해야 하지만, try/catch 블록이 없는 편이 async/await 개념을 이해하기 쉽게 만들어 주기 때문에 제거했다.
// 이것은 HTTP를 통해 호출할 수 있는 Cloud Functon이다.
// 이 Cloud Function은 sendgrid에서 email을 받아온 뒤, fields를 파싱해서 courseId로 실제 email 주소를 조회하여,
// Firestore에 email을 저장하고 sendgrid를 이용해서 email을 전송한다. 
// 마지막으로, `res.send()`로 Cloud Function이 끝났음을 알린다.

// {* import들.. *}

// 메인 함수
exports.emailFunction = functions.https.onRequest(async (req, res) => {
  let fields = getFieldsFromRequest(req); // 동기
  let courseId = extractCourseIdFromEmailAddress(fields); // 동기
  let courseEmail = await getEmailOfCourseWithCourseId(courseId); // 비동기
  let savedToCloud = await saveToCloudFirestore(fields, courseEmail, courseId); // 비동기
  let sentEmail = await sendEmailWithSendgrid(fields, courseEmail);  // 비동기
  res.status(200).send(savedToCloud, sentEmail); // sentEmail과 saveToCloud이 반환되면(프로미스가 resolve되면 혹은, 각 함수들이 실행되면), res.send()가 실행된다. Firebase와 SendGrid에게 이 Function이 완료되었음을 알려주게 된다.
});

// 헬퍼 함수들
function getFieldsFromRequest(req) { // 동기
    let fields = readTheFieldsFromReqWithBusboy(req);
    return fields;
}

function extractCourseIdFromEmailAddress(fields) { // 동기
    let courseId = fields.to.substring(0, fields.to.indexOf('@'));
    return courseId;
}

async function getEmailOfCourseWithCourseId(courseId) { // 비동기
    let courseData = await database.get(courseId);
    let courseEmail = courseData.email;
    return courseEmail; // async라는 라벨이 함수 옆에 붙어 있으므로, 함수 내부의 모든 코드를 'return new Promise(resolve) => {}' 로 감싼 것과 똑같이 동작한다. 'return result'는 'resolve(result)'로 바뀐다.
}

async function sendEmailWithSendgrid(fields, courseEmail) { // 비동기
    let msg = {to: courseEmail, from: fields.from, text: fields.text}
    let sentEmail = await sendgrid.send(msg)
    return sentEmail; // async라는 라벨이 함수 옆에 붙어 있으므로, 함수 내부의 모든 코드를 'return new Promise(resolve) => {}' 로 감싼 것과 똑같이 동작한다. 'return result'는 'resolve(result)'로 바뀐다.
}

async function saveToCloudFirestore(fields, courseEmail, courseId) { // 비동기
    let savedToCloud = await database.add(fields, courseEmail, courseId)
    return savedToCloud;
}

다시 말하지만, 실제 개발 시 주의해야할 내용이 있다. 하단의 비동기 함수 3개와 메인 함수를 try/catch로 감싸서 에러를 처리해야 한다. 그리고 데이터베이스 코드는 비동기 체이닝의 모습을 보여주기 위한 용도이므로 그대로 복사 붙여넣기 하면 안된다.


박정환, FE Development Lab2019.08.26Back to list