본문 바로가기
Programming

[Javascript] Promise란 무엇인가? ( async await 사용 방법)

by 개발자 염상진 2022. 5. 18.

How to deal with callback chain(Callback Hell)?

 

 

 

Promise, Callback Hell을 벗어나기 위한 도구

 

Javascript 비동기 프로그래밍을 위해 콜백 함수를 사용하지만, 전통적인 콜백 함수 방식은 콜백 헬(Callback Hell)이 발생할 가능성이 높고 가독성이 떨어지기 때문에 여러개의 비동기 함수를 처리하는데 한계를 가질 수 밖에 없다.

 

ES6에서는 비동기 프로그래밍을 위한 Promise 패턴을 지원한다. Promise 패턴을 사용하면 전통적인 콜백 헬을 피할 수 있고 명확하게 비동기 로직을 표현할 수 있다.

 

const printString = function(string){
	return new Promise((resolve, reject)=>{
    	setTimeout(function(){
            console.log(string);
            resolve();
        },
        Math.floor(Math.random() * 100 + 1)	
    }
    
    
}


const printAll() = function(){
	printString("Callback 1")
    .then(()=>{
    	return printString("Callback 2");
    })
    .then(()=>{
    	return printString("Callback 3");
    })
}

 

비동기 함수를 실행하면 함수의 코드가 종료되지 않았음에도 불구하고 즉시 종료해버린다. 비동기 함수 내부의 비동기 방식으로 작동하는 코드들은 비동기 함수가 종료된 후 완료된다. 

 

비동기 함수는 비동기 처리 결과를 함수 외부에 반환할 수 없다. 또한 상위 스코프의 변수에 할당할 수도 없다. 비동기 함수의 처리결과에 대한 후속 처리는 반드시 비동기 함수 내부에서 수행되어야 한다. 

 

비동기 함수의 처리결과에 대한 후속 처리는 콜백 함수를 전달하는게 일반적이다. 비동기 처리의 결과가 성공이면 실행할 콜백함수와 실패시 수행할 콜백함수를 인자로 전달한다. 

 

콜백 함수의 한계

 

일단 가독성이 떨어진다. 무한히 늘어지는 콜백함수를 작성하다 보면 어떤 로직에서 어떤 결과값을 도출해 낼 수 있는지 확인하기가 힘들다.

두번째 한계는 콜백함수를 전개할 때 Error 처리가 힘들다는 것이다. 비동기 함수가 최초 실행되면 코드가 평가되고 실행 컨텍스트가 생성되어 콜 스택에 올라간다.

비동기 함수는 콜백 함수가 호출되는 것을 기다리지 않고 바로 종료된다. 즉 콜 스택에서 바로 제거되어 버린다. 이 후 타이머가 완료되어 setTimeout 비동기 함수의 콜백함수가 태스크 큐로 푸시된다. 이벤트 루프가 돌면서 태스크 큐의 함수를 콜 스택으로 가져오기를 기다린다.

Javascript에서 에러(Error)는 호출자(Caller) 방향으로 전파된다. 즉 콜 스택의 바로 밑에 있는 함수의 방향으로 에러가 전파된다. 하지만 비동기 함수는 이미 콜 스택에서 제거된 상황이고, 비동기 함수의 매개변수가 콜 스택으로 push 되었을 때는 콜백 함수를 호출한 비동기 함수가 사라진 상태다.

결과적으로 setTimeout 비동기 함수의 콜백함수의 에러는 setTimeout으로 전파되지 않고 결국 setTimeout의 catch 문에 걸리지 않게 되는 것이다. 

 

try{
	setTimeout(()=>{throw new Error("Error")}, 100)
}catch(e){
	console.log(e);
}

 

 

Promise 생성

 

Promise 객체는 new 연산자로 생성한다. Promise는 ESMAScript에서 정의한 표준 빌트인 객체다. Promise 생성자 함수는 비동기 처리가 성공했을 때 인자로 전달되는 resolve 함수와 실패했을 때 전달되는 reject 함수를 인자로 받는다.

 

const promise = new Promise((resolve, reject) => {
	if(비동기 처리 성공)
		resolve();
        
    else(비동기 처리 실패)
    	reject();
})

 

Promise 객체 상태

 

Promise는 비동기 처리가 어떻게 진행되는지에 관한 상태 정보를 저장한다. Promise는 Object Prototype을 상속받는 객체다.

 

Promise가 가지는 상태정보는 [[PromiseState]]에 저장된다.

pending

비동기 처리가 아직 수행되지 않은 상태다. Promise 객체가 생성된 직후의 모습이다.

 

fulfilled

비동기 처리가 수행되었고 성공한 상태다. resolve 함수를 호출한다. setteled 상태다.

 

rejected

비동기 처리가 수행되었고, 실패한 상태다. reject 함수를 호출한다. setteled 상태다.

 

최초 Promise 객체가 생성되면 pending 상태로 저장된다. 이 후 비동기 함수가 작동하면 fulfilled 혹은 rejected 상태로 변경되는 이 상태를 setteled(결정된) 상태라 한다. 한번 상태가 결정되면 변경할 수 없다. 

 

비동기 함수가 가지는 Callback 함수의 결과값은 [[PromiseResult]] 에 저장된다.

 

 

Promise Chaining

 

최초 생성된 Promise는 pending 상태값을 가진다. 이후 비동기 함수 처리 결과에 따라 상태가 변하게 되는데 상태 변화에 따른 후속처리를 진행해준다. 이 때 사용하는 키워드가 then, catch, finally 다.

 

Promise.prototype.then

then 메서드는 Promise 처리 결과에 따라 2개의 콜백함수를 인자로 전달받는다.

첫번째 콜백 함수는 Promise 상태값이 fulfilled 인 경우 전달된다. 

두번째 콜백 함수는 Promise 상태값이 rejected 인 경우 전달된다. 

 

then 메소드는 항상 프로미스를 반환한다. then 메소드가 Promise 객체를 반환하면 Promise를 그대로 반환하고, then 내의 콜백함수가 Promise가 아닌 다른 값을 반환하면 암묵적으로 resole 혹은 reject해서 Promise를 생성해 반환한다. 

const sleep = (wait) => {
  return new Promise((resolve, reject)=>{
    setTimeout(()=>{
      // resolve('hello')
      reject("reject")
    }, wait)
  })
}

function runPromise(){
  sleep(100)
  .then((a)=>{console.log(a)}, (b)=>{console.log(b)})
}

runPromise();

 

Promise.prototype.catch

catch 메소드는 한개의 콜백함수를 인자로 전달받는다. Promise 상태값이 rejected 인 경우만 호출된다. catch 메소드는 언제나 Promise를 반환한다.

만약 then 에서 (resolve, reject) 두개의 콜백함수 인자를 받으면 catch에 도달하지 않는다. 

const sleep = (wait) => {
  return new Promise((resolve, reject)=>{
    setTimeout(()=>{
      // resolve('hello')
      reject("reject")
    }, wait)
  })
}

function runPromise(){
  sleep(100)
  .then((a)=>{console.log(a)})
  .catch(a=>console.log(`In Catch : ${a}`))
}

runPromise();

 

Promise.prototype.finally

finally 메소드는 한개의 콜백함수를 인자로 전달받는다. finally 메소드는 Promise 상태값에 상관없이 무조건 한번 호출되게 되어 있다. 

 

const sleep = (wait) => {
  return new Promise((resolve, reject)=>{
    setTimeout(()=>{
      resolve('hello')
    }, wait)
  })
}

function runPromise(){
  sleep(100)
  .then((a)=>{console.log(a)})
  .catch(a=>console.log(`In Catch : ${a}`))
  .finally(()=>console.log("This is the end"))
}

runPromise();
➜  Javascript node test.js
hello
This is the end

 

 

Promise Chaining

 

Promise의 then, catch, finally 후속처리 메소드들은 Promise를 반환하기 때문에 연속적으로 호출할 수 있게 된다. 이를 Promise Chaining(프로미스 체이닝)이라 한다.

 

만약 then, catch, finally Promise 후속 처리 메소드가 Promise 가 아닌 다른 값을 반환하더라도 암묵적으로 resolve 혹은 reject 해서 Promise를 생성후 반환하게 된다.

 

 

 

마이크로태스크 큐(Microtask Queue)

 

비동기 함수의 콜백함수들은 태스크 큐에 저장된다. 이 후 이벤트 루프에 의해 콜 스택이 비었는지와 태스크 큐에 가 차있는지를 비교해서 태스크 큐의 콜백함수를 콜 스택에 옮기면서 비동기 함수의 매개변수가 실행된다.

 

Promise의 후속 처리 메소드들은 태스크 큐가 아니라 마이크로태스크 큐(Microtask Queue)에 저장된다. 마이크로태스크 큐는 태스크 큐와 구분된다. 

 

마이크로태스크 큐에는 Promise 후속 처리를 위한 메소드 콜백 함수가 임시 저장된다. Promise가 아닌 일반 비동기 함수의 콜백 함수 혹은 이벤트 핸들러 함수는 태스크 큐에 저장된다.

 

주목할만한 점은 마이크로태스크 큐는 태스크 큐 보다 우선순위가 높다는 것이다. 즉, 이벤트 루프는 콜 스택이 비어있다는 것을 확인한 시점에 가장 먼저 마이크로태스크 큐를 확인하고 대기 중인 콜백 함수를 가져와 콜 스택에 올린다. 이 후 마이크로태스크 큐가 비게 되면 이벤트 루프는 태스크 큐에서 콜백함수를 가져오 콜 스택에 올린다.

 

setTimeout은 비동기 함수로 내부의 콜백함수는 태스크 큐에 저장된다.

Promise.resove()로 Promise를 생성한 후 then 후속 처리 메소드를 수행한다. then의 콜백함수들은 마이크로태스크 큐에 저장된다. 

 

실행 결과를 보면 마이크로태스크 큐의 콜백 함수를 먼저 수행하고, 이후 태스크 큐의 콜백함수를 수행한다.

 

 

 

 

 

Javascript Promise 정적 메소드

 

Promise는 생성자 함수다. Javascript에서는 함수도 객체이므로 메소드를 가지게 된다. Promise 객체는 5가지 메소드를 가지고 있다.

 

Method 설명
Promise.resolve 이미 존재하는 값을 resolve()로 수행 후  Promise를 생성
Promise.reject 이미 존재하는 값을 reject()로 수행 후 Promise를 생성
Promise.all 복수의 비동기 처리를 병렬처리할 때 사용

Promise 요소를 소유하는 배열등의 이터러블을 인수로 받음.

배열의 Promise가 하나라도 rejected 상태가 되면 즉시 reject()를 수행하고 Promise 반환

모든 요소가 fulfilled 이면 resolve()를 수행 후 Promise 반환
Promise.allSetteled Promise를 요소로 소유하는 배열등의 이터러블을 인수로 받음
전달받은 Promise의 상태가 모두 settled(fulfilled || rejected) 인 경우 처리 결과를 반환
Promise.race Promise를 요소로 소유하는 배열등의 이터러블을 인수로 받음.

전달 받은 모든 Promise의 상태가 fulfilled이 되면 가장 먼저 fulfilled된 Promise의 처리 결과를 resolve()를 수행해 Promise를 반환

 

 

Promise.all

Promise를 소유하는 배열을 인자로 전달받게 되며, Promise 중 하나라도 reject 상태를 반환하면 Promise를 반환한다.

 

먼저 Promise를 생성해준다. 

 

Promise.all() 메소드에는 Promise로 구성된 배열을 매개변수로 전달한다. 

Promise.all([promise1, promise2, promise3])
 .then((value)=>{
     console.log(value);
 })
 .catch((err)=>{
     console.log(err);
 });

 

결과값을 확인해보면 reject() 된 Promise가 catch 문에 걸려서 출력되고, 전체적으로는 fulfilled 된 Promise 자체를 반환한다.

3
Promise {<fulfilled>: undefined}

 

 

 

[Javascript] 비동기 프로그래밍이란? Asynchronous Call

Callback 함수 비동기 호출 Callback 함수는 다른 함수의 파라미터로 전달하는 함수를 말한다. 인자로 전달된 함수는 동기적으로 실행하거나 비동기적으로 실행할 수 있다. callback 함수를 비동기적으

about-tech.tistory.com

 

 

댓글