async와 await, 그리고 마이크로테스트 큐

devAnderson·2021년 12월 30일
0

TIL

목록 보기
11/106

Round 0

우선 기본 베이스로 정리해두고 갈 내용은 이렇다

  • async : 함수 내에서 무엇이 리턴되든, 해당 내용을 promise 객체로 변환시켜 리턴한다.
  • await : 함수 내에서 해당 코드의 마이크로테스크 큐가 완료될때까지 모든 함수블록 내의 코드는 스탑된다.
  • try...catch : try 블록 안에 있는 내용을 실행한다. 만약 그 내용 가운데 실패가 되어 Error 객체를 throw 할 시, 이것을 catch블록으로 넘기면서 인자로 그 에러객체를 전달한다.

Round 1

간단하게 만들어본 해당 케이스로 4가지의 경우의 수를 확인해보자

async function test() {
  try {
    let result = "unchanged";

    let test = new Promise((resolve, reject) => {
      return resolve("changed");
    }).then((res) => (result = res));

    console.log(test, result);

    return result;
  } catch (e) {
    console.log("ERRER!");
  }
}

console.log(test());

자바스크립트 엔진은 test 함수를 먼저 함수객체화하고, 스코프 체인에 따라 console 객체를 글로벌에서 찾은 후, 거기 안에서 프로토타입 체인에 따라 log 메소드를 확인하고 호출을 할 것이다. (로그 메서드의 콜스텍 추가)

그 후 인자로 test를 호출하는 작업이 존재하므로, test의 실행컨텍스트를 만들기 시작한다.

test 함수스코프 안에서 try 구문을 확인한 엔진은 이제 그 내용대로 힙에 저장할 실행 컨텍스트를 만든다.
스크립트의 상단부터 읽어나가면서 식별자를 확인하는 과정을 거치고, execution phase에서 할당과 함수실행등을 진행할 것이다.
이때 중간에 있는 promise를 한번 더 보도록 하자.

let test = new Promise((resolve, reject) => {
  return resolve("changed");
}).then((res) => (result = res));

execution phase에서 new 문을 통해 그 뒤에 따라오는 함수를 생성자라고 인식하고 객체를 만들어낸다.
그런데 해당 생성자는 Promise 객체이므로(prototype에 Function.prototype이 존재하는) 내부적으로 존재하는 [[promiseState]] 과 같은 변경을 일으키는 작업, 즉 Promise 생성자에 인자로 들어오는 함수의 콜백함수(resolve,reject를 담은) 실행작업을 비동기로 처리해야한다고 이미 인지하고 있는 상태다
따라서, 인자로 들어온 함수의 실행 컨텍스트는 return문에서 undefined를 리턴하면서 콜스택에서 빠져나가고, resolve 메서드는 callback queue에 대기상태로 저장된다.

단, then,catch,finally와 같은 메서드들의 콜백들은 따로 마이크로테스크큐라고 하는 자료구조로 들어가게 되며, 체이닝이 시작하게 되는 프로미스가 이행이 완료되는 이후에 차례대로 앞에서 정의된 큐가 끝날 때마다 실행되게 된다.

비동기는 동기적인 부분이 처리가 끝나 콜스텍이 비워지기 전까지는 실행되지 않으므로 대기상태가 된다

console.log(test, result);

return result;

그 후, 다시 스코프 체인에 따라 콘솔을 찾고, 로그를 호출한 후, test와 result의 내용을 로그로 찍는다.

그리고 그 후에 result를 리턴하는데, 이때 return되는 결과값은 aysnc에 의하여 프로미스 객체화하여 리턴되게 된다.

따라서 콘솔에 찍히는 값과 result는 이렇다

async function test() {
  try {
    let result = "unchanged";

    let test = new Promise((resolve, reject) => {
      return resolve("changed");
    }).then((res) => (result = res));

    console.log(test, result);

    return result;
  } catch (e) {
    console.log("ERRER!");
  }
}

console.log(test());

/// 결과 ///
Promise { <pending> } unchanged // 내부 콘솔 console.log(test, result)
Promise { 'unchanged' } //외부 콘솔 console.log(test())

예상했던대로, 해당 내부 순서에 따르면 test에 할당된 것은 pending 상태의 프로미스 객체이고, ressult는 바뀌기 전인 "unchanged" 가 할당된 상태 그대로이다.

리턴되는 값은 result이므로, 이 값은 async에 의하여 프로미스 객체화 하기 때문에 외부에서 콘솔로 찍은 결과값은 이 'unchanged'가 [[PromiseResult]] 슬롯에 담긴 형태로 리턴되게 된다.


Round 2

그렇다면 await을 사용하면 어떻게될까.

위에서 정리했듯 async 안에서의 await이 달린 문은 해당 마이크로테스크 큐까지의 처리가 모두 완료되기 전까지는 함수 내부 실행 과정이 중지된다고 알고있다.

그러면

async function test() {
  try {
    let result = "unchanged";

    // await만 추가
    let test = await new Promise((resolve, reject) => {
      return resolve("changed");
    }).then((res) => (result = res));

    console.log(test, result);

    return result;
  } catch (e) {
    console.log("ERRER!");
  }
}
console.log(test());

해당 구문은 비동기 함수 처리 앞에 await이 존재하는 상태다.

자 우리는 그렇다면 저 await 뒤에 존재하는 테스크 큐 포함 마이크로테스크 큐가 다 처리되기 전까지는 뒤에 호출이 멈춘다는 것을 이해하고 있다.

결과물은 과연 어떻게 될까

Promise { <pending> } // 외부 콘솔 (console.log(test())
changed changed // 내부콘솔 (console.log(test,result))

외부 콘솔에서 찍히는 Promise가 pending임에도, [[PromiseState]] 안에는 fullfiled인것은 아직도 의문이지만, 확실한 것은 await을 이용하면
해당 함수내부에서는 그 마이크로테스크 큐가 끝나기 전까지 코드실행이 중지되는 것을 확인할 수 있었다.


Round3

자, 그러면 이제 resolve 케이스는 확인했으니 reject 케이스도 봐야 할 것이다.

async function test() {
  try {
    let result = "unchanged";

    // reject만 추가된 상태
    let test = new Promise((resolve, reject) => {
      return reject("changed");
    }).then((res) => (result = res));

    console.log(test, result);

    return result;
  } catch (e) {
    console.log("ERRER!");
  }
}
console.log(test());

해당 내용을 살펴보면, reject는 아까처럼 콜백 큐로 넘어가고, then은 마이크로테스크 큐로 넘어갈 것이다.

Promise 생성자의 인자로 들어간 함수의 실행컨텍스트는 return으로 undefined를 리턴하므로 test에는 pending Promise 객체가,
result에는 여전히 "unchanged" 가 들어간 상태로 Promise객체에 감싸져 나오게 되므로 결과는 아래와 같다.

Promise { <pending> } unchanged //내부콘솔 (console.log(test, result);)
Promise { 'unchanged' } // 외부콘솔 (console.log(test());)

UnhandledPromiseRejectionWarning: change ....... //?

자 위에 두개는 처음에 예상했던 대로, 비동기가 처리가 되기 전 먼저 다 실행된 결과로 인해 나온 내용이다.

근데 그 밑에 저 문구는 왜 뜨고 있는 것일까.

다시금, 비동기적 처리는 함수 내의 동기적인 처리가 끝나 콜스텍이 비워진 이후에 비로소 시작된다고 하였다.

비동기 처리로 인해 발생하는 것은 reject() 이다.

단, reject가 콜스택이 비워져서 실행이 된다면 암묵적으로 내부에서 try... catch가 생성이 되면서 에러객체를 throw한다.
이 객체를 인자로 받은 catch는 그 객체의 내용을 가지고 해당 비동기 작업과 연결된 체이닝 메서드들, 즉
then,catch,fianlly들의 유무를 마이크로테스크큐에서 등록된 순으로 찾아나가기 시작하다가
catch를 발견하는 순간 그 메서드에 인자로 에러객체를 전달한다.

현재 위에서는 then 체이닝밖에 존재하지 않으므로, catch가 존재하지 않아 에러를 핸들링할 함수가 없다고 경고를 내는 것이다(UnhandledPromiseRejectionWarning)

쭉 이어나가서 확인해봤는데도 이를 처리할 catch 메소드의 호출을 발견하지못했다면 자바스크립트 엔진은 자동으로 eventListner을 통해 'unhandledrection' 을 등록하고 자발적으로 에러를 처리하게 된다. 이떄 처리되는 에러가 바로 저 상위 문구가 되는 것이다.


Round4

그렇다면 마지막으로, await을 붙이고 reject가 되면 어떻게 될까.

async function test() {
  try {
    let result = "unchanged";

    let test = await new Promise((resolve, reject) => {
      return reject("changed");
    }).then((res) => (result = res));

    console.log(test, result);

    return result;
  } catch (e) {
    console.log("ERRER!");
  }
}
console.log(test());

해당 코드의 결과는 이러하다.

Promise { <pending> }
ERRER!

해당 내용을 보면, reject로 인해 Error("ERROR!") 객체가 만들어짐과 동시에 이것이 throw가 된다.

현재 체이닝에는 then밖에 존재하지 않으므로, 마이크로태스크큐에서는 더이상 에러를 처리할 방안이 없다.

하지만 그 위의 컨텍스트를 보면 try... catch구문으로 이루어져 있으므로 이 catch에 인자로 해당 에러 객체를 전달하게 되고,
이에 따라 console.log("ERRER!"); 가 찍히게 된다.

profile
자라나라 프론트엔드 개발새싹!

0개의 댓글