JS

Async, Await (feat. Generator)

Hyeon_E 2024. 6. 13. 15:40

Async, Await

async/await는 ES7(2017)부터 추가된 것으로, 이를 사용하면 비동기 코드를 마치 동기 코드처럼 보이게 작성할 수 있음

명세의 차이가 있을 뿐 async 함수를 호출하면 명시적으로 Promise 객체를 생성하여 리턴하지 않아도 Promise 객체를 리턴

 

async function greet(){
    return 'hi!'
}

const hi = greet()
console.log(hi)  // Promise {<fulfilled>: 'hi!'}

 

▶ Async, Await 기본 구조

async 키워드는 function 앞에 붙임(function 앞에 async를 붙이지 않으면 syntax Error 발생)
await 키워드는 async 키워드가 붙어있는 함수 내부에서만 사용할 수 있음

 

기존 비동기 처리 방식

const printName = () => {
  const getData = fetchData('abc.com/datas/3')
  .then(data => { 
    if (data.id === 3) {
      console.log(data.id); 
    }
  })
}


async/await 방식

const printName = async () => {
  const data = await fetchData('abc.com/datas/3');
  if (data.id === 3) {
    console.log(data.id);
  } 
}

 

▶ Async, Await 작동

await 키워드를 이용하면 awaited 하고 있는 값을 promise가 resolve할 때 까지 기다려서 비동기 함수를 일시정지 할 수 있음(promise의 resolve된 함수를 기다리기 위해서 then() 콜백을 한 것처럼 변수에 promise 값을 할당 할 수 있)

 

const one = () => new Promise((res, rej) => {
  setTimeout(() => {
    res('one!');
    console.log('setTimeOut')
  }, 10000);
})

async function myFunc() {
  console.log('In function');
  const res = await one();
  console.log(res);
  console.log('In function2');
}

console.log('Before function');
myFunc();
console.log('After function');

// 'Before function'
// 'In function'
// 'After After function'
// 10초후~!!
// 'setTimeOut'
// 'one!'
// 'In function2'

 

  1. console.log('Before function'); 실행
  2. myFunc() 함수가 호출
    1. console.log('In function'); 실행
    2. const res = await one();에서 one() 함수가 호출되어 Promise 반환
    3. setTimeout을 실행
    4. setTimeout의 정보(타이머, 콜백함수)가 Web API로 보내져 10초후 실행되도록 대기
    5. one 함수가 반환되고 await 키워드로 인해 비동기 함수가 완료될 때까지 대기
    6. my func 함수 실행이 일시 중지
  3. console.log('After function'); 실행
  4. Web API에서 타이머 완료 후 setTimeout 콜백이 매크로태스크(Macro Task) 큐에 추가
  5. 이벤트 루프는 콜스택이 비어있음을 확인하고 대기 큐의 첫번째 콜백을 실행
  6. res('one!') 실행. Promise가 완료되어 await one()이 해결됨
  7. console.log('setTimeOut') 실행
  8. const res = await one(); 완료되며 res는 one! 값을 가지게 됨
  9. console.log(res); 실행
  10. console.log('In function2'); 실행

 

▶ Async, Await 동작원리

제너레이터 함수는 next 메서드와 yield 표현식을 사용하여 함수 호출자와 함수의 상태를 주고 받을 수 있음

이러한 특징을 활용하면 Promise를 사용한 비동기 처리를 동기처럼 구현할 수 있음

즉, 프로미스의 후속 처리 메서드 없이 비동기 처리 결과를 반환하도록 구현할 수 있는 것

 

const async = (generatorFunc) => {
  const generator = generatorFunc();

  const onResolved = (arg) => {
    const result = generator.next(arg);

    return result.done ? result.value : result.value.then((res) => onResolved(res));
  };
  return onResolved;
};

async(function* fetchTodo() {
  const url = '요청하는 url';

  const response = yield fetch(url);
  const todo = yield response.json();
  console.log(todo);
})();

 

  1. async 함수가 호출되면 인수로 전달받은 제네레이터 함수 fetchTodo 호출하여 제네레이터 객체 생성
    • → onResolved 함수는 상위 스코프의 generator 변수를 기억하는 클로저
    • → async 함수가 반환한 onResolved 함수를 즉시 호출하여 const generator 에서 생성한 제네레이터 객체의 next 메서드 처음 호출
    • → onResolved 함수 반환
  2. next 메서드 처음 호출되면 fetchTodo 의 첫 번째 yield 문까지 실행
    • → 이터레이터 리절트 객체의 value 프로퍼티 값, 즉 첫 번째 yield 된 fetch(url) 함수가 반환한 프로미스가 resolve 한 Response 객체를 onResolved 함수에 인수로 전달하면서 재귀 호출
    • → 이때 next 메서드가 반환한 이터레이터 리절트 객체의 done 프로퍼티 값이 false라면
  3. onResolved 함수에 인수로 전달된 Response 객체를 next 메서드에 인수로 전달하면서 next 메서드를 두 번째로 호출
    • → 제네레이터 함수 fetchTodo 의 두 번째 yield 문까지 실행
    • → 이때 next 메서드에 인수로 전달한 Response 객체는 제네레이터 함수 fetchTodo의 response 변수에 할당
  4. next 메서드가 반환한 이터레이터 리절트 객체의 done 프로퍼티 값이 false라면 이터레이터 리절트 객체의 value 프로퍼티 값인 response.json 메서드가 반환한 프로미스가 resolve 한 todo 객체를 onResolved 함수에 인수로 전달하며 재귀 호출
  5. onResolved 함수에 인수로 전달된 todo 객체를 next 메서드에 인수로 전달하면서 next 메서드를 세 번째로 호출
    • → 이때 next 메서드에 인수로 전달한 todo 객체는 제네레이터 함수 fetchTodo의 todo 변수에 할당되고 제네레이터 함수 fetchTdoo 가 끝까지 실행
  6. next 메서드가 반환한 이터레이터 리절트 객체의 done 프로퍼티 값이 true라면
    • → 이터레이터 리절트 객체의 value 프로퍼티 값, 즉 제네레이터 함수 fetchTodo의 반환값인 undefined를 그대로 반환하고 처리 종료

결론적으로 yield 표현식을 할당받는 변수에 값이 할당되는 시점yield 표현식이 끝나는 시점이라고 정리할 수 있음

 

async function fetchTodo() {
  const url = '요청 url';

  const response = await fetch(url);
  const todo = await response.json();
  console.log(todo);
}

 

yield 표현식이 await 키워드로 바뀌었음

await 키워드 뒤에 있는 일을 끝날 때 까지 기다린 뒤 변수에 할당하는 것

Babel을 통해 위 예시 코드를 ES5 버전으로 변환해보면 generator를 활용한 코드와 비슷한 형식을 볼 수 있음

 

더 정확히 말하면 await 키워드는 Promise가 Settled(처리된) 상태가 될 때까지 대기하다가 Settled 상태가 되면 Promise가 resolve한 처리 결과를 반환함 따라서 반드시 Promise 객체 앞에서 사용해야함

 

- Babel: 입력과 출력이 모두 자바스크립트 코드인 컴파일러

최신 버전의 자바스크립트가 실행되지 않는 구버전의 브라우저에서도 정상적으로 실행되도록 변환해줌

 

 

Reference 

async와 await

자바스크립트 비동기 처리: Microtasks and (Macro)tasks

async/await의 동작원리를 살펴보자!