비동기 JavaScript: 콜백 지옥에서 비동기 및 대기로

게시 됨: 2022-03-11

성공적인 웹 애플리케이션 작성의 핵심 중 하나는 페이지당 수십 개의 AJAX 호출을 수행할 수 있다는 것입니다.

이것은 일반적인 비동기 프로그래밍 문제이며 비동기 호출을 처리하기로 선택하는 방법은 대부분 앱을 만들거나 끊을 수 있으며 확장하면 전체 시작을 잠재적으로 만들 수 있습니다.

JavaScript에서 비동기 작업을 동기화하는 것은 오랫동안 심각한 문제였습니다.

이 문제는 JavaScript 프레임워크를 사용하는 프런트엔드 개발자만큼 Node.js를 사용하는 백엔드 개발자에게 영향을 미칩니다. 비동기식 프로그래밍은 일상 업무의 일부이지만 종종 문제를 가볍게 여기고 적절한 시기에 고려하지 않습니다.

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

첫 번째이자 가장 간단한 솔루션은 중첩 함수 의 형태로 제공되었습니다. 이 솔루션은 콜백 지옥( callback hell )이라는 문제로 이어졌고 너무 많은 애플리케이션이 여전히 소진감을 느끼고 있습니다.

그런 다음 Promise 를 얻었습니다. 이 패턴은 코드를 훨씬 읽기 쉽게 만들었지만 DRY(Don't Repeat Yourself) 원칙과는 거리가 멀었습니다. 애플리케이션의 흐름을 적절하게 관리하기 위해 동일한 코드를 반복해야 하는 경우가 여전히 너무 많습니다. 최근 추가된 async/await 문의 형태는 마침내 JavaScript의 비동기 코드를 다른 코드 조각처럼 읽고 쓰기 쉽게 만들었습니다.

이러한 각 솔루션의 예를 살펴보고 JavaScript에서 비동기 프로그래밍의 발전을 반영해 보겠습니다.

이를 위해 다음 단계를 수행하는 간단한 작업을 살펴보겠습니다.

  1. 사용자의 사용자 이름과 암호를 확인합니다.
  2. 사용자에 대한 애플리케이션 역할을 가져옵니다.
  3. 사용자의 애플리케이션 액세스 시간을 기록합니다.

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

이러한 호출을 동기화하는 고대 솔루션은 중첩 콜백을 사용하는 것이었습니다. 이것은 간단한 비동기 JavaScript 작업에 적합한 접근 방식이었지만 콜백 지옥이라는 문제로 인해 확장되지 않았습니다.

그림: 비동기 JavaScript 콜백 지옥 안티 패턴

세 가지 간단한 작업에 대한 코드는 다음과 같습니다.

 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 원칙은 이 경우 전혀 가치가 없습니다. 예를 들어 오류 처리는 각 함수에서 반복되고 기본 콜백은 각 중첩 함수에서 호출됩니다.

비동기 호출을 통한 루프와 같은 더 복잡한 비동기 JavaScript 작업은 훨씬 더 큰 문제입니다. 사실, 콜백으로 이것을 하는 간단한 방법은 없습니다. 이것이 Bluebird 및 Q와 같은 JavaScript Promise 라이브러리가 많은 주목을 받은 이유입니다. 언어 자체가 아직 제공하지 않는 비동기 요청에 대해 일반적인 작업을 수행하는 방법을 제공합니다.

바로 여기에서 네이티브 JavaScript Promise가 등장합니다.

자바스크립트 약속

약속은 콜백 지옥을 탈출하는 다음 논리적 단계였습니다. 이 방법은 콜백의 사용을 제거하지 않았지만 함수의 연결을 간단하게 만들고 코드를 단순화하여 훨씬 더 읽기 쉽게 만들었습니다.

그림: 비동기 JavaScript Promise 다이어그램

Promise가 있는 경우 비동기 JavaScript 예제의 코드는 다음과 같습니다.

 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 }); };

이러한 종류의 단순성을 달성하려면 예제에 사용된 모든 함수가 Promised 이어야 합니다. Promise 를 반환하도록 getRoles 메서드가 어떻게 업데이트되는지 살펴보겠습니다.

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

두 개의 콜백이 있는 Promise 를 반환하도록 메서드를 수정했으며 Promise 자체가 메서드에서 작업을 수행합니다. 이제 resolvereject 콜백이 각각 Promise.thenPromise.catch 메서드에 매핑됩니다.

getRoles 메서드가 여전히 내부적으로 파멸의 피라미드 현상에 취약하다는 것을 알 수 있습니다. 이것은 데이터베이스 메서드가 Promise 를 반환하지 않기 때문에 생성되는 방식 때문입니다. 데이터베이스 액세스 메서드가 Promise 를 반환하는 경우 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: 비동기/대기

운명의 피라미드는 약속의 도입으로 크게 완화되었습니다. 그러나 우리는 여전히 Promise.then.catch 메소드에 전달되는 콜백에 의존해야 했습니다.

Promise는 JavaScript에서 가장 멋진 개선 사항 중 하나를 위한 길을 열었습니다. ECMAScript 2017은 asyncawait 문의 형태로 JavaScript의 Promises 위에 구문 설탕을 도입했습니다.

이 코드 샘플에서 보여주듯이 메인 스레드를 차단하지 않고 동기식인 것처럼 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 } };

Awaiting Promise to resolve는 비동기 함수 내에서만 허용되며 이는 verifyUserasync async function 를 사용하여 정의되어야 함을 의미합니다.

그러나 이 작은 변경이 이루어지면 다른 방법에 대한 추가 변경 없이 모든 Promiseawait 수 있습니다.

비동기 - 오랫동안 기다려온 약속의 해결

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

왜 지금 JavaScript async 기능을 사용하기 시작해야 합니까?

  1. 결과 코드는 훨씬 깨끗합니다.
  2. 오류 처리는 훨씬 간단하며 다른 동기 코드와 마찬가지로 try / catch 에 의존합니다.
  3. 디버깅은 훨씬 간단합니다. .then 블록 내부에 중단점을 설정하면 동기 코드만 단계별로 실행하기 때문에 다음 .then 으로 이동하지 않습니다. 그러나 await 호출을 동기 호출인 것처럼 단계별로 실행할 수 있습니다.