얼만 전까지도 비동기 프로그래밍은 경험 많은 전문가들에게도 도전 과제였으며, 이로 인해 ‘콜백 지옥’이라는 말이 생겨났습니다. 이 글에서는async 함수가 어떻게 우리를 고난에서 구해주었는지, 그리고 왜 그것을 사용해야 하는지 설명합니다.

성공적인 웹 애플리케이션을 작성하는 열쇠 중 하나는 페이지당 수십 개의 AJAX 호출을 할 수 있는 능력입니다.이것은 전형적인 비동기 프로그래밍 도전 과제이며, 비동기 호출을 어떻게 처리하는지에 따라 앱의 성공여부가 좌우되며, 그로 인해 여러분의 업무 전체가 영향을 받을 수 있습니다.

자바스크립트에서 비동기 작업을 동기화하는 것은 오랫동안 심각한 문제였습니다.

이 도전은 Node.js를 사용하는 백엔드 개발자뿐만 아니라, 모든 자바스크립트 프레임워크를 사용하는 프론트엔드 개발자들에게도 영향을 미쳤습니다. 비동기 프로그래밍은 우리의 일상 업무의 일부이지만, 종종 가볍게 여겨지고 적절한 시기에 고려되지 않습니다.

비동기 자바스크립트의 간략한 역사

가장 간단하고 초기의 해결책은 콜백으로서 중첩된 함수의 형태로 나타났습니다. 이 해결책은 ‘콜백 지옥(Callback Hell)’으로 불리는 상황을 초래했고, 너무 많은 애플리케이션이 아직도 이로 인한 영향을 받고 있습니다.

그 다음, 우리는 프로미스(Promise)를 얻었습니다. 이 패턴은 코드를 읽기 매우 쉽게 만들었지만, ‘자신을 반복하지 않기(Don’t Repeat Yourself, DRY)’ 원칙에서는 한참 멀었습니다. 애플리케이션의 흐름을 제대로 관리하기 위해 동일한 코드 조각을 반복해야 하는 경우가 여전히 너무 많았습니다. 최신 추가사항인 async/await 자바스크립트 구문은 마침내 자바스크립트의 비동기 코드를 다루는 것을 다른 코드 조각만큼이나 쉽게 만들었습니다.

이제 이러한 해결책들 각각의 예시로 살펴 봅시다. 그리고 자바스크립트의 비동기 프로그래밍이 어떻게 발전했는지 되돌아보겠습니다.

이를 위해 우리의 비동기 자바스크립트 튜토리얼은 다음과 같은 단계를 수행하는 간단한 작업을 살펴볼 것입니다:

  1. 사용자의 사용자 이름과 비밀번호를 확인합니다.
  2. 사용자에게 애플리케이션 역할을 가져옵니다.
  3. 사용자의 애플리케이션 접근 시간을 기록합니다.

1번 접근 방식: 콜백 지옥 (“파멸의 피라미드”)

이 호출들을 동기화하는 가장 오래된 방법은 중첩된 콜백을 통한 것이었습니다. 단순한 비동기 자바스크립트 작업에는 어느 정도 적합한 접근법이었지만, ‘콜백 지옥’이라는 문제로 인해 확장성이 떨어졌습니다.

세 개의 간단한 작업을 위한 코드는 다음과 같이 보입니다.

const verifyUser = function(username, password, callback){
   dataBase.verifyUser(username, password, (error, userInfo) => {
       if (error) {
           callback(error)
       }else{
           dataBase.getRoles(username, (error, roles) => {
               if (error){
                   callback(error)
               }else {
                   dataBase.logAccess(username, (error) => {
                       if (error){
                           callback(error);
                       }else{
                           callback(null, userInfo, roles);
                       }
                   })
               }
           })
       }
   })
};


각 함수는 이전 작업의 응답을 매개변수로 호출되는 또 다른 함수인 인수를 받습니다.

위 문장을 읽기만 해도 머리가 얼어붙을 것 같은 사람이 많을 것입니다. 유사한 코드 블록이 수백 개인 애플리케이션을 유지하는 것은, 그것을 직접 작성한 사람에게도 더 많은 문제를 야기할 것입니다.

database.getRoles가 또 다른 중첩된 콜백을 가진 함수라는 사실을 깨닫게 되면 이 예제는 더욱 복잡해집니다.

const getRoles = function (username, callback){
   database.connect((connection) => {
       connection.query('get roles sql', (result) => {
           callback(null, result);
       })
   });
};


유지관리가 어려운 코드를 가지고 있는 것 외에도, 이 경우에서 DRY 원칙은 전혀 가치가 없습니다. 예를 들어, 에러 처리는 각 함수에서 반복되고 주 콜백은 각 중첩 함수에서 호출됩니다.

비동기 자바스크립트 호출을 반복하는 것처럼 더 복잡한 비동기 자바스크립트 작업은 더욱 큰 도전입니다. 사실, 콜백으로 이러한 작업을 하는 간단한 방법은 없습니다. 이것이 바로 Bluebird와 Q와 같은 자바스크립트 프로미스 라이브러리가 많은 관심을 받은 이유입니다. 그들은 언어 자체가 제공하지 않는 비동기 요청에 대한 일반적인 작업을 수행하는 방법을 제공합니다.

이것이 바로 네이티브 자바스크립트 프로미스가 등장하는 시점입니다.

자바스크립트의 프로미스(Promises)

콜백 지옥(callback hell)에서 탈출하기 위한 다음 단계는 당연히 프로미스였습니다. 이 방식은 콜백의 사용을 완전히 없애지는 못했지만, 자바스크립트에서 비동기 함수의 연쇄를 직관적으로 만들고 코드를 단순화하여 읽기가 훨씬 쉽게 되었습니다.


프로미스를 도입하면, 우리의 비동기 자바스크립트 예제 코드는 다음과 간결하게 변모합니다:

const verifyUser = function(username, password) {
   database.verifyUser(username, password)
       .then(userInfo => dataBase.getRoles(userInfo))
       .then(rolesInfo => dataBase.logAccess(rolesInfo))
       .then(finalResult => {
           //do whatever the 'callback' would do
       })
       .catch((err) => {
           //do whatever the error handler needs
       });
};

이러한 단순함을 달성하기 위해서는 예제에서 사용한 모든 함수들이 프로미스화(Promisified)되어야 합니다. getRoles 메소드가 프로미스를 반환하도록 어떻게 업데이트되는지 살펴보겠습니다:

const getRoles = function (username){
   return new Promise((resolve, reject) => {
       database.connect((connection) => {
           connection.query('get roles sql', (result) => {
               resolve(result);
           })
       });
   });
};


우리는 메소드가 프로미스를 반환하도록 수정했고, 두 개의 콜백을 가지게 되며, 프로미스 자체가 메소드의 작업을 수행합니다. 이제, resolve와 reject 콜백은 각각 Promise.then과 Promise.catch 메소드에 매핑될 것입니다.

아직도 getRoles 메소드는 내부적으로 ‘파멸의 피라미드(pyramid of doom)’ 현상에 취약하다는 것을 알아챌 수 있습니다. 이는 데이터베이스 메소드들이 프로미스를 반환하지 않는 방식 때문입니다. 만약 우리의 데이터베이스 접근 메소드들도 프로미스를 반환한다면, getRoles 메소드는 다음과 같이 보일 것입니다:

const getRoles = new function (userInfo) {
   return new Promise((resolve, reject) => {
       database.connect()
           .then((connection) => connection.query('get roles sql'))
           .then((result) => resolve(result))
           .catch(reject)
   });
};

제3의 접근 방식: Async/Await

Promise의 도입으로 ‘콜백 헬(Pyramid of doom)’이 크게 경감되었습니다. 그럼에도 우리는 .then과 .catch 메소드에 전달되는 콜백에 여전히 의존해야 했습니다.

Promise는 JavaScript에서 가장 멋진 발전 중 하나를 가능하게 했습니다. ECMAScript 2017은 JavaScript의 Promise 상단에 async와 await 문이라는 문법적 설탕을 추가했습니다.

이들은 마치 동기식처럼 Promise 기반 코드를 작성할 수 있게 해주지만, 다음 코드 샘플이 보여주듯이 주 실행 스레드를 차단하지 않습니다:

const verifyUser = async function(username, password){
   try {
       const userInfo = await dataBase.verifyUser(username, password);
       const rolesInfo = await dataBase.getRoles(userInfo);
       const logStatus = await dataBase.logAccess(userInfo);
       return userInfo;
   }catch (e){
       //handle errors as needed
   }
};


Promise의 해결을 기다리게 하는 것은 async 함수 내에서만 허용되므로, verifyUser는 async 함수를 사용하여 정의되어야 합니다.

그러나, 이 작은 변경을 하고 나면 다른 메소드에서 추가 변경 없이 모든 Promise를 await 할 수 있습니다.

Async JavaScript – 오랫동안 기다려온 Promise의 해결

Async 함수는 JavaScript 비동기 프로그래밍의 진화에서 다음 단계입니다. 이들은 코드를 훨씬 깔끔하고 유지 관리하기 쉽게 만들어줍니다. 함수를 async로 선언하면 항상 Promise를 반환하게 되므로 더 이상 그 점에 대해 걱정할 필요가 없습니다.

자바스크립트에서 async가 무엇을 하며 왜 오늘날 JavaScript async 함수를 사용하기 시작해야 다시 정리하면 다음과 같습니다.

  • 결과적으로 코드가 훨씬 깔끔해집니다.
  • 에러 처리가 훨씬 단순해지며, 다른 동기식 코드처럼 try/catch에 의존합니다.
  • 디버깅이 훨씬 쉬워집니다. .then 블록 안에 중단점을 설정하면 다음 .then으로 이동하지 않습니다. 왜냐하면 동기식 코드만을 거치기 때문입니다. 그러나 await 호출은 동기식 호출처럼 거치게 됩니다.

하지만 무엇보다도 동기식 처리 사고(1번라인 후 2번라인 3번 라인 실행)가 프로그래머가 함수를 작성할 때 가장 자연스러운 방식이며 async await 의 도입으로 비동기식 언어인 Javascript를 동기식 처리 방식으로 작성할 수 있게 해 주었다는 점입니다.