본문 바로가기
프로그래밍 언어/Javascript

Javascript Async

by whale in milktea 2023. 2. 19.

거의 대부분의 프로그래밍 언어에서 비동기는 중요하게 다뤄진다.

그 이유는, 대부분의 프로그래밍 언어가 싱글 스레드 기반의 동기식 작업 처리방식을 갖고 있기 때문이다.

 

동기식 처리와 비동기식 처리의 차이는 다음의 그림을 통해 확인할 수 있다.

그림에서도 보여지듯, 비동기적으로 처리된 작업시간은 동기적으로 작업하는 작업시간보다 빠르게 작업이 이뤄진다.

이는, 서버 통신이 이뤄지는 동안 타 작업을 수행하든지, 일정 시간이 지난 후 팝업/영상을 띄우는 setTimeout / 일정 간격으로 특정 작업을 수행하는 setInterval 등 호출 스케줄링 등의 경우에서 아주 중요하게 다뤄진다.

 

그 예시로, 만약 GPS 정보를 활용한 작업이 필요한 경우, GPS 정보를 가져오고 이에 관한 연속 작업이 이뤄지는 동안 사용자는 아무것도 할 수 없는 것이 종전의 동기식 작업처리이다. 반면 비동기로 처리할 경우, 이와 달리 위치정보를 받아오는 동안 사용자는 JS 코드에 정의된 다른 작업을 수행할 수 있게 된다.

 

비동기적으로 처리된 작업은 Callback 함수에 의해 다시 호출되고, 작업이 동기적으로 처리된다.

 

Callback 함수와 Callback Hell

비동기 처리를 위해 사용되는 Callback 함수의 기본 사용법은 MDN의 예시 코드를 바탕으로 참조하고자 한다.

// MDN Callback function quick example
// 가장 아래의 studyAlert는 본인의 학습을 위해 추가함

function greeting(name) {
  alert(`Hello, ${name}`);
}

function processUserInput(callback) {
  const name = prompt("Please enter your name.");
  callback(name);
}

function studyAlert() {
	alert("동기식 코드")
}

processUserInput(greeting);
studyAlert();

위의 식은 동기적으로 즉시 처리되는 함수이다.

함수의 실행 순서는 processUserInput에 매개변수로 greeting 함수를 넣고 이 함수가 processUserInput 함수 내에서 호출되어 실행된다. 그 뒤에 studyAlert가 실행된다.

* 순서 : prompt에 이름을 넣고 -> greeting의 alert가 뜬 뒤 -> studyAlert의 alert가 실행된다.

 

비동기적으로 callback 함수가 처리된 코드는 다음과 같다.

function greeting(name) {
  alert(`Hello, ${name}`);
}

function processUserInput(callback) {
  setTimeout(function() {
    const name = prompt("Please enter your name.");
    callback(name);
  }, 1000); // 1초 후에 실행되도록 지연시킴
}

function studyAlert() {
  alert("비동기적으로 처리된 코드");
}

processUserInput(greeting);
studyAlert();

여기서 processUserInput과 greeting은 앞서 동기적 처리가 이뤄지는 콜백함수와 동일한 작업으로 묶여있지만,

processUserInput 안에 setTimeout 메서드가 들어가 1초 뒤에 함수가 실행되게 되었다.

이 경우, 1초라는 시간동안 함수가 실행되지 않음으로 비동기처리 되어 studyAlert 함수가 먼저 실행되고 이후 processUserInput 함수가 호출되며 greeting 함수가 내부에서 콜백함수로 호출된다.

 

* 순서 : 순서 : studyAlert의 alert가 실행된다 ->prompt에 이름을 넣고 -> greeting의 alert가 뜬다.

 

근데, 만약 name뿐만 아니라, age, gender, profession까지 물어보는 함수를 만든다면 어떻게 될까?

다음의 코드를 살펴보고자 한다.

function greeting(name, age, gender, profession) {
  alert(`Hello, ${name}. 당신은 ${age}살의 ${gender}이고, ${profession}일을 하고 계시군요!!! 반갑습니다!.`);
}

function promptName(callback) {
  const name = prompt("이름을 입력해주세요.");
  callback(name);
}

function promptAge(name, callback) {
  const age = prompt(`Hello, ${name}. 나이를 입력해주세요.`);
  callback(name, age);
}

function promptGender(name, age, callback) {
  const gender = prompt(`성별을 입력해주세요.`);
  callback(name, age, gender);
}

function promptProfession(name, age, gender, callback) {
  const profession = prompt(`직업을 입력해주세요.`);
  callback(name, age, gender, profession);
}

promptName((name) => {
  promptAge(name, (name, age) => {
    promptGender(name, age, (name, age, gender) => {
      promptProfession(name, age, gender, (name, age, gender, profession) => {
        greeting(name, age, gender, profession);
      });
    });
  });
});

위와 같이 상당히 가독성이 떨어지는 코드가 만들어지게 된다.

이와 같은 현상을 콜백지옥(Callback Hell)이라 부른다.

 

Promise와 Promise-chain

실리콘 밸리의 천재적인 선배 개발자님들은,,, 이를 해결하기 위해 반복되는 함수의 내용은 비동기처리로 묶어놓고,

Promise-Chain이라는 개념을 도입해서 "그 다음에는?", "아니라면?"에 해당하는 메서드를 개발하셨다.

 

이렇게 비동기처리를 위해 새롭게 도입된 개념이 Promise 개념이고, Promse 객체를 활용해서 callback 함수를 호출한다.

callback 함수에서 name, age, gender, professtion을 활용한 코드를 promise로 리팩토링하면 다음과 같다.

 

function askQuestion(q) {
  // Promise 객체 생성
  return new Promise(function(res, err) {
    // setTimeout으로 비동기 처리
    setTimeout(function() {
      // 사용자에게 prompt로 질문을 던짐
      const answer = prompt(q);
      // 사용자가 입력한 값이 있는 경우, 이행(resolve)시키고 사용자의 답변을 반환
      if (answer) {
        res(answer);
      // 사용자가 입력한 값이 없는 경우, 거부(reject)시키고 오류 메시지를 반환
      } else {
        err(Error("답변을 입력하지 않았습니다."));
      }
    }, 1000); // 1초 후에 실행되도록 지정
  });
}

askQuestion("이름을 입력해주세요") // askQuestion 함수 실행
  .then(function(name) { // 이행된 경우, 다음 then 메서드로 넘어감
    console.log(`Hello, ${name}`); // 이행된 결과를 출력
    return askQuestion("나이를 입력해주세요"); // 새로운 질문을 던져서 Promise 반환
  })
  .then(function(age) { // 이행된 경우, 다음 then 메서드로 넘어감
    console.log(`${name}은 ${age}살입니다.`); 
    return askQuestion("성별을 입력해주세요"); 
  })
  .then(function(gender) { 
    console.log(`${name}은 ${gender}`); 
    return askQuestion("직업을 입력해주세요"); 
  })
  .then(function(profession) { 
    console.log(`${name}은 ${profession}`); 
    console.log("답변해주셔서 감사합니다."); 
  })
  .catch(function(error) { // Promise가 거부된 경우, catch 메서드로 이동하여 처리
    console.log(error); // 오류 메시지를 출력
  });

Promise-Chain에 해당하는 개념은 .then(), .catch() 부분이다.

이는 Promise 객체에서 호출된 콜백함수가 실행이 완료되면, 그 다음에 어떻게 할지 정의하는 메서드이다.

 

Promise 체인에서 쓰이는 메서드는 .then(), .catch() 말고도 꽤나 많다.

.all() : 여러 개의 프로미스 체인이 모두 완료된 후 이행된 결과를 한번에 배열로 반환한다

.race() : 여러 개의 프로미스 체인 중 가장 먼저 이행된 결과만을 반환한다.

.finally() : 프로미스 체인 과정에서 발생한 에러를 넘어가고 마지막에 정의된 함수를 반드시 실행한다.

... 등등!

 

Async / Await

위에서 살펴본 Promise도 callback 함수를 따로 정의하주고, 여러 체인으로 묶어야 하는 불편함이 있다.

이를 극복하기 위해, Async 함수를 선언하고, await 메서드로 체이닝을 하는 개념이 등장했다.

 

promise에서 특별한 객체를 선언하고 객체 내에서 함수를 호출했듯, 

async / await 개념에서도 try라는 특별한 객체를 사용한다. 또한 async/await 개념도 promise chain에 기반하기에 promise 체인에 쓰이는 메서드들을 동일하게 사용할 수 있다.

 

async/await 코드는 다음과 같다.

function askQuestion(q) {
  return new Promise(function(res, err) {
    setTimeout(function() {
      const answer = prompt(q);
      if (answer) {
        res(answer);
      } else {
        err(Error("답변을 입력하지 않았습니다."));
      }
    }, 1000); // 1초 후에 실행되도록 지정
  });
}

async function askQuestions() {
  try {
    const name = await askQuestion("이름을 입력해주세요");
    const age = await askQuestion("나이를 입력해주세요");
    const gender = await askQuestion("성별을 입력해주세요");
    const profession = await askQuestion("직업을 입력해주세요");
  } catch (error) {
    console.log(error);
  }
}

askQuestions();