async & await

myway_7·2022년 3월 1일
0
post-thumbnail

async await는 promise를 조금 더 간결하고 간편하고, 동기적으로 실행되는 것 처럼 보이게 만드는 키워드

promise/then+promise/then 이렇게 연속적으로 작성하게 되면 코드가 상당히 복잡하게 구성된다. 하지만 여기에 async await를 사용하게 되면 일반 동기적 실행 코드를 작성할 때 처럼 코드를 직관적으로 작성할 수 있게 된다.

이는 새로운 것이 아니라 기존의 promise 위에 조금더 간편하게 쓸 수 있게 제공되는 API이다. 기존의 것을 조금더 간편하게 사용할 수 있게 제공되는 API를 일컬어 synctatic sugar라고 부른다. 다른 예로 클래스는 프로토타입을 기반으로 그 위에 살짝 덧붙여진 syntatic sugar이다.

1. async

function fetchUser() {
	//네트워크에서 사용자 정보 받아오는데 10초 정도 걸리는 작업이 있다고 가정...
	return '다운로드한 사용자 이름';
}

const user = fetchUser();
console.log(user);

위 코드에서 만약 화면에 표시되는 요소가 user에 저장된 정보를 사용한다면, 다운로드가 완료되는 10초 동안 사용자는 아무것도 표시되지 않는 빈 화면만 쳐다보고 있어야 한다.

알다시피 이렇게 오래 걸리는 작업은 비동기적으로 처리할 수 있게 해준다. 바로 앞서 배운 promise를 사용하는 방법이다.

function fetchUser() {
	//네트워크에서 사용자 정보 받아오는데 10초 정도 걸리는 작업이 있다고 가정...
	return new Promise();
}

const user = fetchUser();
console.log(user);

Promise를 사용할 때 executer에 해당하는 콜백함수가 Promise 클래스의 인자로 들어가고, 이 콜백함수는 각각 resolve, reject라는 콜백함수를 인자로 갖는다.

function fetchUser() {
	//네트워크에서 사용자 정보 받아오는데 10초 정도 걸리는 작업이 있다고 가정...
	return new Promise((resolve, reject) => {
		resolve('user name');
		//만약 그냥 return 'user name'; 을 사용하면 콘솔창에 promise 객체의 state는 Pending이 뜰 것이다
	});
}

const user = fetchUser();
console.log(user);
  • promise 객체는 pending, fulfill, reject 이렇게 3가지 state (상태)를 갖는다.

resolve 콜백함수로 반환된 결과값은 .then 메소드를 통해 사용 가능하다.

function fetchUser() {
	//네트워크에서 사용자 정보 받아오는데 10초 정도 걸리는 작업이 있다고 가정...
	return new Promise((resolve, reject) => {
		resolve('user name');
		//만약 그냥 return 'user name'; 을 사용하면 콘솔창에 promise 객체의 state는 Pending이 뜰 것이다
	});
}

const user = fetchUser();
user.then(console.log); //괄호 등으 생략 문법은 promise 글 참고할 것

자 이제 여기에 async를 사용하여 Promise 클래스를 대체 해 보자.

async function fetchUser() {
	return 'user name';
}

const user = fetchUser();
user.then(console.log);

위의 코드는 우리가 처음 작성했던 기본형과 동일하다. 단지 function 키워드 앞에 async 키워드만 추가되었을 뿐이다. 이걸 통해 우리는 async라는 키워드의 기능이

함수가 promise 객체를 return 하도록 만든다.

2. await (기다려!)

await이라는 키워드는 async 키워드가 붙은 함수 안에서만 사용 가능하다.

await를 이용하는 함수를 한번 만들어보자

//마치 특정 시간 동안 다운로드 받은 후 결과를 반환하는 fetch 함수가 있다고 가정
//또한, 이 delay 함수는 비동기 함수라고 가정한다.
function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

위의 함수는 비동기 실행으로 동작한다고 가정하자. 즉 특정 ms 시간동안 백그라운드에서 다운로드가 이뤄지고, 그 동안 그 다음 동기 실행 함수들은 동작한다.

이제 async, await를 활용해보자

 
 function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  async function getApple() {
    await delay(1000);
    return '🍎';
//여기서 이 사과 이모티콘은 하드코딩으로 직접 넣어놨지만 실제론 delay()
//함수가 가져온 어떤 데이터를 return 하는 것이라 생각하자.
}

위의 getApple 함수는 앞서 배운 것 처럼 async 키워드를 사용하여 return 되는 값이 promise가 되도록 만들었다.

그 후에 delay() 함수 호출 코드 앞에 await 키워드를 붙였다.

여기서!

await 키워드의 의미는

이 키워드를 붙인 함수는, “이 함수는 비동기 함수지만 해당 함수의 작업이 완료된 후 그 다음 작성된 동기 함수를 실행하라“

앞서, delay() 함수를 비동기 함수로 가정한다고 했다.

따라서 위의 의미를 다시 말하면, 비동기 함수의 경우 완료까지 시간이 걸리는 작업은 백그라운드에서 작업하고 메인 작업에서는 그 다음 작성된 코드를 실행하지만, await 키워드는 이러한 비동기 함수가 완료될 때 까지 그 다음 함수들의 실행을 멈추는 역할을 한다.

이러한 성질을 이용하여 promise 체인과 같은 작업을 만들어보자

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  async function getApple() {
    await delay(1000);
    return '🍎';
  }

  const getBanna = async () => {
    await delay(1000);
    return '🍌';
  };

  function pickFruits() {
    return getApple().then(apple => {
      return getBanna().then(banna => `${apple + banna}`);
    });
  }

  pickFruits().then(console.log);

위의 코드를 하나하나 살펴보자

우선 getApple 부터 다시 살펴보면, 앞서 기존에 작성한 delay() 함수는 1000ms 지난 후 결과를 반환하는데 await 키워드는 이 작업이 완료될 때 까지 그 다음 코드들의 실행을 기다리게 한다. 1초 후 return '🍎'; 코드가 실행되어 getApple() 함수는 최종 값을 return 한다.

getBanna() 함수는 위의 동작과 완전히 동일하다.

또한 두 함수 모두 async 키워드로 인해 return 값은 promise 객체이다.

이제 pickFruits() 함수를 보면, return 결과는 getApple() 함수를 실행하고(그 결과는 promise객체) promise 객체에 내장된 then 메소드를 실행한다.

여기서 then 메소드의 콜백함수를 보면, getApple 함수가 return 한 promise 객체를 인자로 받는다.

그 후 콜백함수는 getBanna() 함수의 실행과 앞선 작업과 동일하게 getBanna() 함수가 return한 promise 객체의 then 메소드를 사용하고 최종적으로 getApple과 getBanna 함수의 return 값을 합친 값을 return 한다.

동작 순서

  1. getApple 함수 실행 → apple 관련 값을 담은 promise 객체 return
  2. promise 객체에 담긴 값을 콜백함수의 인자로 받아 then 메소드 실행
  3. 이 then 메소드는 getBanna() 함수의 실행을 return
  4. getBanna() 함수는 실행 결과로 역시나 promise 객체를 return
  5. promise 안에 담긴 값을 인자로 받아 then 메소드 실행
  6. 최종적으로 then 메소드는 앞서 getApple이 넘겨준 값과 그 후에 getBanna가 넘겨준 값을 합쳐 결과를 return

두 개의 return이 중첩되어 헷갈린다면 마지막 return만 보면 된다. 앞의 return 되는 결과를 이어받아 두 번째 return이 마지막 결과를 return 하므로 마지막 return만 보면 된다.

위의 코드 실행은 1초 씩 기다리는 함수 2개가 존재하므로, 도합 2초 후 콘솔 창에 결과가 출력된다.

콜백지옥 코드를 정리해보자

위의 await를 이용하는 코드는 return 이 중첩되어 가독성이 매우 떨어진다. 이를 더욱 간략하게 정리해보자

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  async function getApple() {
    await delay(1000);
    return '🍎';
  }

  const getBanna = async () => {
    await delay(1000);
    return '🍌';
  };

 async function pickFruits() {
    const apple = await getApple();
    const banna = await getBanna();

    return apple + banna;
  }

  pickFruits().then(console.log);

위의 pickFruits 함수를 보면, 기존 코드에 비하여 매우 직관적으로 변한 것을 볼 수 있다.

getApple, getBanna 함수 모두 1초씩의 완료 시간이 필요하므로 이 함수들의 결과가 return 되기까지 await 키워드를 이용하여 기다린 후 각각 apple, banna 라는 변수에 결과를 저장하도록 했다.

await 키워드 때문에 두 줄의 변수 정의 코드가 완료될 때 까지, return apple + banna 명령은 실행되지 않고 기다리다가 변수 정의가 완료 된 후 실행된다.

그 결과는 다음과 같다.

3. try...catch

async, await 구문을 사용하는 경우 then을 쓸 때 처럼 catch를 사용할 수 없다.

이 경우 try...catch 구문을 사용하면 에러 처리를 할 수 있다.

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  async function getApple() {
    await delay(1000);
    return '🍎';
  }

  const getBanna = async () => {
    await delay(1000);
    return '🍌';
  };

 async function pickFruits() {
    try {
      const apple = await getApple();
      const banna = await getBanna();

      return apple + banna;
    } catch (err) {
      console.log(err);
    }
  }

  pickFruits().then(console.log);

pickFruits 함수를 보자.

try...catch 구문은, try 구문에서 중괄호 안의 내용을 실행하고 해당 실행에서 error 메시지가 반환된다면 catch가 이를 받아 (이때 에러 객체는 인자로 제공된다) 실행된다.

3-1 throw 구문

throw 구문은

throw문은 사용자 정의 예외를 발생(throw)할 수 있습니다. 예외가 발생하면 현재 함수의 실행이 중지되고 (throw 이후의 명령문은 실행되지 않습니다.), 제어 흐름은 콜스택의 첫 번째 [catch](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/try...catch) 블록으로 전달됩니다. 호출자 함수 사이에 catch 블록이 없으면 프로그램이 종료됩니다.
출처 - MDN

위의 설명처럼 throw 구문은 어떤 예외 상황 (에러)를 return 한다.

만약 throw 구문 실행 이후 catch 구문이 작성되어 있다면 catch는 throw가 반환한 에러를 받아 실행한다.

Syntax

throw expression;

4. await의 병렬처리

위의 getApple, getBanna 함수가 연속해서 실행된다면 각 1초씩 총 2초의 대기시간이 발생한다.

즉, await를 순차적으로 실행하면 비효율적이다. (물론 getBanna가 getApple의 결과를 이용하는 함수라면 순차적으로 실행해야만 하지만...)

promise는 정의, 생성되는 순간 내부의 콜백함수가 바로 실행된다.

async function pickFruits() {
    const applePromise = getApple();
    const bannaPromise = getBanna();

    const apple = await applePromise;
    const banna = await bannaPromise;

    return apple + banna;
  }

새롭게 수정한 위의 pickFruits 함수를 보면, 우선 2개의 변수에 getApple(), getBanna() 함수의 실행을 정의했다. 두 함수의 실행은, new Promise 구문을 실행시켜 promise 객체를 생성한다.

여기서 중요한 점은 이미 한번 생성된 promise 객체는 그 다음부턴 즉시 실행이 가능하다는 점이다. 즉 여러개의 promise 객체를 미리 생성했다면, 여러 개의 promise 객체의 중복 실행은 병렬적으로 처리된다는 점이다. promise 객체 생성 === 내부의 콜백함수를 작업대 위에 올려놓는 꼴

때문에 이후 apple = await applePromise는와 bannaPromise는 순차적으로 실행되는 것이 아니라 병렬로 실행된다.

5. Promise.all() API

만약 병렬로 처리해야 하는 작업이 매우 많다면, 위와 같이 하나하나 promise 객체를 생성해주는 코드를 작성해야 한다.

이러한 번거로움을 해결하기 위해 자바스크립트에서는 Promise.all() API 를 제공한다.

pickFruits 함수를 다시 간략하게 수정해보자

async function pickFruits() {
    return Promise.all([getApple(), getBanna()]).then(res => {
      return res.join(' + ');
    });
  }

Promise.all API는 인자로 전달받은 promise 배열(각 요소가 promise)의 전체 요소가 작업이 완료될 때 까지 기다린 후, 완료된 결과 값을 배열의 각 요소 자리에 담은 promise 객체를 반환한다.

사용자는 이를 .then 키워드를 사용하여 배열 처리 작업을 해주면 된다.

위의 코드의 경우 getApple(), getBanna()의 각 결과값을 + 문자열 기호로 합쳐 return 한다.

6. Promise.race() 가장 빠른 놈 하나만...

추가적으로 여러 개의 promise 작업이 병렬로 처리될 때, 가장 빨리 완료된 작업 결과물만 return 하게 해주는 API가 있다. Promise.race()

위의 코드를 기반으로 예시를 들어보면

async function pickFruits() {
    return Promise.race([getApple(), getBanna()]);
  }

pickFruit().then(console.log);

race API의 인자로는 all API와 마찬가지로 promise 배열을 전달한다.

배열의 각 요소가 실행되고 가장 빨리 완료된 promise의 값만을 외부로 return한다.

profile
[...Way to FrontEnd Web Developer]

0개의 댓글