본문 바로가기
Javascript

콜백 지옥과 Promise, 그리고 async/await

by shinbian11 2022. 4. 27.

1
2
3
끝

 

와 같은 순서로 콘솔에 출력하는 함수를 만들고 싶다고 하자.

 

이것을 setTimeout 함수를 사용하여, 프로그램 실행 뒤, 바로 1이 출력 => 1초 뒤에 2가 출력 => 1초 뒤에 3이 출력 => 1초 뒤에 '끝!' 이 차례대로 출력되도록 하고 싶다고 가정하자.

 

그렇다면, 아래와 같이 구현할 수 있다.

 


function action() {
	console.log(1)
	setTimeout(() => {
			console.log(2)
			setTimeout(() => {
				console.log(3)
				setTimeout(()=>{
					console.log('끝');
				}, 1000)
			}, 1000)
	}, 1000)
}

action();

 

setTimeout의 콜백 함수 자리에 다음 함수를, 그 함수의 setTimeout의 콜백 함수 자리에 다음 함수를, ... 이것을 반복하고 있다.

이렇게 해도 원하는 결과는 출력된다. 

 

하지만, 콘솔에 출력하는 문장을 하나 만들려고 할 때마다 콜백 함수의 자리에 직접 내용을 정의하는 것은 그리 좋은 방법은 아닐 수 있다.

 

지금의 경우에서는 단 4개의 문장만 출력하였지만, 만약 100개의 문장을 출력하고 싶다면? 아님 1000개는? 10000개는...?

 

너무나도 복잡하여 말도 안되는 낮은 가독성을 가진 완성물이 나올 것이다. (함수 안에 함수 안에 함수 안에 함수 안에...)

 

 

그래서 아래와 같이 코드를 개선해보았다.

 

아래는 firstAction() => secondAction() => thirdAction() 의 순서대로 실행되는 코드이다.

 


function firstAction() {
	console.log(1)
	setTimeout(()=> { secondAction() }, 1000)
}

function secondAction() {
	console.log(2)
	setTimeout(()=> { thirdAction() }, 1000)
}

function thirdAction() {
	console.log(3)
	setTimeout(()=> { console.log('끝') }, 1000)
}

firstAction();

 

위와 같다면, 출력 결과는 역시나 우리가 원하는 대로 나올 것이다. 밑처럼!

 


1
2
3
끝

 

물론 이런 식으로 코드를 짜도 좋지만, Promise 객체를 사용하여 구현한다면 더 편리하게 구현이 가능하다.

 

일단 미리 결론부터 이야기를 간단히 하자면, Promise 객체에 .then() / .catch() / .finally() 메서드를 체이닝하여 연결하고, 콜백 함수는 연결한 메소드 내부에 첨부하는 방식이다.

 

이렇게 구현하는 것만으로도 콜백 함수를 분리하여 가독성을 높일 수 있다.

또한 chaining을 이용하여 비동기를 처리할 수 있다.

 

이러한 방식을 사용하기 때문에 콜백 지옥으로부터 벗어날 수 있다.

 

아래는 Promise 를 이용하여 구현한 코드이다.

 


function firstAction() {
	return new Promise((resolve) => resolve(1))	
}

function secondAction() {
	return new Promise((resolve) => {
		setTimeout(() => { resolve(2) }, 1000)
	})	
}

function thirdAction() {
	return new Promise((resolve) => {
		setTimeout(() => { resolve(3) }, 1000)
	})	
}


firstAction()
.then(function(result) { 
	console.log(result)
	return secondAction(result); 
}) 
.then(function(result) { 
	console.log(result)
	return thirdAction(result); 
}) 
.then(function(result) { 
	console.log(result)
	setTimeout(() => { console.log('끝') }, 1000) 
})

 

위 코드도 함수가 많아지고 복잡해지면 가독성이 떨어질 수 있다.

 

이럴 때는 async/await 를 이용할 수 있다.

 

async/await 는 비동기적인 코드를 동기적인 코드인 것처럼 바꾼다. 

 

async 함수는 Promise 객체를 암시적으로 반환하고, await는 async 함수 내에서만 사용이 가능하다. (콘솔 환경 제외)

 

또한, await 를 사용하면 비동기 함수를 사용하더라도 동기식의 스타일로 처리할 수가 있다.

 

다시 말해, 해당 줄이 끝나지도 않았는데 다음 줄을 먼저 실행하는 비동기식 작업이 아니라, await 가 포함되어 있는 코드의 실행이 모두 완료되어야 다음 줄로 넘어가는, 동기식 작업을 수행한다.


또한, async 함수를 호출하는 부분에서, await가 없으면 Promise 객체(약속) 를 반환하며, await가 있으면 그 Promise 객체의 데이터 (약속의 결과) 를 반환한다. 보통은 Promise 객체의 데이터가 궁금한 것이므로, await 키워드를 붙여서 함수를 호출한다.

 


function firstAction() {
	return new Promise((resolve) => resolve(1))	
}

function secondAction() {
	return new Promise((resolve) => {
		setTimeout(() => { resolve(2) }, 1000)
	})	
}

function thirdAction() {
	return new Promise((resolve) => {
		setTimeout(() => { resolve(3) }, 1000)
	})	
}

function finalAction() {
	return new Promise((resolve) => {
		setTimeout(() => { resolve('끝') }, 1000)
	})	
}

async function asyncStart() { // await를 사용하려면 async 함수 내에서 사용해야 한다.

	console.log(await firstAction()); // resolve 함수의 인수인 1이 반환 (Promise 객체의 데이터)
	console.log(await secondAction()); // resolve 함수의 인수인 2가 반환 (Promise 객체의 데이터)
	console.log(await thirdAction()); // resolve 함수의 인수인 3이 반환 (Promise 객체의 데이터)
	console.log(await finalAction()); // resolve 함수의 인수인 '끝'이 반환 (Promise 객체의 데이터)

}

asyncStart();

 

위 코드 역시도 같은 결과를 반환한다.