Ecma와 딥다이브로 살펴보는 Promise

·2022년 7월 12일
0

promise란

es6에서 비동기 처리를 위한 패턴으로 promise 를 도입했다,
왜 생겼을까? 그 이유는 전통적인 콜백 패턴의 단점을 보완하기 위해서이다.

기존 콜백 패턴

function get(url) {
  // XMLHttpRequest 객체 생성
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.send();

  // 서버 응답 시 호출될 이벤트 핸들러
  xhr.onload = () => {
    if (xhr.status === 200) {
      // 정상 응답
      console.log(xhr.response);
      return xhr.response; // ①
    } else {
      console.log('Error: ' + xhr.status);
    }
  };

}
const res = get('http://jsonplaceholder.typicode.com/posts/1');
console.log(res); // ② undefined

위 와 같은 코드를 살펴보자. 해당 코드를 실행 했을때 log에 res값이 찍힐까?
그렇지 않다, 왜냐하면 onload가 비동기 적으로 실행이 되기 때문이다.

어떻게 실행되는지 순서를 살펴보자.

  1. get 함수가 실행되고 new XMLHttpRequest() 후 xhr로 open,send함수실행
  2. xhr.onload로 이벤트 핸들러를 바인딩 하고 종료하게 된다.
  3. get함수에는 return값이 없어서 undefined값이 반환되고 그 값이 출력된다.

만약 return 'test' 같은 리턴값을 주면 그값이 로그에 찍힌다.

그렇다면 어떻게 xhr.response를 받을수 있을까? 콜백 함수를 넘기면 된다.

function get(url,sucess,fail) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.send();

  xhr.onload = () => {
    if (xhr.status === 200) {
      // 정상 응답
      sucess(xhr.response)
      return xhr.response; 
    } else {
	    fail(xhr.status)
      console.log('Error: ' + xhr.status);
    }
  };

  return 'test'
}

// 비동기 함수 내의 readystatechange 이벤트 핸들러에서 처리 결과를 반환(①)하면 순서가 보장되지 않는다.
const res = get('http://jsonplaceholder.typicode.com/posts/1',console.log,console.log);

이렇게 하면 이제 서버로 부터 얻어온 값을 받을수 있지만, 만약 api호출로 받아온 값을 가지고 또 호출을 하고 그 값을 가지고 또 호출을 해야한다면?

get(
  url,
  () => {
    get(
      url,
      () => {
        get(url, () => {});
      },
      func
    );
  },
  func
);

그럼 콜백 지옥이다.

그리고 에러처리가 너무 힘들게 된다.

try{
	setTimeout(()=>{throw new Error("err");},1000)
	} catch(e){
		console.log('캐치',e)
	}

왜냐하면 try-catch문으로 비동기 함수의 오류를 캣치 할수 없기 때문이다

위 코드를 보면 try문에서 setTimeout 함수가 실행되고 나면 타이머를 설정하고 setTimeout실행은 종료된다, 콜백이 실행되기전 종료가 되기때문에,
catch에서 당연히 오류가 잡히지가 않는다.

이런 문제들로 promise가 나오게 되었다

다시 Promise로

프로미스의 생성

프로미스는 생성자 함수로서 promise 객체를 생성 할 수 있다.
Promise 생성자 함수를 호출 할 때에는 비동기 함수 처리를 수행할 콜백함수(excutor)를 인자값으로 넘겨줘야 하고 해당 함수는 resolve,reject함수를 인자로 받아야한다,

const promise = new Promise((resolve, reject) => {
  // 비동기 작업을 수행한다.
  if (/* 비동기 작업 수행 성공 */) {
    resolve('result');
  }
  else { /* 비동기 작업 수행 실패 */
    reject('failure reason');
  }
});
});

해당 형태로 작성을 하게 된다.

원하는 비동기 로직을 콜백 함수내에서 적고, 만약 성공하면 resolove함수에다 원하는 값을 넘기게되면 동작이 끝났을때 해당 값이 반환된다.

const promise = new Promise((resolve, reject) => {
  // 비동기 작업을 수행한다.
  setTimeout(()=>{resolve("test")},1000)
});

![[스크린샷 2022-07-11 오후 5.13.27.png]]
위와 같은 형태가 된다.
그리고 만약 해당 비동기로직이 실패 했을경우 reject에 다 담아서 보내면 된다.

위 사진을 보면 promise 객체에는 여러가지의 내부 슬롯이 있는데
[[PromiseState]] 는 pending : 현제 비동기 로직이 진행중
fulfilled: 비동기 처리가 성공
rejected: 비동기 처리가 실패
의 상태이고 resole,reject에 넘긴 인지가3
[[PromiseResult]] 에 담겨 있게 된다.

ECMA에서는?

추가적으로 가장 확실한 문서인 EMCA에서는 promise를 어떻게 정의하고 실행되는지 생성자 함수부분을 살펴보자 emca-promise 생성자:

오역이 있을수 있습니다 주의 자세한건 원문을 보세요
원문과 함께 스스로 이해하기 쉽게 정확하지 않은 의역도 포함을 했습니다.

(해당 번역에서 쓰는 'b()를 실행헤서 a에할당' 은 메모리할당을 의미 하는것이 아닌
'b에서 수행한 해서 리턴한 값을 a 라고 말할께' 라는 뜻입니다
)

When the Promise function is called with argument executor, the following steps are taken:

Promise 함수가 생성자로 excutor 인자를 받고 실행이 되면 아래의 순서대로 실행이 된다. :

  1. If NewTarget is undefined, throw a TypeError exception.

NewTarget 이 undefined 이면 타입에러를 뱉는다

2. If IsCallable(executor) is false, throw a TypeError exception.

만약  IsCallable(executor) 실행후 false이면( IsCallalbe은 아마 해당 객체가 [[Call]] 를 가지고 있는가 즉 실행가능 객체인가 를 확인하는 내부 추상 함수이다), 타입에러를 뱉는다

3. Let promise be ? OrdinaryCreateFromConstructor(NewTarget, "%Promise.prototype%", « [[PromiseState]], [[PromiseResult]], [[PromiseFulfillReactions]], [[PromiseRejectReactions]], [[PromiseIsHandled]] »)

=>  OrdinaryCreateFromConstructor 추상연산( 해당 추상 연산은 2,3번째 인자로 intrinsicDefaultProto, internalSlotsList 를 받게 되는데,
여기서 예상되는 동작은
1번째로 받은 newTarget에 [[ProtoType]] 이 없으면, intrinsicDefaultProto 로 받은 "%Promise.prototype%" 가 [[ProtoType]] 에 할당되는 값이 된다,

그리고 이후 internalSlotsList 들을 가지고 객체를 생성하고 아까 위해서 계산한 [[ProtoType]] 에 할당할 값 여기서는 "%Promise.prototype%" 를 생성한 객체의 [[ProtoType]] 에 할당게 된다.

) 를 실행 해서 객체를 생성해서 promise에 할당한다.

4. Set promise.`[[PromiseState]] to pending.

아까 생성한 promise의 [[PromiseState]] 에 pending값 할당

 5. Set promise. [[PromiseFulfillReactions]] to a new empty List.

7번째 까지 동일한 내용

6. Set promise.[[PromiseRejectReactions]] to a new empty List

  1. . Set promise.[[PromiseIsHandled]] to false.

8. Let resolvingFunctions be CreateResolvingFunctions(promise).

CreateResolvingFunctions(promise)를 실행해서 resolvingFunctions에 할당한다

9. Let completion be Completion(Call(executor, undefined, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)).

인자로 받은 executor 즉 콜백 함수를 아까 생성한resolvingFunctions 와 함께 호출하고 실행한 값이 올바르게 종료되는지 확인 합니다

10. If completion is an abrupt completion, then

a. Perform ? Call(resolvingFunctions.[[Reject]], undefined, « completion.[[Value]] »).

이때 만약 종료가 비정상적으로 되었다면 reject함수 호출

11. Return promise.

그리고 promise를 리턴합니다

Promise 후속 처리 메서드

프로미스의 상태가 fulfilled or rejectec가 될때 해당 결과를 가지고 처리를 해야한다 그 메서드 들이

then
catch
finally 이다.

then

then은 2개의 콜백함수를 인수로 받는다.
1번째는 fulfilled 상태 일때 호출, 2번째는 rejected 될때 실행되는 함수이다.

그리고 then은 실행수 언제나 프로미스를 반환한다.
만약 콜백함수가 promise를 반환 하지 않더라고, 암묵적으로 resolve 또는 reject해서 프로미스를 생성한뒤 반환한다.

catch

해당 메서드는 한개의 콜백 함수를 인수로 받고 rejected일떄만 실행된다.
then과 같이 언제나 promise를 반환한다.

finally

하나의 콜백 함수만 인수로 전달 받는다.
해당 프로미스의 성공과 실패에 상관없이 무조건 한번 호출된다.
then과 같이 언제나 promise를 반환한다.

Promise에서의 에러처리

then과 catch 둘다 에어처리를 수행 할수 있지만.
catch에서 하는것을 권장한다.

Promise 체이닝

위에서 Promise 후속 메서드 then,catch, finally 들은 프로미스를 반환을 하기 떄문에 연속해서 promise호출이 가능하다.

promiseGet().then((tmp)=>func(tmp)).then()..

이때 콜백 함수의 인자는 이전 함수가 반환한 프로미스가 resolve한 값이다.

프로미스의 정적 메서드

Promise.resolve/ Promise.reject

이미 존재하는 값을 래핑해서 프로미스를 생성할 때에 사용한다.

const resolve=Promise.resolve([1,2,3])
resolve.then(console.log);

Promise.all

여러개의 비동기 처리를 모두 병렬 처리 할때 사용한다.

Promise.all([ new Promise(resolve => setTimeout(() => resolve(1), 3000)), new Promise(resolve => setTimeout(() => resolve(2), 2000)),  new Promise(resolve => setTimeout(() => resolve(3), 1000))]).then(console.log) // [ 1, 2, 3 ] .catch(console.log);

해당 코드가 있을시, Promise.all에 배열로 넘긴 인자들의 비동기 처리가 끝난후 콜백 함수를 실행 시킨다.

Promise.race

Promise.all 과 동일하고 프로미스의 배열등의 이터러블을 인수로 받지만
Promise.all 과 다르게 먼저 fulfilled상태가 된 프로미스의 처리결과를 resolve한다

Promise.allSettled

전달 받은 프로미스의 배열이 전부다 fulfilled또는 rejected상태가 되었을때 처리결과를 배열로 반환한다.

마이크로 태스크큐

프로미스는 비동기이지만 프로미스의 콜백 함수는 태스크큐가 아니라 마이크로 태스크큐에 저장이 되고 마스크로 테스크큐는 태스크큐보다 우선순위가 높아서

이벤트 루프는 콜스택이 비면 먼저 마이크로 테스크큐에서 대기중인 함수를 가져오고 그후 태스크큐의 함수를 가져온다

Fetch 함수

이 함수는 XMLHttpRequest객체와 같이 http요청 전송을 하는 webApi 이지만
fetch함수는 프로미스를 지원하기 때문에 비동기 처리가 비교적 간단하고 할수있다.

  • 주의점은
    fetch 함수가 반환하는 프로미스는 404,500 애러등에도 rejected가 되지 않고
    네트워크 장애나 core에러 같이 요청이 완료 되지 못한 경우에만 프로미스를 rejected 한다

자세한건 https://developer.mozilla.org/ko/docs/Web/API/Fetch_API/Using_Fetch 여기를 참고하자

0개의 댓글