TypeScript 비동기의 모든 것 - 2

haaaalin·2023년 12월 9일
post-thumbnail

앞선 글에서는 JavaScript가 어떻게 비동기를 처리하는지, 그 비동기 작업을 처리할 수 있도록 코드로 작성하는 법(callback)을 살펴봤습니다.
이제는 callback 지옥에서 벗어나게 해준 Promise를 보도록 하겠습니다.

Promise란?

비동기 코드를 작성하기 위한 JavaScript의 클래스입니다.
설명을 쉽게 하자면, 비동기 작업을 품은 클래스라고 이해하면 쉽습니다. 만약, 비동기 작업이 성공한다면 작업 결과를 품고, 만약, 비동기 작업이 실패한다면 Error 내용을 품고 있을 겁니다. 또, 비동기 작업 상태에 따라, promise 또한 상태가 바뀝니다.

Promise의 상태

Promise 객체의 상태는 비동기 작업 상태에 따라 3가지로 분류됩니다. Pending, Fulfilled, Rejected로 말이죠.

  • Pending: 비동기 작업이 진행 중, result -> undefined
  • Fulfilled: 비동기 작업 성공, result -> value
  • Rejected: 비동기 작업 실패, result -> Error

executor

const promise = new Promise(function(resolve, reject){
	// 코드 생략
});

Promise 객체에는 resolve reject(resolve와 reject)를 인수로 갖는 함수가 전달되는데, 이 함수 안에 비동기 작업 코드를 넣으면 됩니다. 이 함수를 바로 executor라고 부릅니다. executor는 새로운 Promise 객체 인스턴스가 만들어질 때 자동으로 실행되며, executor에서는 인수로 넘겨준 콜백 함수(resolve, reject) 중 하나를 반드시 호출해야 합니다.

  • resolve(value): 일이 성공적으로 끝난 후에 그 결과인 value와 함께 호출
  • reject(error): Error 발생 시 에러 객체를 나타내는 error와 함께 호출

Promise로 비동기 함수 만들기

function getData() {
	const promise = new Promise((resolve, reject) => {
		setTimeout(() => {
			const data = { name: '철수' };
			if (data) {
				console.log('네트워크 요청 성공');
				resolve(data);
			} else {
				reject(new Error('네트워크 문제!!!'));
			}
		}, 1000);
	});

	return promise;
}
  • Promise를 새로 생성하며, executor를 전달해주고 있습니다.
  • data가 있다면, resolve 함수를 호출해 비동기 작업을 성공적으로 마치고 있습니다.
  • data가 없다면, reject 함수를 호출해 비동기 작업을 error 객체를 전달하며 실패한 것을 나타내고 있습니다.

아까 promise 객체는 비동기 작업 결과를 품고 있다고 했었는데, 그렇다면 그 작업 결과를 어떻게 얻고 후처리를 진행할까요? 이를 위해서 promise는 몇 가지 함수를 제공하고 있습니다.

then(), catch(), finally()

getData()
	.then((data) => {
		const name = data.name;
		console.log(`${name}님 안녕하세요`);
	}).catch((error) => {
		console.log('에러를 처리했습니다.');
	}).finally(() => {
		console.log(`마무리 작업`);
	})

1. then()

앞서 작성했던 getData() 함수를 보면, 로직이 성공했을 때 콜백 함수 resolve(data)를 호출하며 비동기 작업 결과인 data를 넘겨주고 있습니다. 이렇게 resolve 함수로 전달된 data는 promise의 비동기 작업이 성공적으로 수행된 후, 자동으로 호출되는 then()이라는 함수를 통해 후처리를 진행할 수 있습니다.

2. catch()

catch()then()과 반대로, 비동기 작업이 실패했을 때 호출되는 함수입니다. 바로 getData() 함수에서 호출했던 콜백 함수 reject의 인자로 넘겨줬던 Error 객체를 받아 후처리를 진행할 수 있도록 해주는 함수죠.

3. finally()

finally()는 비동기 작업이 성공 유무에 상관 없이 무조건 실행되는 함수입니다. 따라서 비동기 작업이 성공 유무에 관계 없이 완료됐을 때 수행하고 싶은 로직을 넣을 수 있습니다.

Promise Chaining

Promise Chaning은 여러 개의 비동기 작업을 순차적으로 실행할 수 있도록 하는 방식입니다.
아래 코드처럼 then()을 사슬처럼 계속 연결하면서 사용하는 방식이죠. 일단 실제 코드를 더 살펴보겠습니다.

const promise = getData();
promise.then().then().then()....

아래는 Promise Chaining을 사용한 코드입니다. 첫 번째 promise 작업이 끝난 후, then()을 호출하고 있고, 또 getData() 를 호출해 promise를 return 하고 있습니다. 두 번째 then() 도 마찬가지입니다.

const promise = getData();
promise.
	then((data) => {
		console.log(data);
		return getData();
	})
	.then((data) => {
		console.log(data);
		return getData();
	})
	.then((data) => {
		console.log(data);
	})	

이렇게 then() 함수 안에서 또 다시 promise를 return 하게 되면, 또 다시 then()을 바로 연결해서 사용할 수 있기 때문에 비동기 작업을 순차적으로 실행할 수 있습니다.

위 코드의 출력 결과는 다음과 같습니다.

네트워크 요청 성공
{ name: 철수 }

네트워크 요청 성공
{ name: 철수 }

네트워크 요청 성공
{ name: 철수 }

callback 지옥보다는 코드가 간결해졌지만, 개발자는 더욱 간결하고 가독성이 좋은 코드를 원합니다.. 🧐 따라서 Promise Chaining 대신 사용할 수 있는 것이 바로 다음 글에서 다룰 Syntatic Sugar인 Async/Await 입니다.

Runtime의 Promise 동작 방식

아래 코드를 실행할 때 Runtime 동작은 어떻게 될지 보겠습니다!

console.log('Start!');

setTimeout(() => {
	console.log('Timeout!')
}, 0);

Promise.resolve('Promise!')
.then(res => console.log(res));

console.log('End!');

Task Queue

보기 전에 이전 글과 달라진 점을 하나 짚고 가야할 것 같습니다. 사실 Queue는 두 가지 유형이 존재합니다. 다음은 각 queue가 처리하는 작업인데, 여기서 중요한 것은 Promise의 callback 함수가 Micro task에 할당된다는 것입니다.

  • (Macro) task: setTimeout, setInterval, setImmediate
  • Micro task: process.nextTick, Promise callback, queueMicrotask

따라서, then(), catch(), finally() 메서드를 호출하면, 메서드 내의 callback 함수는 바로 microtask queue에 추가되는 것이죠. 역시 비동기 작업이기 때문에 바로 실행되지 않고 queue 추가되는 것입니다.😮

Task Queue의 작업 처리 순서

Task Queue의 작업 처리 순서는 Event Loop가 관리하는 것으로 알고 있습니다.
1. 먼저, Event Loop는 call stack이 비어 있는지 확인합니다.
2. call stack이 비어있다면, microtask queue에 작업이 있는 지 확인 후에 있다면 순서대로 call stack에 할당합니다.
3. call stack과 microtask queue 둘다 비어 있다면 그제서야 macrotask queue에 작업을 순서대로 call stack에 할당합니다.

  • 먼저, console.log()가 call stack에 쌓이고 바로 실행됩니다.

  • 이어서 setTimeout()이 call stack에 쌓이고, callback 함수가 Web API로 전달됩니다.

  • Web API에서 Macrotask queue에 해당 callback 함수를 할당합니다.
  • Promise.resolve()를 call stack에 추가하고, then의 callback 함수는 microstack queue에 추가됩니다.

  • 그 후 console.log() 를 callstack에 쌓아 바로 실행합니다.

  • 이제 callstack이 비어있기 때문에 microtask queue에 대기 중인 작업이 있는 지 확인 후, promise의 then callback 함수가 기다리고 있는 것을 확인했습니다.
  • 따라서 해당 작업을 call stack에 쌓아 실행합니다.

  • 더 이상의 microtask queue에서 대기 중인 작업이 없는 것을 확인했습니다.
  • 따라서 macro task queue에서 setTimeout()의 callback 함수가 대기 중인 것을 확인 후, call stack에 할당해 바로 실행합니다.

다음 글에 이어서 Async/Await 개념과 사용방식, 그리고 JavaScript 런타임에서는 어떻게 동작하는 지 작성해보겠습니다!

profile
한 걸음 한 걸음 쌓아가자😎

0개의 댓글