<About async/await +++++ 4탄>

강민수·2021년 12월 11일
0

백엔드

목록 보기
4/21

이제 콜백의 최종 완결판 async/await를 진행 한다. 물론 astnc와 await이 깔끔하게 프로미스를 사용하는 것은 맞다. 하지만, 그렇다고 무조건 프로미스가 나쁘고 에이싱크와 어웨이트로 대체해서 사용해야 한다는 것은 아니다. 상황에 맞게 써야 한다.

01) 기본 구조

async: 비동기 작업을 만드는 손쉬운 방법
async 키워드는 함수를 선언할 때 붙여줄 수 있다. async 키워드가 붙은 함수를 async 함수로, async 가 없는 함수는 일반 함수라고 부르도록 하겠다. 의미를 생각해본다면 async 함수는 비동기 작업 그 자체를 뜻한다는 말일 것 같은데, 실제로 우리가 어떻게 사용해볼 수 있을까?

async 함수는 Promise 와 굉장히 밀접한 연관을 가지고 있는데, 기존에 작성하던 executor 로부터 몇 가지 규칙만 적용한다면 new Promise(…) 를 리턴하는 함수를 async 함수로 손쉽게 변환 가능하다.

함수에 async 키위드를 붙인다.
new Promise... 부분을 없애고 executor 본문 내용만 남긴다.
resolve(value); 부분을 return value; 로 변경.
reject(new Error(…)); 부분을 throw new Error(…); 로 수정.
자 그럼 기다릴 것 없이 바로 직전에 작성했던 startAsync 함수를 async 함수로 바꾸어보자.

 // 기존
// function startAsync(age) {
//  return new Promise((resolve, reject) => {
//    if (age > 20) resolve(`${age} success`);    
//   else reject(new Error("Something went wrong"));
//  });
// }
<script>
async function startAsync(age) {
  if (age > 20) return `${age} success`;
  else throw new Error(`${age} is not over 20`);
}

setTimeout(() => {
  const promise1 = startAsync(25);
  promise1
    .then((value) => {
      console.log(value);
    })
    .catch((error) => {
      console.error(error);
    });
  const promise2 = startAsync(15);
  promise2
    .then((value) => {
      console.log(value);
    })
    .catch((error) => {
      console.error(error);
    });
}, 1000);
</script>

02) await: Promise 가 끝날 때까지 기다려!

어웨이트 역시 에이싱크와 함께 동반되는 개념이다. 다만, 어웨이트는 에이싱크 없이는 쓸 수가 없다. 그 부분은 후술하겠다. 또한, 어웨이트는 말 그대로 기다리는 함수다. 그래서 await 는 Promise 가 fulfilled 가 되든지 rejected 가 되든지 아무튼 간에 끝날 때까지 기다린다.

<script>
function setTimeoutPromise(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(), ms);
  });
}

async function startAsync(age) {
  if (age > 20) return `${age} success`;
  else throw new Error(`${age} is not over 20`);
}

async function startAsyncJobs() {
  await setTimeoutPromise(1000);
  const promise1 = startAsync(25);
  try {
    const value = await promise1;
    console.log(value);
  } catch (e) {
    console.error(e);
  }
  const promise2 = startAsync(15);
  try {
    const value = await promise2;
    console.log(value);
  } catch (e) {
    console.error(e);
  }
}

startAsyncJobs();
</script>

<어웨이트로 변경하면서 바뀐 변경 점>

  1. setTimeout 을 Promise 버전으로 하여 setTimeoutPromise 함수를 새로 만들었다. 이 함수는 setTimeout 함수를 활용하여 지정된 ms 만큼 기다린 후 resolve 를 호출한다. 이렇게 만든 Promise 의 then 으로 다음 동작을 정의할 수 있다. then 동작은 resolve 함수가 호출되면 실행된다고 했었다. 자연스럽게 ms 만큼 기다린 후 다음 동작으로 넘어간다.

  2. startAsyncJobs 함수를 새로 만들었다. 이 함수 내에서 await 을 사용하기 위해 async 함수로 정의내린 후, 코드의 마지막 부분에서 호출함으로써 비동기 작업을 시작했다. 기존의 then 과 catch 하던 작업들은 모두 이 함수 내에 있다.

<어웨이트의 특징>

문법적으로 await [[Promise 객체]] 이렇게 사용한다.

await 은 Promise 가 완료될 때까지 기다린다. 그러므로 setTimeoutPromise 의 executor 에서 resolve 함수가 호출될 때까지 기다린다. 그 시간동안 startAsyncJobs 의 진행은 멈춰있다.

await 은 Promise 가 resolve 한 값을 내놓는다. async 함수 내부에서는 리턴하는 값을 resolve 한 값으로 간주하므로, ${age} success 가 value로 들어온다는 점을 알 수 있다.

해당 Promise 에서 reject 가 발생한다면 예외가 발생한다. 이 예외 처리를 하기 위해 try-catch 구문을 사용했다. reject 로 넘긴 에러(async 함수 내에서는 throw 한 에러)는 catch 절로 넘어간다. 이로써 본래 해왔던 에러 처리 하듯이 진행할 수 있다.

await 은 then 과 catch 의 동작을 모두 자기 나름대로 처리한다. 그래서 async 함수 내에서 then, catch 메소드의 존재를 잊게 할 수 있다. 즉 콜백 함수를 넘기고 흐름을 제어하던 때가 엊그제 같은데… 라며 과거 회상을 할 수 있다는 뜻!

!!!< await가 async에서만 사용 되는 이유>

비동기는 동작 특성상 실제 작업과 그 작업의 후속조치를 따로 분리시킬 수 밖에 없는데, (그래서 then, catch 등을 썼는데) async 와 await을 쓰면 하나의 흐름 속에서 코딩할 수 있게 해준다! 실제 작업이 끝난 다음 그 후속조치를 수행한다. 가 아니라, 실제 작업이 끝나는 걸 기다린 다음 다음 코드를 수행한다의 느낌으로, 코딩할 수 있는 것. 기다리는 게 뭔가? 동기 코드를 쓸 때 마냥 기다렸다? 그걸 할 수 있다는 것. async와 await은 우리가 예전에 동기 코드를 작성했던 익숙한 경험 속에서 비동기 작업들을 코딩할 수 있게 해준다. 당장 then과 catch를 사용한 코드와 async, await 까지 활용한 코드를 비교해보면 체감이 확 될 것이다.

03. 기타 프로미스 함수들

01) promise.all

아래와 같이 직원의 id 를 입력하면 그 직원의 나이를 반환해주는 fetchAge 함수가 있다고 가정하자. (내부적으로는 1초 기다린 뒤 랜덤 나이를 반환하는 식.) 그 다음 id 0번 부터 9번까지의 직원의 나이에 대한 평균치를 구하고 싶다. 그렇다면 모든 결과를 한 데 모아서 평균치를 구하면 된다! 여기서의 비동기 동작의 의존 관계는 지금과는 사뭇 다른 느낌. 0번부터 9번까지 직원의 정보를 요청하는 것은 비동기로 작업하면 되지만, 모든 비동기 작업이 완료되고 나서 다음 작업을 해야 한다! 그래서 한 번에 구하고 싶어 등장한 것이 프로미스 all~

<script>
// 다른 코드는 똑같습니다.

async function startAsyncJobs() {
  let promises = [];
  for (let i = 0; i < 10; i++) {
    promises.push(fetchAge(i));
  }
  
  let ages = await Promise.all(promises);

  console.log(
    `평균 나이는? ==> ${
      ages.reduce((prev, current) => prev + current, 0) / ages.length
    }`
  );
}

startAsyncJobs();// 다른 코드는 똑같습니다.

async function startAsyncJobs() {
  let promises = [];
  for (let i = 0; i < 10; i++) {
    promises.push(fetchAge(i));
  }
  
  let ages = await Promise.all(promises);

  console.log(
    `평균 나이는? ==> ${
      ages.reduce((prev, current) => prev + current, 0) / ages.length
    }`
  );
}

startAsyncJobs();



</script>

0 사원 데이터 받아오기 완료!
1 사원 데이터 받아오기 완료!
2 사원 데이터 받아오기 완료!
3 사원 데이터 받아오기 완료!
4 사원 데이터 받아오기 완료!
5 사원 데이터 받아오기 완료!
6 사원 데이터 받아오기 완료!
7 사원 데이터 받아오기 완료!
8 사원 데이터 받아오기 완료!
9 사원 데이터 받아오기 완료!
평균 나이는? ==> 33.1

Promise.al을 쓰면 한 번에 다 완료된 값이 동시에 송출 된다. 이 함수는 인자로 Promise 의 배열을 받으며, 하나의 특별한 Promise 를 새로 생성한다. 이 Promise는 배열로 받은 모든 비동기 작업이 성공했다면 내부적으로 resolve 를 호출하며, 하나라도 비동기 작업이 실패한다면 reject 를 호출한다.

02) promise.race

때로는 이럴때 도 있을 것이다. 다 기다려 주지말고 가장 빨리 되는 것 구하기는 없나? 있다. 그게 바로 레이스다. 마치 경주에서 1등 생각하면 된다.

Promise.race() 메소드는 Promise 객체를 반환한다. 이 프로미스 객체는 iterable 안에 있는 프로미스 중에 가장 먼저 완료된 것의 결과값으로 그대로 이행하거나 거부한다.

<script>
const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two');
});

Promise.race([promise1, promise2]).then((value) => {
  console.log(value);
  // Both resolve, but promise2 is faster
});
// expected output: "two"
</script>

휴~ 아직 갈 길은 멀지만, 그래도 어느 정도 긴 레이스가 끝이 보이기 시작한다. 동기 비동기로 시작한 레이스가 이제는 거의 끝이 보인다. 우리가 그동안 나무를 보는 학습을 했다면 이제는 우리가 자바스크립트가 웹에서 처리되는 숲의 처리 과정을 익혀야 될 때가 왔다. 다음 시리즈는 바로 대망의 이벤트 루프가 처리되는 일련의 과정을 설명하겠다.

--------------------to be continued---------------------

profile
개발도 예능처럼 재미지게~

0개의 댓글