JavaScript/동작원리

자바스크립트 - Promise

태기의삶 2020. 9. 6. 18:54

 

 

Promise란?

자바스크립트에서 비동기 동작을 다루는 하나의 패턴이며, 어떤 일의 진행 상태를 나타내는 객체로, 진행 "상태""값"이라는 속성을 가지고 있는 것을 말한다.

 

Promise가 생긴 이유

Promise 생기기 이전에는 비동기 처리를 콜백 함수나 ajax 메소드를 통해서 처리해왔다.

jQuery ajax 메소드 비동기 처리

$.ajax("http://a.com/api/book", (result) =>{
	console.log(result);
});

위의 예제는 ajax 메소드를 통해 서버에 응답을 보내고 응답이 왔을 때, 콜백 함수를 통해 result로 응답을 받아 처리하는 예제이다.

 

setTimeout() 비동기 함수

setTimeout()은 비동기 동작을 하는 함수를 말한다.

function delay(sec, callback){
	setTimeout(() => {
    	callback(new Date().toISOString());
    }, sec * 1000);
}

delay(1, (result) => {
	console.log(1, result);
})

delay(1, (result) => {
	console.log(2, result);
})

delay(1, (result) => {
	console.log(3, result);
})

위 예제는 시간과 콜백 함수를 인자로 받아 setTimeout()을 통해 비동기 동작을 하는 delay 함수를 가지고 여러 번 호출하고 있는 예제이다. 

이 예제의 출력 값은 순서대로 1, 2, 3이 찍히는데, 

크롬 브라우저 콘솔창

시간을 보면 모두 같은 시간에 동시에 출력된 것을 확인할 수 있다.

그 이유는 delay 함수는 비동기 연산을 처리하는 함수이기 때문에, 먼저 console.log()가 찍히기 전에 브라우저에서 web API로 delay 함수들을 모두 보내기 때문이다.

그러고 나서, 정확히 1초가 지나면 모든 함수들이 다 같이 실행되기 때문에 같은 시간에 동시에 출력된 것이다.

 

자바스크립트 - 동작 원리

자바스크립트는 싱글 스레드 프로그래밍 언어이다. 싱글 스레드 런타임을 가지고 있다는 의미이다. 이것은 결국 한 번에 하나의 싱글 콜 스택(Call Stack)만을 가지고 있다는 말을 뜻한다. 여기서 �

ljtaek2.tistory.com

 

콜백 함수를 통한 비동기 처리

그러면 만약 위의 예제처럼 한 번에 출력하지 않고 1초 뒤, 2초 뒤, 3초 뒤에 값을 출력하고 싶다면,

function delay(sec, callback){
	setTimeout(() => {
    	callback(new Date().toISOString());
    }, sec * 1000);
}

// 동기적으로 1초씩 간격으로 실행됨
delay(1, (result) => {
	console.log(1, result);
    
    delay(1, (result) => {
		console.log(2, result);
    		
            delay(1, (result) => {
				console.log(3, result);
		})
	})
})

콜백 함수 안에 넣어서 처리하면 비동기 동작을 동기적으로 처리할 수 있다.

 

콜백 함수의 단점

//하지만 이럴 경우
delay(1, (result) => {
	console.log(1, result);
    
    delay(1, (result) => {
    		
            delay(1, (result) => {
				console.log(3, result); // 밑에 console.log(2, result)가 찍히고 나서 실행
		});
        
       console.log(2, result); // 1초 뒤에 실행되기 전에 바로 밑에 console.log()가 출력
	});
});
// 이런 코드는 정말 햇갈린다..

위 예제는 앞의 예제와 똑같이 1초 뒤, 2초 뒤, 3초 뒤에 값을 출력한다.

하지만 이 코드를 보면 어느 게 먼저 실행하는지 예측하기가 매우 어렵고 헷갈린다.

그렇다는 말은 즉, 콜백 함수는 가독성이 떨어진다는 뜻이며 동시에 단점이기도 하다.

step1(function(value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        step5(value4, function(value5) {
            // value5를 사용하는 처리
        });
      });
    });
  });
});

또, 콜백 함수는 위의 예제처럼 비동기 처리 순서를 보장하기 위해 여러 개의 콜백 함수가 중첩되어 복잡도가 높아지는 콜백 헬 또는 콜백 지옥이 발생하는 단점도 있다.

이러한 단점들 때문에 나온 것이 바로 promiseasync await이다.

async await은 나중에 다룰 예정이다.

 

콜백 지옥이란 무엇인가?

콜백 지옥은 콜백 패턴을 사용하여 비동기 처리 순서를 보장해주기 위해 여러 개의 콜백 함수를 중첩되어 사용하는데,

너무 많이 사용하면 복잡도가 높아져 코드의 가독성이 안 좋아지는 현상을 말한다. 

 

Promise 비교 및 생성

콜백 함수 vs Promise 비동기 동작

// 콜백 함수 비동기 동작 
function delay(sec, callback){
	setTimeout(() => {
    	callback(new Date().toISOString());
    }, sec * 1000);
}

delay(1, (result) => {
	console.log(1, result);
})

// Promise 비동기 동작
function delayP(sec){
	return new Promise((resolve, reject) => {
    	setTimeout(() => {
    		resolve(new Date().toISOString());
    	}, sec * 1000);
    });
}

delayP(1).then((result) => {
	console.log(1, result);
})

위 예제를 보면 두 함수의 차이

  • delay() 함수에서의 인자 값은 실행에 필요한 옵션과 실행이 끝났을 때 결과값을 반환하는 콜백 함수를 넘긴다.

  • 반면, delayP() 함수는 실행에 필요한 옵션을 파라미터로 넘기고, .then()을 통해 받은 결과를 콜백에 넘긴다. 

여기서 생각해보면 delayP()라는 함수를 호출했을 때 반환되는 그 값은 then()이라는 메소드를 가지고 있는 아이를 말하며, 즉, then이라는 애를 가진 아이가 바로 promise의 인스턴스라는 말이다.

여기서 인스턴스라는 말은 객체라고 이해하면 된다.

즉, promise를 사용하려면 promise의 인스턴스를 반드시 리턴해야 한다.

그래야만 then()에서 호출되어 처리할 수 있기 때문이다.

그럼 이제 delayP() 함수를 통해 promise의 생성 과정을 살펴보자.

// Promise 객체 생성
function delayP(sec){
	return new Promise();
}

return new Promise();라는 구문이 있는데, new 키워드를 통해 promise의 인스턴스를 생성하고 리턴한다는 의미이다.

그리고 promise를 생성할 때, resolvereject를 가진 콜백을 하나 넘긴다.

function delayP(sec){
	return new Promise((resolve, reject) => {
    // 비동기 작업 수행
    	if (/* 비동기 작업 수행 성공 */) {
    		resolve('result');
 		}
  		else { /* 비동기 작업 수행 실패 */
    		reject('failure reason');
    	}
});

여기서 resolve의 역할은 할 일을 다 했을 때 호출하는 것이고, reject의 역할은 할 일을 하다가 에러가 났을 때 또는 예외가 났을 때 호출하는 것을 말한다.

위에서는 callback() 함수로 결과값을 처리했지만, promise에서는 resolve를 통해서 처리한다.

function delayP(sec){
	return new Promise((resolve, reject) => {
    	setTimeout(() => {
    		resolve(new Date().toISOString());
    	}, sec * 1000);
    });
}

resolve를 통해서 어떠한 결과값을 넘기게 되면, 그 결과값이 then()에서 받은 그 함수에게 넘겨주게 된다.

이제 두 함수를 실행해보면 똑같은 결과값이 출력되는 것을 확인할 수 있다.

 

Promise를 통한 비동기 처리

그러면 이제 코드를 간단히 바꿔보자.

아까 콜백 함수 예제에서 처럼 순차적으로 1초씩 출력되는 것을 promise 패턴에 적용시켜보자.

// 기존 콜백 함수 1초 딜레이 예제를 프로미스 패턴에서 콜백 패턴으로 적용시키면
function delayP(sec){
	return new Promise( (resolve, reject) => {
    	setTimeout(() => {
    		resolve(new Date().toISOString());
    	}, sec * 1000);
    });
}

delayP(1).then((result) => {
	console.log(1, result);
    
    delayP(1).then((result) => {
		console.log(2, result);
	})
})

결과는 1초 뒤에 1이 찍히고, 2초 뒤에 2가 찍힌다. 

하지만 이러면 콜백 패턴이랑 다를 것이 전혀 없다.

 

그렇다면 어떻게 처리해야 할까?

promise에서는 콜백 안에서 콜백이 아니라 콜백을 순차적으로 지정해주는 것이다.

delayP(1).then((result) => {
    console.log(1, result);
    return delayP(1);
    }).then((result) => {
		console.log(2, result);
	})
}) 

안에 있던 콜백을 밖으로 빼고, 두 번째 줄에서 promise를 리턴하는 것이다.

그렇게 되면, 동일하게 1초씩 순차적으로 1과 2가 찍힐 것이다.

이 이야기는 뭐냐 하면, 첫 번째 then()에서 반환하는 promise가 있는데, 이 promise가 resolve가 되어야만 그다음 이어서 then()에서 받은 콜백을 실행한다는 의미이다.

이제 3초까지 딜레이를 주고 출력한다면,

delayP(1).then((result) => {
  console.log(1, result);
  return delayP(1);
}).then((result) => {
	console.log(2, result);
	return delayP(1);
}).then(result) => {
	console.log(3, result);
});

1초 간격으로 1, 2, 3이 찍힐 것이다.

 

만약 어떠한 값도 return하지 않은 상태에서 then()을 한다면?

delayP(1).then((result) => {
  console.log(1, result);
  return delayP(1);
}).then((result) => {
	console.log(2, result);
	return delayP(1);
}).then(result) => {
	console.log(3, result);
}).then(result) => {
	console.log(result); // undefind
});

undefined를 반환한다.

그 이유는 return 값이 없어 resolve를 하지 않았기 때문이다.

 

만약 return값이 promise가 아닌 다른 값을 리턴한다면?

delayP(1).then((result) => {
  console.log(1, result);
  return delayP(1);
}).then((result) => {
	console.log(2, result);
	return delayP(1);
}).then(result) => {
	console.log(3, result);
    return "Hello";
}).then((result) => {
	console.log(result)
});

결과를 출력하면 3초와 동시에 Hello가 찍힌다.

그 이유는 비동기 연산인 promise를 return하지 않았기 때문에 지연 없이 3과 동시에 Hello가 찍힌 것이다.

 

🎈 정리! 

  • Promise는 비동기 처리를 위한 하나의 패턴으로 자바스크립트에서 비동기 처리를 할 수 있게 해 준다.

  • Promise가 생기기 이전에는 ajax와 콜백 함수로만 비동기 처리를 했다.

  • 하지만 콜백 함수를 많이 쓰게 될 경우 콜백 지옥과 함께 코드의 가독성이 떨어지는 단점들이 생기게 된다.

  • 이러한 이유들로 인해 Promise와 async await이 생겨났다.

  • Promise는 new promise로 인스턴스를 생성하고 resolve, reject를 통해 값을 반환한다.

  • Promise를 사용하려면 promise의 인스턴스를 반드시 리턴해야 한다.

  • 반환된 promise 인스턴스는 then() 메소드를 통해 처리한다.

  • 이를 통해 promise를 통해 비동기 동작을 동기적으로 처리 할 수 있다. 

 

📝 느낀 점

이 Promise를 정리하기 위해 구글링을 통해 다양한 자료들을 찾아보면서 정리를 해보았다.

Promise 너 정말 어려운 아이군아.. 정리하기 쉽지 않았다..

하지만 정말 기초적인 것들만 정리했기 때문에 아마 깊은 내용은 없을 것이다.

그리고 아마 분명 틀린 내용도 존재할 텐데, 그럴 땐 댓글로 알려주시면 바로 수정하겠습니다.