책정리/Deep Dive Javascript

async, await는 Promise는 제너레이터의 조합이다

뽀글보리 2023. 12. 13. 07:39
반응형

콜백 지옥

비동기 프로그래밍을 사용하다 보면, 비동기 호출에 따른 후속 처리를 진행하면서 콜백 함수를 중첩해서 사용하게 되는 경우가 있다.

get('/step1', a => {
  get(`/step2/${a}`, b => {
    get(`/step3/${b}`, c => {
      get(`/step4/${c}`, d => {
       // do something 
      }
    }
  }
}

 

이렇게 중첩 함수를 호출하면서 복잡도가 높아지는 현상을 콜백 지옥이라고 부른다. 다음과 같은 코드는 가독성을 나쁘게 하며, 개발 경험 면에서도 부정적인 영향을 끼친다. 또한 비동기 프로그래밍을 사용하면 에러를 캐치하지 못한다는 치명적인 단점이 있다.

Promise의 등장

 

Promise는 ES6에서 도입된 ECMAScript 사양의 표준 빌트인 객체이다. Promise는 기본적으로 pending 상태인 데, 비동기 처리가 성공하면 resolve 함수를 호출하여 프로미스를 fullfilled 상태로 변경한다. 비동기 처리가 실패하면 reject 함수를 호출하여 프로미스를 rejected 상태로 변경한다.

promiseGet('step1'
  .then((a) => promiseGet(`/step2/${a}`)
  .then((b) => console.log(b))
  .catch(err => console.log(err))

 

promise를 사용하여 비동기 프로그래밍을 구현하면, 비동기 처리 결과에 따라서 성공일 경우 then, 에러발생일 경우 catch 메서드를 선택적으로 호출 하기 때문에, 콜백 지옥 문제를 해결할 수 있다. 다만, 프로미스도 콜백 패턴을 사용하고 있기 때문에 어느 정도 콜백 함수를 사용할 수 밖에 없게 되는 데, 이러한 문제는 ES8에서 도입된 async/await를 통해 해결할 수 있다.

제너레이터 문법

async/await에 대해 살펴보기 전에 제너레이터 문법에 대해 알아보자. 제너레이터는 ES6에서 도입된 문법으로, 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재개할 수 있는 함수이다.

제너레이터 함수는 다음과 같은 특별한 특징을 가지고 있다.

  1. 제너레이터 함수는 함수 호출자에게 함수 실행의 제어권을 양도할 수 있다.
  2. 제너레이터 함수는 함수 호출자와 함수의 상태를 주고받을 수 있다.
  3. 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
  4. 제너레이터 객체는 이터러블이면서 동시에 이터레이터이다.

제너레이터 함수를 사용하면 일반 함수와는 다르게 호출자와 제너레이터 함수가 서로 문맥/상태를 주고받으며 함수 실행을 일시 중지/재게 할 수 있다.

function* getFunc() {
  yield 1;
  yield 2;
  yield 3;
}

function callerFunc() {
  const generator = getFunc();

  console.log(generator.next()); // {value: 1, done: false}
  console.log(generator.next()); // {value: 2, done: false}
  console.log(generator.next()); // {value: 3, done: true}
}

 

위 코드를 보면, 제너레이터의 메서드를 호출할 때마다 제너레이터 함수의 yield 표현식까지의 문맥만 실행하고, 다시 호출차로 함수 제어 권한이 넘어가는 것을 알 수 있다. 더 상세히 코드를 분석해보면 다음과 같다.

  1. 제너레이터 함수를 정의할 때에는 function 키워드 뒤에 *을 붙인다.
  2. yield 키워드를 사용하면 함수의 실행을 일시 중지 시켜 yield 키워드에 있는 표현식의 결과를 제너레이터 함수 호출자에게 반환한다.
  3. getFunc()을 사용하여 제너레이터 함수를 호출하면 제너레이터 객체가 반환된다.
  4. 반환된 제너레이터 객체는 이터러블이면서 이터레이터이기 때문에, next 메서드를 갖는다.
  5. generator.next()를 호출하면 yield 표현식까지 실행되고 일시 중지 된다.
  6. next 메서드는 value, done 값을 가지는 이터레이터 리절트 객체를 반환한다.

async/await의 등장

async/await는 Promise와 제너레이터의 조합으로 구현된 일종의 문법적 설탕이다. async, await를 사용하면 프로미스의 then, catch, finally와 같은 후속 처리 메서드를 마치 동기 처리하듯 구현할 수 있다.

try {
  const response = await fetch('/user1');
  const user1 = await response.json();

  if (user1.name) {
    //do something;
  }
} catch (err) {
  //do something;
}

 

await 키워드는 프로미스의 상태를 확인하여, 프로미스 처리가 완료되면 프로미스가 resolve한 처리 결과를 response 변수에 할당한다. 에러 처리는 try-catch문을 사용하여 에러 처리할 수 있다. 비동기 프로그래밍과 다르게 호출자의 실행 컨텍스트가 유지되기 때문에 fetch에서 발생하는 에러가 호출자로 전파되기 때문이다.

반응형