프론트엔드/Javascript

비동기 - async, await

syleemomo 2024. 3. 18. 16:33
728x90

* async 함수

async, await 문법을 사용하면 프로미스를 조금더 편하게 사용할 수 있다. 

async function f() {
  return 1
}

function 앞에 async 를 붙이면 해당 함수는 항상 프로미스를 반환한다. 프로미스가 아닌 값(return 1)을 반환하더라도, 내부적으로는 해당 값(1)을 프로미스로 감싸서 반환한다. return 이 resolve 호출과 동일하다. 

async function f() {
  return 1
}

f().then(alert) // 1

해당 코드를 실행하면 result 가 1인 이행 프로미스가 반환된다. 그러므로 then 을 연결해서 사용할 수 있다. 

async function f() {
  return Promise.resolve(1)
}

f().then(alert) // 1

해당 코드와 같이 명시적으로 프로미스를 반환하는 것도 가능한데, 결과는 동일하다.

 

* await 키워드 

// await는 async 함수 안에서만 동작합니다.
let value = await promise

자바스크립트가 await 키워드를 만나면 프로미스가 처리될때까지 기다린다. 결과는 그 이후에 반환된다. 

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000)
  })

  let result = await promise // 프라미스가 이행될 때까지 기다림 

  alert(result) // 1초 후 "완료!" 출력
}

f()

해당 코드는 아래의 순서로 동작한다. 

1. f 함수가 호출된다.
2. new Promise 에 의하여 프로미스 객체가 반환되고, 동시에 executor 함수가 실행된다.
3. await 키워드가 위치한 줄에서는 프로미스가 처리(성공 혹은 실패)되길 기다린다.
4. 프로미스가 처리되길 기다리는 동안에는 awiat 키워드 아래쪽 코드는 실행되지 않는다.
5. 1초 후에 resolve 함수를 호출하면서 프로미스가 처리되면 결과값을 result 변수에 저장한다. 
6. await 키워드 아래쪽 코드가 실행된다.

함수를 호출하고, await 키워드가 위치한 줄에서는 코드 실행을 잠시 중단하고, 프로미스가 처리(성공 혹은 실패)되길 기다린다. 프로미스가 처리되면(resolve 혹은 reject 함수 호출시), 결과값을 result 변수에 저장한다. 따라서 예제를 실행하면 1초 후에 경고창이 출력된다. await 은 promise.then 보다 좀 더 세련된 방식으로 결과값을 조회할 수 있으며, 가독성도 좋다.

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000)
  })

  let result = await promise // 프라미스가 이행될 때까지 기다림 

  alert(result) // 1초 후 "완료!" 출력
}

f()
alert("함수 아래쪽 코드 실행!")

만약 함수 f() 아래쪽에 코드가 더 있으면 "완료"가 먼저 출력될까? 아니면 함수 f() 아래쪽 코드가 먼저 출력될까?

실행을 해보면 함수 아래쪽 코드가 먼저 실행된다. 함수 f() 안의 setTimeout 은 비동기 코드이므로 메모리에 등록해놓고, 함수 f() 아래쪽 코드를 먼저 실행한다. 하지만 f() 함수 내부의 await 키워드 아래쪽 코드는 실행이 되지 않고 프로미스가 처리되길 기다린다. 그런 다음 1초 후에 resolve 함수가 호출되면,  그제서야 await 키워드 아래쪽 코드가 실행이 재개된다.

function f() {
  let promise = Promise.resolve(1)
  let result = await promise // Syntax error
}

async 키워드가 없는 일반 함수에는 await 을 사용할 수 없다. 사용시 아래와 같은 문법에러가 발생한다.

일반함수에 await 키워드를 사용한 경우

 

 

* async/await 활용하기

fetch('user.json')
  // 원격 서버가 응답하면 .then 아래 코드가 실행됩니다.
  .then(response => response.json())
  // GitHub에 요청을 보냅니다.
  .then(user => fetch(`https://api.github.com/users/${user.name}`)) // {"name": "Violet-Bora-Lee", "isAdmin": true}
  // 응답받은 내용을 json 형태로 불러옵니다.
  .then(response => response.json())
  // 3초간 아바타 이미지(githubUser.avatar_url)를 보여줍니다.
  .then(githubUser => new Promise(function(resolve, reject){
    const img = document.createElement('img')
    img.src = githubUser.avatar_url
    img.className = "promise-avatar-example"
    document.body.append(img)

    setTimeout(() => {
      img.remove()
      resolve(githubUser)
    }, 3000) 
  }))
  // 3초 후 동작함
  .then(githubUser => alert(`${githubUser.name}의 이미지를 성공적으로 출력하였습니다.`))

앞선 수업에서 프로미스 체이닝으로 작성한 예제를 아래와 같이 async/await 을 사용하여 다시 작성해보자!

async function showAvatar() {

  // JSON 읽기
  let response = await fetch('user.json')
  let user = await response.json()

  // github 사용자 정보 읽기
  let githubResponse = await fetch(`https://api.github.com/users/${user.name}`)
  let githubUser = await githubResponse.json()

  // 아바타 보여주기
  let img = document.createElement('img')
  img.src = githubUser.avatar_url;
  img.className = "promise-avatar-example"
  document.body.append(img)

  // 3초 대기
  await new Promise((resolve, reject) => setTimeout(resolve, 3000))

  img.remove()

  return githubUser
}

showAvatar()
.then(githubUser => alert(`${githubUser.name}의 이미지를 성공적으로 출력하였습니다.`))

먼저 then 호출을 모두 await 으로 변경해야 한다. function 앞에 async 를 붙여 await 을 사용할 수 있도록 한다. 물론 showAvatar 함수 외부에서는 await 키워드를 사용할 수 없기 때문에 then 메서드를 어쩔수 없이 사용한다. githubUser 를 반환하면, promise 에서 resolve 함수를 호출한 것과 동일하다. 이제 코드가 훨씬더 깔끔해지고, 읽기도 쉬워졌다. 

 

* await 사용시 주의할 점

// 최상위 레벨 코드에선 문법 에러가 발생함
let response = await fetch('user.json')
let user = await response.json()

await 은 최상위 레벨 코드에서는 사용할 수 없다. 언제나 async 가 붙은 함수 내부에서만 사용이 가능하다. 

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

하지만 즉시실행함수를 이용하여 await 이 포함된 코드를 한번 감싸주면 최상위 레벨에서도 동작한다. 

 

* await 와 thenable 객체 함께 사용하기

promise.then 처럼 await 에도 thenable 객체(then 메서드가 있는 호출가능한 객체)를 사용할 수 있다. thenable 객체는 서드파티 객체가 프로미스는 아니지만 프로미스와 호환 가능한 객체를 제공할 수 있다는 점에서 생긴 기능이다. 서드파티에서 받은 객체가 then 메서드를 포함하면 해당 객체를 await 과 함께 사용할 수 있다. 

class Thenable {
  constructor(num) {
    this.num = num
  }
  then(resolve, reject) {
    alert(resolve)
    // 1000밀리초 후에 이행됨(result는 this.num*2)
    setTimeout(() => resolve(this.num * 2), 1000) 
  }
}

async function f() {
  // 1초 후, 변수 result는 2가 됨
  let result = await new Thenable(1)
  alert(result)
}

f()

await 은 then 메소드를 포함하면서 프로미스가 아닌 객체(thenable)를 받으면, then 메서드를 호출한다. 다시말해, 프로미스 객체를 생성할때 new Promise 의 executor 를 실행하는 것과 동일하다. 그리고 나서 await 은 resolve 혹은 reject 이 호출되길 기다렸다가, 호출이 되면 결과값을 result 변수에 담고, await 다음줄의 실행을 재개한다. 

 

* async 클래스 메서드

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

new Waiter()
  .wait()
  .then(alert) // 1

메소드 이름 앞에 async 를 붙이면 해당 메서드는 프로미스를 반환한다. 즉, 함수와 동일하게 동작한다. 그래서 메서드 호출 이후에 then 을 연결해서 체이닝할 수 있다. 

 

* async/await 에러처리 

프로미스가 정상적으로 이행되면(resolve 함수 호출시) await promise 는 프로미스 객체의 result 에 저장된 값을 반환한다. 반면에 프로미스가 실패하면(reject 함수 호출시) await promise 는 프로미스 객체의 result 에 에러객체를 반환한다. 

async function f() {
  const result = await Promise.reject(new Error("에러 발생!"))
  console.log(result)
}
f()

위 코드는 아래 코드와 동일하다. 

async function f() {
  throw new Error("에러 발생!")
}
f()

await 이 던진 에러는 throw 가 던진 에러를 잡을때처럼 try ~ catch 문을 사용하여 처리할 수 있다. 

async function f() {
  try {
    let response = await fetch('http://유효하지-않은-주소')
  } catch(err) {
    alert(err) // TypeError: failed to fetch
  }
}

f()

await 이 포함된 코드를 try ~ catch 문으로 감싸주면 catch 블럭에서 아래와 같이 에러를 처리한다. 

try ~ catch 문으로 async/await 에러처리

async function f() {

  try {
    let response = await fetch('http://유효하지-않은-주소')
    let user = await response.json()
  } catch(err) {
    // fetch와 response.json에서 발행한 에러 모두를 여기서 잡습니다.
    alert(err)
  }
}

f()

해당 코드와 같이 여러줄의 코드를 try 문으로 감싸는 것도 가능하다. 

async function f() {
  let response = await fetch('http://유효하지-않은-주소')
}

// f()는 거부 상태의 프라미스가 됩니다.
f().catch(alert) // TypeError: failed to fetch

만약에 try ~ catch 없이 에러가 발생하면 함수 f() 를 호출해서 만든 프로미스는 거부상태가 된다. 다시말해, reject 함수를 호출한 것과 동일하게 동작한다. 그러므로 함수 f() 에 catch 메서드를 연결해주면 해당 에러를 처리할 수 있다.  

 

* async/await 와 promise.then/catch 비교

async/await 을 사용하면 await 이 대기를 처리하기 때문에 then 이 거의 필요하지 않다. 아울러 catch 메서드 대신 try ~ catch 문으로 에러를 처리할 수 있다. 그래서 async/await 을 사용하는 것이 좀더 편리하다. 그럼에도 불구하고, 문법적인 제약 때문에 async 함수 외부에서는 await 을 사용하지 못하기 때문에, 이러한 경우에는 then/catch 메서드를 추가하여 최종결과나 에러를 처리해야 한다. 

728x90

'프론트엔드 > Javascript' 카테고리의 다른 글

함수와 this  (0) 2024.04.20
프로토타입 상속 참고자료  (0) 2024.04.20
비동기 - 프로미스 체이닝  (0) 2024.03.15
비동기 - 프로미스 (Promise)  (0) 2024.03.14
비동기 - 콜백  (0) 2024.03.13