[모던JS: Core] 비동기 - 프라미스(Promise) 및 async/await (4)

KG·2021년 5월 28일
0

모던JS

목록 보기
22/47
post-thumbnail

Intro

본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.

async와 await

asyncawait은 비동기 관련하여 ES8(ES2017)에 추가된 비교적 최신 스펙의 문법이다. 이를 이용하면 기존의 프라미스 기반의 코드를 조금 더 편하게 사용할 수 있다.

1) async

asyncfunction 앞에 위치한다.

async function f() {
  return 1;
}

함수 앞에 async를 붙이게 되면 해당 함수는 항상 프라미스를 반환한다. 프라미스가 아닌 값을 반환하더라도 이행 상태의 프라미스(resolved promise)로 감싸 이행된 프라미스가 반환되도록 한다. 물론 명시적으로 프라미스를 반환하는 것도 가능한데 아래 두 코드의 결과는 동일하다.

async function f1() {
  return 1;
}

async function f2() {
  return Promise.resolve(1);
}

f1().then(console.log);	// 1
f2().then(console.log);	// 1

async가 붙은 함수는 반드시 프라미스를 반환하고, 프라미스 아닌 것은 프라미스로 감싸서 반환한다.

2) await

await은 항상 async function 안에서만 동작한다. 자바스크립트는 await 키워드를 만나면 프라미스가 처리(settled)될 때까지 기다리고 처리가 완료되면 그 이후에 값을 반환한다.

async function f() {
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve('done'), 1000)
  });
  
  // 1초 뒤에 프라미스가 이행되기 까지 대기 후 반환
  let result = await promise;
  
  // 따라서 이 시점에서 바로 값을 사용 가능
  console.log(result);	// done
}

f();

await은 그 뜻 그대로 프라미스가 처리될 때 까지 함수 실행을 기다리게 한다. 프라미스가 처리되고 나서 그 결과와 함께 실행이 재개된다. 프라미스가 처리되길 기다리는 동안에는 엔진이 다른 일을 할 수 있기 때문에 리소스가 낭비되지 않는 장점이 있다.

awaitpromise.then(...)과 같은 역할을 하지만 코드의 흐름을 동기 방식으로 작성하더라도 비동기를 처리할 수 있다는 장점이 있다.

3) 프라미스 체이닝 with async/await

프라미스 체이닝에서 구현했던 showAvatar() 코드를 async/await을 사용해 다시 작성해보자.

async function showAvatar() {
  // JSON 파싱
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();
  
  // github 사용자 정보 처리
  let githubResponse = awaitfetch(`https://api.github.com/users/${user.name}`);
  let githubUser = await githubResponse.json();
  
  // 아바타 렌더링
  let img = document.createElement('img');
  img.src = githubUser.avatar_url;
  img.className = 'avatar';
  document.body.append(img);
  
  // 3초 대기
  await new Promise((resolve, reject) => setTimeout(resolve, 3000));
  
  img.remove();
  
  return githubUser;
}

showAvatar();

프라미스 역시 콜백 방식에 비해 코드가 깔끔하다는 장점이 있었지만 async/await 방식인 이보다 더 가독성이 뛰어나다. 코드의 작성 흐름 자체도 대부분이 익숙한 동기 방식과 동일하기에 작성 역시 더 편하다는 장점도 있다.

이때 다시 한 번 주의해야 할 것이 있다. awaitasync function 내부에서만 작동한다고 했다. 따라서 await은 최상위 레벨 코드에서는 사용할 수 없다.

let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();	// Error

만약 최상위 레벨에서 await을 사용하고자 한다면 다음과 같이 IIFE 방식으로 익명 async function을 사용해 코드를 감싸는 방법이 있다.

(async () => {
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();
})();

Node와 유사한 자바스크립트 런타임 환경은 Deno의 경우엔 자체적으로 top-level-await를 지원하기 때문에 await를 최상위 코드에서도 사용할 수 있다.

또한 await는 프라미스에 then 핸들러가 그러했던 것처럼 Thenable 객체를 처리할 수 있다.

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then (resolve, reject) {
    console.log(resolve);
    setTimeout(() => resolve(this.num*2), 1000);
  }
};

async function f() {
  let result = await new Thenable(1);
  console.log(result);
}

f();

또한 class의 메서드 역시 async를 사용하여 async 클래스 메서드로 만들어 줄 수 있다.

class Waiter {
  async wait() {
    return await Promise.resolve(1);
  }
}

new Waiter()
  .wait()
  .then(console.log);	// 1

4) 에러 핸들링

프라미스가 정삭적으로 이행되면 await promise는 프라미스 객체의 result에 저장된 값을 반환한다. 반면 프라미스가 거부되면 마치 throw문을 작성한 것처럼 에러가 던져진다. 따라서 아래의 두 코드는 동일하다.

async function f1() {
  await Promise.reject(new Error('error'));
}

async function f2() {
  throw new Error('error');
}

실제 상황에서는 프라미스가 거부 되기 전에 약간의 시간이 지체되는 경우가 있다. 이런 경우엔 await가 에러를 던지기 전에 지연이 발생한다. await가 던지는 에러는 throw가 던지는 에러를 잡을 때 처럼 try...catch 구문으로 잡을 수 있다.

앞서 다룬 것과 동일하게 try 블록에서 발생하는 모든 에러는 catch 블록에서 잡을 수 있다.

async function f() {
  try {
    let response = await fetch('http://wrong');
    let user = await response.json();
  } catch (err) {
    // fetch와 response.json()에서 발생하는 
    // 모든 에러를 여기서 잡을 수 있다
    console.log(err);	
  }
}

f();

또는 try...catch 구문을 사용하지 않고 프라미스의 catch 핸들러를 사용하는 것도 가능하다. 거부 상태가 된 프라미스가 있을때 .catch 핸들러를 추가하면 거부된 프라미스를 손쉽게 처리할 수 있다.

async function f() {
  let response = await fetch('http://wrong');
}

f().catch(console.log);	// TypeError: failed to fetch

이처럼 async/await을 사용하면 await가 대기를 처리해주기 때문에 .then이 거의 필요하지 않다. 또한 .catch 핸들러 대신 일반적인 try...catch 구문으로 에러를 처리할 수 있다는 장점 또한 생긴다. 항상 그러한 것은 아니지만 promise.then 을 사용하는 것보다 async/await을 사용하는 것이 대개 편리한 경우가 많다.

여러 개의 프라미스가 모두 처리되길 기다려야 하는 상황이라면 이 프라미스들을 Promise.all로 감싸고 여기에 await을 붙여 사용할 수 있다.

// async function ... 이라고 가정
let results = await Promise.all([
  fetch(url1),
  fetch(url2),
  ...
)];

resultsPromise.all이 처리를 완료할 때까지 대기한 후 그 결과를 저장한다. 만약 실패한 프라미스로 에러가 전파되면 보통 에러와 마찬가지로 Promise.all로 전파되는 것 역시 동일하다. 이때 생긴 예외 또한 try...catch로 감싸 잡을 수 있다.

크롬 개발자 도구에는 top-level-await가 적용되어 있어 async function을 선언하지 않더라도 await이 정상 작동한다. (참고링크)

References

  1. https://ko.javascript.info/async
profile
개발잘하고싶다

0개의 댓글