JavaScript 비동기 처리 - Promise 객체

bp.chys·2020년 5월 19일
1
post-thumbnail

velopert님의 모던 자바스크립트 강의자료를 참고하여 작성했습니다.

자바스크립트는 동기식인가 비동기식인가?

우선 프로그래밍에서 동기(sync)와 비동기(async)의 정의를 이해할 필요가 있다.

동기 : 이전 작업의 실행이 끝나야 다음 작업 실행을 시작한다.
비동기 : 이전 작업의 실행과 무관하게 다음 작업을 실행한다.

동기와 비동기를 설명할 때 대표적인 예시로 은행과, 카페를 예시로 들곤한다. 은행에서는 사람들에게 번호표를 나눠주고, 이전 사람의 은행 일이 끝날 때 까지는 계속 대기해야 한다. 반면에 카페에서는 주문과 함게 진동벨을 받게 되고 진동벨이 울리기 전에 대기하면서 개인 작업을 할 수 있다.

싱글스레드인 자바스크립트는 기본적으로 동기식이지만, API에 요청을 보낼 때 응답이 올 때까지 마냥 기다리기만 할 수 없기 때문에 비동기 처리가 필요하다.

하지만 비동기 처리를 할 경우, 의도하지 않은 순서로 함수가 실행될 수 있기 때문에 원하는 부분에서 동기 방식으로 변환을 해줘야 한다.

첫번째 방식이 지난번에 살펴본 콜백함수이다. 콜백함수는 함수안에서 또 다른 함수를 호출하는 것인데 여러 함수를 순서대로 호출할 필요가 있을 경우 콜백지옥을 경험하게 되는 문제점이 있다.

숫자 n 을 파라미터로 받아와서 다섯번에 걸쳐 1초마다 1씩 더해서 출력하는 작업을 setTimeout 으로 구현해보자.

function increaseAndPrint(n, callback) {
  setTimeout(() => {
    const increased = n + 1;
    console.log(increased);
    if (callback) {
      callback(increased);
    }
  }, 1000);
}

increaseAndPrint(0, n => {
  increaseAndPrint(n, n => {
    increaseAndPrint(n, n => {
      increaseAndPrint(n, n => {
        increaseAndPrint(n, n => {
          console.log('끝!');
        });
      });
    });
  });
}); 

비동기를 동기 방식으로 변환하는 두 번째 방법은 Promise객체를 사용하는 것이다. 이번 글에서는 Promise에 대해 알아보자.

Promise

Promise는 성공할 수도 있고, 실패할 수 도 있다. 성공할 때는 resolve 함수를 호출하고, 실패할때는 reject 함수를 호출한다.

const myPromise = new Promise((resolve, reject) => {
  // 구현..
})

1초 뒤 성공하는 상황을 구현해 보자

const myPromise = new Promise((resolve, reject) => {
	setTimeout(() => {
		resolve(1);
	}, 1000);
});

myPromise.then(n => {
	console.log(n);
});

resolve 를 호출 할 때 특정 값을 파라미터로 넣어주면, 이 값을 작업이 끝나고 나서 사용 할 수 있다. 작업이 끝나고 나서 또 다른 작업을 해야 할 때에는 Promise 뒤에 .then(...) 을 붙여서 사용하면 된다.

이번에는 1초뒤 실패하는 상황을 만들어보자

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error());
  }, 1000);
});

myPromise
  .then(n => {
    console.log(n);
  })
  .catch(error => {
    console.log(error);
  });

실패하는 상황에서는 reject를 사용하고 .catch를 통하여 실패했을시 수행할 작업을 설정할 수 있다.

이제 Promise를 만드는 함수를 작성해 보자.

function increaseAndPrint(n) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			const value = n + 1;
			if (value === 5) {
				const error = new Error();
				error.name = 'ValueIsFiveError';
				reject(error);
				return;
			console.log(value);
			resolve(value);
		}, 1000);
	});
}

increaseAndPrint(0)
	.then(increaseAndPrint)
	.then(increaseAndPrint)
	.then(increaseAndPrint)
	.then(increaseAndPrint)
	.then(increaseAndPrint)
	.catch(e => {
		console.error(e);
	});

Promise 를 사용하면, 비동기 작업의 개수가 많아져도 코드의 깊이가 깊어지지 않게 된다.

promise 좀 더 알아보기

프로미스 객체는 생성자를 사용해서 만들 수 있다.
생성자의 인자로 executor라는 함수를 이용한다.

new Promise( /*executor*/ );

executor함수는 resolve와 reject를 인자로 가진다.
(resolve, reject) ⇒ {...}
여기서 resolve와 reject는 각각 함수이다.

new Promise( /*executor*/ (resolve, reject) => {...} );

생성자를 통해 프로미스 객체를 만드는 순간 pending(대기) 상태라고 한다.

new Promise((resolve, reject) => {}); // pending

executor함수 인자중 하나인 resolve함수를 실행하면, fulfilled(이행) 상태가 된다.

new Promise((resolve, reject) => {
	//pending
	//... 비동기 처리
	resolve();  // fulfilled
});

executor함수 인자중 하나인 reject함수를 실행하면, rejected(거부) 상태가 된다.

new Promise((resolve, reject) => {
	reject();  // rejected
});

p라는 프로미스 객체는 1초 후에 fulfilled된다.
또한 fulfilled된 시점에 p.then안에 설정한 callback함수가 실행된다.

const p = new Promise((resolve, reject) => {
	/*pending*/
	setTimeout(() => {
		resolve(); /* fulfilled */
	}, 1000);
});

p.then(() => {
	/* resolve 된 이후에 실행됨*/ 
	/* callback */
	console.log('1000ms후에 fulfilled 됩니다.');
});

then을 설정하는 시점을 명확히하고 함수의 실행과 동시에 프로미스 객체를 만들면서 pending이 시작하도록 하기 위해 프로미스 객체를 생성하면서 리턴하는 함수 p를 만들어 p실행과 동시에 then을 설정한다.

function p() {
	return new Promise((resolve, reject) => {
		/*pending*/
		setTimeout(() => {
			resolve(); /* fulfilled */
		}, 1000);
	});
}

//원하는 시점에 프로미스 객체를 생성하고 콜백함수도 호출할 수 있다.
p().then(() => {
	console.log('1000ms후에 fulfilled 됩니다.');
});

이번엔 reject상황도 만들어보자. reject는 then이라니라 catch를통해 콜백함수를 처리한다.

function p() {
	return new Promise((resolve, reject) => {
		/*pending*/
		setTimeout(() => {
			reject(); /* rejected */
		}, 1000);
	});
}

//원하는 시점에 프로미스 객체를 생성하고 콜백함수도 호출할 수 있다.
p()
	.then(() => {
		console.log('1000ms후에 fulfilled 됩니다.');
	})
	.catch(() => {
		console.log('1000ms후에 rejected 됩니다.');
});

excecutor의 resolve함수에 인자를 넣어 실행하면, then의 callback함수의 인자로도 받을 수가 있다.

function p() {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve('hello'); 
		}, 1000);
	});
}

//원하는 시점에 프로미스 객체를 생성하고 콜백함수도 호출할 수 있다.
p().then(message => {
	console.log('1000ms후에 fulfilled 됩니다.', message);
});

마찬가지로 excecutor의 reject함수에 인자를 넣어 실행하면, catch의 callback함수의 인자로도 받을 수가 있다.

function p() {
	return new Promise((resolve, reject) => {
		/*pending*/
		setTimeout(() => {
			reject('error');
		}, 1000);
	});
}

//원하는 시점에 프로미스 객체를 생성하고 콜백함수도 호출할 수 있다.
p()
	.then(() => {
		console.log('1000ms후에 fulfilled 됩니다.');
	})
	.catch(reason => {
		console.log('1000ms후에 rejected 됩니다.', reason);
});

하지만 일반적으로는 error 문자열이 대신에 객체를만들어서 던진다.

function p() {
	return new Promise((resolve, reject) => {
		/*pending*/
		setTimeout(() => {
			reject(new Error('error');
		}, 1000);
	});
}

//원하는 시점에 프로미스 객체를 생성하고 콜백함수도 호출할 수 있다.
p()
	.then(() => {
		console.log('1000ms후에 fulfilled 됩니다.');
	})
	.catch(e => {
		console.log('1000ms후에 rejected 됩니다.', e);
	});

추가적으로 fulfilled 되거나 rejected된 후에 최종적으로 실행할 것이 있다면, .finally()를 설정하고, 함수를 인자로 넣는다.

function p() {
	return new Promise((resolve, reject) => {
		/*pending*/
		setTimeout(() => {
			reject(new Error('error');
		}, 1000);
	});
}

//원하는 시점에 프로미스 객체를 생성하고 콜백함수도 호출할 수 있다.
p()
	.then(() => {
		console.log('1000ms후에 fulfilled 됩니다.');
	})
	.catch(e => {
		console.log('1000ms후에 rejected 됩니다.', e);
	})
	.finally(() => {
		console.log('end');
	});

하지만, 이것도 불편한점이 있긴 하다. 에러를 잡을 때 몇번째에서 발생했는지 알아내기도 어렵고 특정 조건에 따라 분기를 나누는 작업도 어렵고, 특정 값을 공유해가면서 작업을 처리하기도 까다로운데 async/await 을 사용하면, 이러한 문제점을 깔끔하게 해결 할 수 있다.


참고자료

Async to sync Javascript

profile
하루에 한걸음씩, 꾸준히

0개의 댓글