Promises, async / await

김상연·2022년 12월 12일

JavaScript

목록 보기
15/19

1. 엄밀히 말하면, new Promise().then()은 Promise 객체를 반환하지 않는다.

promise chaining이라고, promise객체에 .then() 메소드를 계속 갖다 쓸 수 있다. 따로 공부하지 않아도 .then() 이놈이 똑같이 promise객체를 리턴한다고 예상할 수 있다. 하지만 엄밀히 따지면 thenable 객체를 리턴한다.

바꿔말하면 Promise클래스를 상속받지 않은 객체도 똑같이 .then() 메소드를 호출할 수 있다는 것이다.

예시)
class Thenable {
  constructor(num) {
    this.num = num;
  }
  
  then(resolve, reject) {
    ...	 // 	whatever we want 
    setTimeout(() => resolve(this.num * 2), 10); //	resolve 또는 reject를 리턴 해 준다.
  }
}

new Promise(resolve => resolve(1))
  .then(result => {
    return new Thenable(result);
  })
  .then(console.log); // shows 2 after 10ms

2. use case : Promise.all(iterable)

const urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://api.github.com/users/jeresig'
];

const requests = urls.map(url => fetch(url));

const responses = Promise.all(requests);

각 promise 객체들이 완료되는 순서와 상관 없이 기존 순서를 유지한다는 점에 유의해라.

매개변수는 iterable 여부만 확인한다. 즉 promise객체가 아니라도 상관 없다는 것이다. 그런 경우엔 그냥 받은 그대로 return한다.

하나라도 reject되는 순간 Promise.all 객체 전체가 해당 reject를 근거로 rejected 된다.

그러나 코드가 멈추는 것은 아니다. 모두 실행은 되지만 무시 할 뿐이다. 하나라도 실패 시 멈춰야 할 필요가 있다면 따로 처리 해 주저야 한다. Promise.race 또한 마찬가지다. 최적화를 위해서는 신경 써 보자.

3. 여타 Promise api들 소개..

  • Promise.race는 resolve던 reject던 상관 없이 빨리 완료 되는 놈을 반환한다.

  • Promise.any는 reject되는 놈은 건너 뛰고 젤 빨리 resolve되는 놈을 반환한다. 모두 실패라면 모든 에러 정보를 담아 반환한다. 이때의 built-in 에러는 AggregateError 클래스다.

  • Promise.allSettled(iterable)는 각 요청이 resolve 되든 reject 되든 다 담아서 반환 한다. 이놈의 pollyfill 코드를 참고삼아 보자.

if (!Promise.allSettled) {
  const rejectHandler = reason => ({ status: 'rejected', reason });

  const resolveHandler = value => ({ status: 'fulfilled', value });

  Promise.allSettled = function (promises) {
    const convertedPromises = promises.map(p => Promise.resolve(p).then(resolveHandler, rejectHandler));
    return Promise.all(convertedPromises);
  };
}

매개변수로 promise 객체가 오지 않는 경우를 대비하여 Promise static method : .resolve()로 한번 커버 해 준 모습이다.


4. microtask queue

경험 해 봤겠지만, promise 객체의 .then() 메소드는 실행 순서가 뒤로 밀린다. promise객체를 리턴하는 함수가 아무리 빨리 완료 됐다 해도 상관없다. 비동기 작업이 아니었대도, promise 객체를 리턴한다면 무조건 뒤로 밀린다.

이것은 당연히 콜스택이 따로 존재한다는 뜻이다. 정식 명칭은 PromiseJobs 이지만, microtask queue라고 흔히 불린다. queue인 만큼 first in, first out을 지킨다.

들어가는 순서도 중요하다. .then() 또는 .catch()가 연결 되어있다 해도 한번에 들어가지는 않는다. 핸들러 실행을 마쳤는데 이어지는 핸들러가 또 있다면 그때 그놈을 큐에 넣는다. 아래 예시를 보자.

Promise.resolve()
	.then(() => {
		console.log('A');
	})
	.then(() => console.log('B'))

const scriptJob = () => {
	console.log('C');
	return Promise.resolve()
}

scriptJob().then(() => console.log('D'))
//	'C'
//	'A'
//	'D'
//	'B'

한편, 우리가 놓친 rejected promise들은 어떻게 될까? 예를들어 promise chain 끝에 .catch() 핸들러를 깜빡했는데 마침 에러가 발생한다면 말이다.

정답은 해당 에러를 보여주고(rejected promise는 에러 객체를 리턴한다) 프로세스가 종료된다. 이것은 우리가 작성하지 않아도 기본적으로 스크립트가 에러 핸드링이 되어있다는 소리인데, 이 에러 핸들러는 환경에 따라 다음과 같다.

  • window.addEventListener('unhandledrejection', callback(unhandled));
  • process.setUncaughtExceptionCaptureCallback(callback(unhandled));

매개변수 자리에 unhandled가 바로 microtasks queue에서 오는 것들이다. 우리가 앞서 식별하여 처리 해 주었다면 좋겠지만, 그렇지 못했다면 여기서 검거되고 프로세스는 종료된다. 이때 처리되지 않은 모든 에러에 대하여 에러 핸들러(콜백 함수)를 실행한 뒤에 프로세스가 종료된다.

(프로세스 종료는 다른곳에서 한다. 수순이 그렇단 거지. 때문에 여기에서 프로레스를 재시작 한다던지..그런건 매우 좋지 않다. 예상 밖의 프로세스 종료는 물론 마음 아프지만 그보다 더 최악의 경우를 막아주는, 멍청한 나의 최후의 보루라고 생각하자. 프로세스 관리는 프로그램 바깥에서 하는게 좋다.)

예시)

Promise.reject(new Error('mistake ONE'))
Promise.reject(new Error('mistake TWO')).catch(() => {	// 에러 핸들링 ... })
Promise.reject(new Error('mistake Three'))

process.setUncaughtExceptionCaptureCallback(ele => {
	console.log(ele.name, ': ', ele.message)
})

//	Error :  mistake ONE
//	Error :  mistake Three

5. Async/await

async/awaitpromise chain을 좀더 쓰기 쉽게 바꾼 놈이다. async 키워드가 달린 function이 무엇을 리턴하던 promise 객체로 포장해서 리턴하도록 보장 해 준다. promisifing wrapper정도로 볼 수 있곘다.

이때 await 키워드는 당연히 promise를 리턴하는 함수에 붙여준다고 볼 수 있는데, 정확히 말하면 thenable한 객체면 충분하다. 즉 then(resolve, reject) <Promise> 메소드만 가지고 있으면 된다는거다.

대단한 쓸모가 있지도 않고 대체하여 쓰기도 쉬우나 class field로 쓰면 의도가 잘 드러나는 코드 스타일이 될 것 같다. 예를 들면 새로운 유저를 생성하는 클래스 객체가 생성 전에 중복된 아이디를 체크한다던지..

class Thenable {
	constructor(num) {
		this.num = num;
	}
	then(resolve, reject) {
		setTimeout(() => reject(this.num * 2), 3000); 
	}
}

async function f() {
	let result = await new Thenable(10);
	console.log(result);
}

f();	//	3초 후 20 출력

근데 await 키워드와 thenable 또는 then 메소드를 상호 연상 할 수 있다면 몰라도 그게 아니라면 직관적으로 이해하기는 어려울 수도 있을 것 같다. 본적은 없지만 상상의 나래를 펼쳐 보자면 클래스 생성 전에 체크리스트를 돌려보는 섹시한 관습이 분명 있을 것 같기는 하다.

6. promise chain에서 핸들러들은 한 덩어리로 microtask queue로 넘어간다.

setTimeout의 콜백 함수 또는 promise chain에 줄줄이 쏘세시 핸들러들(then, catch, finally)은 즉시 실행되지 않고(실행 순서를 보장하지 않고) PromiseJobs( aka. microtesk queue )에 들어갔다가 script 콜스택을 마치면 실행된다. 근데 나는 promise chain 같은 경우 새로운 핸들러를 만날 때 마다 microtest queue로 보내버리는 줄 알았다. 하지만 이것들은 한 덩어리로 모두 함께 간다. 심지어 unhandled reject를 다루는 핸들러까지 한 덩어리가 되어 같이간다.(unhandled reject 핸들러를 임의로 설정 해 줬을 경우에)

예시를 통해 확인 해 보자.

const oneF = () => {
	console.log('oneF is playing')

	return new Promise((res, rej) => {
		res('data');
	})
}

(
	async () => {

		process.setUncaughtExceptionCaptureCallback(err => console.log(err.name, err.message));

		oneF()
			.then(res => console.log('promise resolved: ', res), rej => {})
			.then(() => {console.log('promise chain: middle')})
			.then(() => {console.log('promise chain: middle')})
			.then(() => {console.log('promise chain: middle')})
			.then(() => {console.log('promise chain: middle')})
			.then(() => {console.log('promise chain: middle')})
			.finally(() => {console.log('promise FINISH')})

		setTimeout(() => console.log('setTimeout callback playing'), 0)

		console.log('script is playing')

	}
)()

//	oneF is playing
//	script is playing
//	promise resolved:  data
//	promise chain: middle
//	promise chain: middle
//	promise chain: middle
//	promise chain: middle
//	promise chain: middle
//	promise FINISH
//	setTimeout callback playing
profile
리눅스와 컴퓨터 프로그래밍

0개의 댓글