프로미스는 왜 등장했고 어떻게 쓸까?

Sheryl Yun·2022년 10월 1일
0
post-thumbnail

동기적인 자바스크립트의 문제점

자바스크립트는 기본적으로 동기적으로 작동한다. 왜냐하면 작업을 처리하는 콜 스택이 하나여서 한 번에 하나의 작업만 처리할 수 있기 때문이다.
따라서 기존의 자바스크립트에서 fetch와 같은 비동기 로직은 제대로 작동하기 어렵다.

다음 예문을 보자.

const data = fetch('your-api-url-goes-here');

console.log('Finished'); // 문자열 출력
console.log(data); // fetch로 가져온 데이터 출력

한 작업이 완료된 뒤에야 다음 코드가 실행되는 동기적인 상황을 가정하면 Finished 문자열이 출력된 후에 data가 순서대로 출력될 것 같다.

하지만 실제 코드를 작동시켜보면 Finished 후의 data가 undefined로 출력되는데, 이는 fetch가 비동기적으로 작동하는 코드여서 반환값을 내놓기 이전에 다음 코드가 실행되기 때문이다.

이러한 상황을 막으려면 fetch가 반환되기까지 기다린 다음에 다음 코드를 실행하면 된다.
이를 위해 콜백 또는 프로미스를 사용할 수 있다.

콜백과 콜백 지옥

콜백 함수는 비동기 코드를 동기적으로 반환값이 나온 뒤 다음 코드를 실행하도록 하기 위해 함수 안에 함수를 넣은 것을 말한다.

근데 이 코드를 여러 번 실행하면서 중첩되는 함수가 많아지면 코드의 가독성이 급격히 떨어지는 콜백 지옥이 발생한다.

// 최종적으로 만들어진 피자
const makePizza = (ingredients, callback) => {
	// 재료를 섞고 결과값을 반환
	mixIngredients(ingredients, function(mixedIngredients)) {
    	// 위에서 반환받은 값으로 피자를 굽고 이 반환값이 최종 피자
        bakePizza(mixedIngredients, function(bakedPizza)) {
        	// 반환 후 끝을 나타내는 문구 출력
            console.log('finished!');
        }
    }
};

함수를 3개만 사용했는데도 벌써 엄청난 중첩이 발생했다.

이러한 '콜백 지옥'을 해결하기 위해 도입된 개념이 바로 프로미스이다.

프로미스

비동기 작업의 '성공' 또는 '실패'를 나타내는 객체 - MDN

프로미스는 내부 실행문이 성공했을 때 실행되는 resolve 함수와, 실패했을 때 실행되는 reject 함수를 가지는 객체이다.

const myPromise = new Promise((resolve, reject) => {
	// 내부 실행문
    // 성공하면 resolve, 실패하면 reject 호출
});

프로미스 안에서 즉시 resolve를 호출하면?

const myPromise = new Promise((resolve, reject) => {
	resolve("The value from the promise");
});

// myPromise를 실행한 반환값을 .then에서 data로 받아서 출력
myPromise.then(data => { console.log(data); });

즉시 실행하지 않고 조금 기다렸다가 실행하고 싶다면 프로미스 안에서 setTimeout을 활용하면 된다.

setTimeout 활용하기

const myPromise = new Promise((resolve, reject) => {
	setTimeout(() => {
    	resolve("The value from the promise");
    }, 2000); // 2초를 기다렸다가 내부 실행문 실행
});

myPromise.then(data => { console.log(data); }); // 2초 뒤에 data 출력

프로미스 내에서 오류가 발생하면?

reject 함수를 활용하여 프로미스 내에서 에러를 발생시킬 수 있다.
(또는 에러가 발생할 경우를 대비하여 반환할 에러 메시지를 미리 reject에 설정할 수 있다)

주의: reject 함수 자체가 new Error의 역할을 수행하므로 인자 안에 'new Error()'를 한번 더 쓸 필요가 없다.

const myPromise = new Promise((resolve, reject) => {
	setTimeout(() => {
    	reject("The value from the promise"); // reject 내부에서 new Error()를 호출하지 않음
    }, 2000);
});

myPromise
	.then(data => { 
		console.log(data); 
	}) // then을 건너뛰고 catch문을 실행
    .catch(err => {
    	console.log(err); // reject의 인자값을 err로 받아 출력한다
    });

프로미스 내에서 reject가 발생하면 이전의 then은 모두 건너뛰고 바로 catch문으로 이동하여 실행한다.

프로미스 체이닝

이전 프로미스에서 반환된 값을 다음 프로미스에서 인자로 받아 사용하면서 프로미스를 계속 연결하는 것을 프로미스 체이닝(chaining)이라고 한다.

const myPromise = new Promise((resolve, reject) => {
	resolve();
});
myPromise
	.then((data) => {
    	return 'working...';
    })
    .then((data) => {
    	console.log(data); // working...
        throw 'failed!'; // 오류 발생
    })
    .catch((err) => {
    	console.error(err); // failed!
    });

프로미스 체이닝은 실패한(catch) 이후에도 계속 이어나가기(then)가 가능하다.

myPromise
	.then((data) => {
    	throw new Error("웁스"); // 오류 발생
    	console.log("first"); // 위에서 바로 catch문으로 넘어가기 때문에 출력 x
    })
    .catch(() => {
    	console.log("에러 캐치"); // then에서 발생한 오류 처리
    })
    .then((data) => {
    	console.log("second"); // 위에서 오류를 처리하고 난 뒤 다시 정상 작동
    });

// 에러 캐치
// second

Promise.resolve()와 Promise.reject()

두 메서드는 프로미스를 즉시 생성 및 성공 또는 실패를 시키는 메서드로, 다음 두 가지 과정을 거쳐 실행된다.

  1. 메서드에 인자를 넣어서 new Promise 과정 없이 프로미스를 생성
  2. 바로 이어지는 then의 실행문에서 resolve 함수나 reject 함수를 호출

Promise.resolve()는 생성된 프로미스를 즉시 성공 처리하여 1항의 resolve가 실행된다.
Promise.reject()는 생성된 프로미스를 즉시 실패 처리하여 2항의 reject가 실행된다.

Promise.resolve("성공!").then(
	// 1항: resolve 함수
	function(value) {
		console.log("성공"); // 출력
	}, 
    // 2항: reject 함수
    function(value) {
		console.log("실패"); // 실행 안 됨
	}
);
Promise.reject(new Error("실패").then(
	function(value) {
		// resolve 함수 => 실행 안 됨
	}, 
    function(error) { // 생성된 프로미스의 "실패"를 인자로 받음
		console.log(error); // 실패 출력
	}
);

Promise.all()과 Promise.race()

all은 '전체, 모든'이라는 뜻이고 race는 '경주하다' 라는 뜻이다.
Promise.all()은 모든 프로미스가 성공해야만 최종 성공한 프로미스를 반환한다.
Promise.race()는 (여러 프로미스들이 경주하여) 가장 빨리 처리된 프로미스 '하나'만 반환된다.

먼저 프로미스를 두 개 생성하자.

const promise1 = new Promise((resolve, reject) => {
	// setTimeout(functionRef, delay, param)
	setTimeout(resolve, 500, 'first value');
});

const promise2 = new Promise((resolve, reject) => {
	setTimeout(resolve, 1000, 'second value');
});

각 프로미스가 독립적으로 처리되는 경우는 다음과 같다.

promise1.then((data) => {
	console.log(data); // 500ms 후 first value 출력
});

promise2.then((data) => {
	console.log(data); // 1000ms 후 second value 출력
});

Promise.all()을 사용하면 한 번에 처리할 수 있다. Promise.all()은 인자로 프로미스 배열을 받는다.

Promise
	.all([promise1, promise2])
    .then((data) => {
    	const [promise1data, promise2data] = data; // 배열 비구조화 할당
        console.log(promise1data, promise2data);
    });
// 1000ms 후 first value second value 출력

주의할 것은 promise1이 500ms가 지나고 완료된 후에 promise2까지 1000ms가 지나고 완료될 때까지 기다린다는 것이다.
즉, Promise.all()은 모든 프로미스가 완전히 다 반환될 때까지 최종 결과가 나오지 않는다.

Promise.all()의 또 다른 특징은 하나라도 실패하면 오류가 반환된다는 것이다.

const promise1 = new Promise((resolve, reject) => {
	resolve('first value');
});

const promise2 = new Promise((resolve, reject) => {
	reject('에러 발생!');
});

Promise
	.all([promise1, promise2])
    .then((data) => {
    	const [promise1data, promise2data] = data;
        console.log(promise1data, promise2data);
    })
    .catch((err) => {
    	console.log(err); 
        // promise1이 성공했어도 promise2 때문에 '에러 발생!' 출력
    });

Promise.race()는 Promise.all()처럼 프로미스 배열을 인자로 받는 형태는 똑같지만 가장 먼저 성공 또는 실패한 프로미스의 결과값만 반환한다는 차이가 있다.

const promise1 = new Promise((resolve, reject) => {
	// setTimeout(functionRef, delay, param)
	setTimeout(resolve, 500, 'first value');
});

const promise2 = new Promise((resolve, reject) => {
	setTimeout(resolve, 200, 'second value');
});

Promise.race([promise1, promise2]).then(function(value) {
	// 프로미스 둘 다 성공이지만 promise2가 더 빨리 성공함
	console.log(second value);
});

TMI: 비어 있는 이터러블을 전달하면?

Promise.all()에 비어 있는 이터러블(예: 배열)을 전달하면 (true값이 전달되는 것으로 인식하는지) 성공 처리된 프로미스를 반환한다.
반면 Promise.race()에 비어 있는 이터러블을 전달하면 (경쟁할 다른 프로미스가 없기 때문인지) 영원히 보류된 상태로 남아 있게 된다.

profile
데이터 분석가 준비 중입니다 (티스토리에 기록: https://cherylog.tistory.com/)

0개의 댓글