[46장] 제너레이터와 async/await ✍️

junjeong·2023년 11월 6일
0
post-thumbnail

제너레이터란?

ES6에서 도입된 제너레이터란 함수인데 특수한 함수이다. 어떤 특수한 동작을 한느냐? 바로 함수 호출자에게 함수 실행의 제어권을 양도할 수 있다는 특징을 가진 함수이다.

일반 함수라면 함수 외부에서 값을 주입받은 매개변수로 함수 내부에서 어떤 로직을 실행시킨 뒤 얻은 결과값을 반환하는 방식으로 상태를 주고받았다. 때문에 실행되고 있는 동안에는 함수 외부에서 함수 내부로 값을 전달하여 함수의 상태를 변경할 수 없었다. 하지만 제너레이터 함수는 함수 호출자와 양방향으로 함수의 상태를 주고받는다. 실행 도중 언제든지 제어권을 다시 함수 외부에게 주었다가, 즉 실행중이던 함수를 잠시 대기시켰다가 함수 호출자가 다시 실행시키라고 하면 그때서야 다시 재개시킬 수 있는 것이다.

중요한 점은 이 때 제너레이터 함수 내부의 특정한 값이 언제든지 호출자에 의해 바뀔 수 있다는 점이다. 아직 실행이 마치기 이전인데 말이다. 이것이 제너레이터 함수의 큰 특징이다.

또한 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다. 이 때의 객체는 이터러블이면서 동시에 이터레이터이다.(for of으로 각 요소 순회가능)

제너레이터 함수의 정의

제너레이터 함수는 function* 키워드로 선언한다. 그리고 하나 이상의 yield 키워드가 반드시 포함되어야 한다. 이것을 제외하면 일반 함수를 정의하는 방법과 동일하다.

애스터리스크(*)의 위치는 function 키워드와 함수 이름 사이라면 어디든지 상관없다. 하지만 function 키워드 바로뒤에 붙이는 것을 권장한다고 한다.

또한 제너레이터 함수는 화살표 함수로 정의할 수 없고 생성자 함수로 호출할 수 없다.

제너레이터 객체??

제너레이터 함수를 호출하면 일반 함수처럼 결과값을 반환하는 것이 아니라 제너레이터 객체를 생성해 반환한다고 했다. 또한 제너레이터 함수가 반환한 제너레이터 객체는 이터레이터 객체이다. next 메서드를 통해 각 요소의 value값을 순회할 수 있다.

이 제너레이터 객체는 yield 키워드와 next 메서드를 통해 실행을 일시 중지시켰다가 필요한 시점에 다시 재개시킬 수 있다. next 메서드를 호출하면 제너레이터 함수의 코드 블록을 실행한다.

단 yield 표현식을 만나면 코드를 일시대기시키고 제어권을 호출자에게 넘긴다고 했으니 실행은 끝까지 되는 것이 아니라 yield 표현식까지만 실행되는 원리이다.

이후 필요한 시점에 호출자가 또다시 next 메서드를 호출하면 일시 중지된 코드부터 실행을 재개하기 시작하여 또 다음 yield 표현식까지 실행되고 또 다시 일시 중지되는 것이다.

이러한 제너레이터의 동작 특성을 잘 활용하면 비동기 처리를 동기 처리처럼 가독성 있게 구현할 수 있다.

제너레이터를 활용한 비동기 처리

제너레이터 함수는 next 메서드와 yield 표현식을 통해 함수 호출자와 함수의 상태를 주고받을 수 있다고 했다. 이러한 특성을 활용하면 또 프로미스를 사용한 비동기 처리를 마치 동기처럼 구현할 수 있다고도 했다. 아래는 프로미스와 제너레이터 함수를 활용하여 비동기 처리를 해 주는 예시 코드이다.

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

function* asyncTask() {
  console.log("Task started");
  yield delay(2000); // 비동기 대기
  console.log("Task resumed after 2 seconds");
  yield delay(1000); // 다시 비동기 대기
  console.log("Task completed");
}

// 제너레이터 객체 생성
const taskGenerator = asyncTask();

function runGenerator() {
  const next = taskGenerator.next();
  if (next.done) {
    // 제너레이터가 완료되면 종료
    return;
  }
  next.value.then(() => {
    runGenerator(); // 다음 스텝 실행
  });
}

runGenerator();

async/await의 등장

하지만 이러한 제너레이터도 치명적인 단점이 존재하는데 바로 코드가 너무 장황하고 가독성이 나쁘다는 점이다. 그래서 ES8(ECMASCRIPT 2017)부터 나온 것이 바로 async/await이다.

async/await는 프로미스르 기반으로 동작한다. 하지만 async/await는 프로미스의 then/catch/finally 후속 처리 메서드 없이도 비동기 처리를 마치 동기처리처럼 구현할 수 있다는 것이 큰 특징이다.

바로 전 단원에서 다루었던 코드 예시를 async/await로 다시 구현하면 아래와 같다.

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

async function asyncTask() {
  console.log("Task started");
  await delay(2000); // 비동기 대기
  console.log("Task resumed after 2 seconds");
  await delay(1000); // 다시 비동기 대기
  console.log("Task completed");
}

async function runAsyncTask() {
  try {
    await asyncTask(); // 실행 및 대기
  } catch (error) {
    console.error("An error occurred:", error);
  }
}

runAsyncTask();

이터레이터의 next메서드를 사용하지 않고 바로 try/catch 문법을 사용해서 보다 가독성있고 간결하게 작성된 모습이다. 다음으로는 이처럼 편리한 async 함수의 특징과 사용법에 대해서 알아보자.

async 함수

await 키워드는 반드시 async 함수 내부에서 사용해야 한다. async 함수는 async 키워드를 사용해 정의해야 하며 언제나 프로미스를 반환한다. async 함수가 명시적으로 프로미스를 반환하지 않더라도 async 함수는 암묵적으로 반환값을 resolve하는 프로미스를 반환한다.

await 키워드

await 키워드는 프로미스가 settled 상태(비동기 처리가 수행된 상태)가 될 때까지 대기하다가 settled 상태가 되면 프로미스가 resolve한 처리 결과를 반환해준다. await 키워드는 반드시 프로미스 앞에서 사용해야 한다. 아래 코드 예시를 보자.

①의 fetch 함수가 수행된 HTTP 요청에 대한서버의 응답이 도착해서 fetch 함수가 반환한 프로미스가 settled 상태가 될 때까지 ①은 대기하게 된다. 이후 프로미스가 settled 상태가 되면 프로미스가 resolve한 처리 결과가 res 변수에 할당된다.

이처럼 await 키워드는 다음 실행을 일시 중지시켰다가 프로미스가 settled 상태가 되면 다시 재개하는 기존에 제너레이터 객체처럼 동작하는 것이다. 하지만 뭐다? 훨씬 직관적이고 간단하다.

주의사항

위에 코드 예시처럼 모든 프로미스에 await 키워드를 사용하는 것은 주의해야 한다. 대기시간이 길어질수록 함수의 실행시간도 길어지기 때문이다.

또한 다수의 비동기 처리들이 서로 연관이 없을 때도 있다. 위에 코드가 그 예시이다. 개별적으로 수행되는 비동기 처리들은 굳이 서로가 완료될 때까지 기다려 줄 필요가 없는 것이다.

아래는 Promise.all 메서드를 사용하여 여러 개의 프라미스들을 하나의 배열로 묶어 병렬처리 해주는 바람직한 코드 예시이다.

에러 처리

비동기 처리를 위한 콜백 패턴의 단점 중 가장 심각한 것은 에러를 캐치하지 못해 처리하기가 곤란하다는 점이다. 이유는 비동기 함수의 콜백 함수를 호출한 주체가 비동기 함수가 아니기 때문이라고 하는데... 이해가 100%는 안되어 일단은 걍 그런가보다 하고 넘어갔다. 이를 보완한 것이 try ...catch 문이다.

try..catch 문


위 예제의 foo 함수의 catch 문은 try 코드 블록 내의 모든 문에서 발생한 일반적인 에러처리를 모두 캐치해낸다.

async 함수 내에서 catch 문을 사용하지 않아도 async 함수는 발생한 에러를 reject하는 프로미스르 반환하기에 async 함수를 호출하고 이후에 Promise.prototype.catch후속 메서드를 사용해 에러를 캐치해낼 수도 있다. 아래는 코드 예시이다.

profile
Whether you're doing well or not, just keep going👨🏻‍💻🔥

0개의 댓글