Promise - new Promise

DatQueue·2022년 3월 13일
3
post-thumbnail

Promise 만들어보기

이전의 Promise와 비동기 처리 포스팅에서 fetch API를 이용한 "json과 javascript의 데이터 주고받기" 를 통해 Promise 객체는 어떻게 쓰이고, 어떻게 처리가 되고 또한 왜 쓰이냐에 대한 원천적인 개념을 알아보았다. fetch는 자체적으로 Promise를 return하게 되고 우리는 그 '만들어진' Promise코드에 대해서만 느껴보았다.

이번 포스팅에선 실제로 Promise를 만들어보는 과정을 담아보고자한다.

참고로 이번 포스팅에선 앞전의 포스팅과는 달리 함수 코드 진행에있어 일반함수가 아닌 arrow function을 사용할 것임을 알린다.

new Promise 와 Promise의 3가지 상태

Promise 객체는 new 키워드Promise 생성자를 사용해 만든다.
생성자는 parameter로 실행함수를 받는다.

Promise 객체의 가장 기본적인 형태를 살펴보자면 다음과 같다.

const promise = new Promise((resolve,reject) => {});

console창을 통해 promise를 호출한 결과를 확인해보자.

promise는 Promise객체를 반환하고 PromiseState는 "pending" 이라고 명시되어 있다. 일단 여기까지 확인해 두고 다음 예제를 보자.

const promise = new Promise((resolve, reject) => {
  resolve("resolved success!");
});
promise.then((data) => {
  console.log("data", data);
});
console.log(promise);

Promise 생성자의 첫 번째 parameter로 받은 resolve를 Promise생성자안에서 return한 예제이다. 그 후 then메서드를 통해서 resolve된 값 (data)를 호출해보았다. 또한 마지막 줄에서 전체적 promise 변수를 콘솔창에 출력해보았다.

그럼 먼저 data 값을 확인해보자.

data값으로 resolve( )안의 값인 "resolved success"가 출력된 것을 확인 할 수 있다.

우리는 이전 Promise시작하기 포스팅에서 resolve란 단어에 대해 아주 짧게 알아보았다. resolve는 어떤 통신에 성공적으로 실행이 됨을 의미한다.

즉, 우리는 유추할 수 있다. ( 그리고 그 유추는 사실로 결론지을 수 있다. )

위 예제의 통신은 성공하였고 그에 따라 then메서드안의 callback parameter인 data는 그 성공한 값을 받아왔다는 것을.

물론, 위 코드처럼 resolve("resolved success!")와 같은 작업은 없다. 단지 예시일 뿐이고 new Promise 생성자 안에는 network 통신이나 파일을 읽는 것과 같은 비동기처리가 필요한 heavy한 작업들이 들어갈 것이다. Promise의 존재이유가 어쩌면 이 비동기 처리를 위해서 나왔다고 해도 무방하기 때문이다.

Promise에 관해 굉장히 기초적이고 원천적으로 다가가는 이번 글에서 그런 heavy한 작업을 넣기는 아직 많이 부족하고 오히려 주제를 잃을 것이라 생각한다. 그렇지만 우리는 최소한의 비동기처리와 Promise의 연관성을 보여주어야 하므로 setTimeout이라는 javascript Web API를 써보도록 하자.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("resolved success!");
  }, 2000);
});
promise.then((data) => {
  console.log("data", data);
});
console.log(promise);

마지막줄인 console.log(promise)가 먼저 출력되고 2초 뒤 resolve의 결과 값인 "resolved success!"가 출력된다.

그렇다면 마지막 줄인 console.log(promise); 의 결과도 확인해보자.

다음과 같이 PromiseState가 "fulfilled" 라고 나와있고 "resolved success"란 결과 값 또한 출력된 것을 볼 수 있다.
처음 예제에서 resolve와 reject중 어느 값도 받지 않았던 상태에서는 "pending (대기) " 이란 PromiseState를 얻었고 바로 위 예제와 같이 resolve값을 받았더니 "fullfilled (이행)" 란 PromiseState를 얻게 되었다.

resolve로 성공하였을 경우를 알아보았으므로 이번엔 성공하지 못하였을 경우, 눈치챘겠지만 "reject" 경우를 코드를 통해 알아보자.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("resolved fail!");
  }, 2000);
});
promise.then((data) => {
  console.log("data", data);
});
console.log(promise);

resolve일때와 같은 예제에 resolve만 reject로 바꾸어주었고 안의 결과 값 또한 통신에 실패하였다는 의미인 "resolved fail"로 적어주었다. 그럼 결과를 확인해보자.

마지막 줄 console.log(promise)가 출력된 후 2초 뒤 Promise 객체의 결과가 호출이 된다.

resolve때와는 달리 data를 출력하는 코드가 실행되지 않고 resolved fail이란 reject안의 결과 값과 함께 error메시지가 호출되었다.

마지막 줄의 promise 전체 변수를 호출한 결과또한 알아보자.

PromiseState가 "rejected"로 변한 것을 확인할 수 있다.

자, 우린 이렇게 어떠한 값도 반환 하지 않은 Pending(대기)상태, 성공한 resolve값을 반환한 Fullfilled(이행)상태, 작업에 실패하거나 오류인 reject를 반환하는 Rejected(실패)상태까지 3가지 Promise의 상태를 알아보았다.

처음부터 promise의 3가지 상태를 먼저 정의하고 예제를 이어가기엔 오히려 3가지 상태에 대해 와닿는 (?) 느낌이 부족할 것 같아서 promise의 간단한 예제를 설명하면서 동시에 진행하였다.
( 물론, 나만 부족할 수 도 있다. )

정리해보자면

Promise의 3가지 상태

  • Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
  • Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
  • Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

함수 안의 Promise

여태까지 Promise객체를 어떤 변수를 통해 ( 위 예제에선 promise로 ) 받아 작성하는 꼴로 진행하였다. 이해를 돕게 하기 위하여 ( 물론 저의 이해력입니다. ) 작성한 코드로 사실은 저렇게 작성하지는 않는다.

일반적으로 어떠한 함수 혹은 자바스크립트의 Class 안에서 Promise 객체를 정의하는 경우가 많다.

다음 예시를 보자.

function promiseOne() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("resolved success!");
    }, 2000);
  });
}
promiseOne().then((data) => {
  console.log("data", data);
});

promiseOne이란 함수명을 가진 함수를 만들어 그 안에 Promise생성자를 바로 return 해주었다.
앞전에 Promise 생성자를 가진 변수를 설정하여 그 변수로써 Promise 과정을 진행하는 것 보다 훨씬 깔끔하고 Promise 생성자가 함수안에서 return되었으므로 재사용성 또한 좋아졌다.

결과는 앞전의 resolve 호출 코드와 동일할 것이다. 그래도 확인해보자.

이렇게 우린 "남이 만든 Promise "코드가 아닌 우리가 직접 Promise를 만들게 된 것이다.

그렇지만 뭔가 여전히 아쉽다. 모두가 그렇게 느낄 것이다. 함수안에 Promise를 정의해 만들었지만 처음과 달리 확연하게 코드가 용이해지거나 실용성이 좋아졌다고 느끼지 못할 것이다.

다음 예제를 보면 전보다 이 Promise가 사용성에 있어 을 발휘하지 않을까 싶다.

function promiseOne() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("promiseOne success!");
    }, 2000);
  });
}
function promiseTwo() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("promiseTwo success!");
    }, 2000);
  });
}
promiseOne().then((data) => {
  console.log("data1", data);
  promiseTwo().then((data) => {
    console.log("data2", data);
  });
});

바로 Promise를 return하는 함수를 2개이상으로 만든 경우이다. ( 일단은 2개로 )

설명할 것 도 없는 예제이지만 아주 간단히 말하자면 Promise객체를 담은 두 함수를 만들었고 작업이 성공하였다는 가정하에 resolve값을 각각 정의하였다. 그리고 비동기 느낌을 내기 위해 resolve값들을 setTimeout으로 감싸준 뒤 2000ms의 지연시간을 주었다.
앞 전에 promise란 변수로 Promise객체를 생성한 뒤 작업을 진행하였을 때, 실행과정에서 promise.then( )과 같이 변수명 호출 뒤 then메서드를 작성한 것처럼, 이번에는 promiseOne( ) .then, promiseTwo.then( )과 같이 함수를 먼저 호출하고 그 뒤에 then메서드를 작성하였다.

결과를 먼저 확인해 보자.

( console 결과 부분이 작아서 잘 보이지 않는다는 점 양해바란다. )

코드를 실행시킨 후 2초 뒤 promiseOne( ) 함수안의 resolve값인 "promiseOne success!"가 data1으로써 먼저 출력되었고 정확히 2초 뒤 잇달아 promiseTwo( )함수안의 resolve값인 "promiseTwo success!" 가 data2로써 출력되었다.

이렇게 promiseOne( ).then 안에 promiseTwo( ).then . 즉, 하나의 then 안에 또 다른 then을 넣었다.
이전 포스팅에서도 언급하였지만 이러한 방법을 "Nested promise" 라 한다.

하지만 느낌 (?) 이라는 것이 오겠지만 우린 위와 같은 방식 , 즉, Nested 방식으로 코드를 짜진 않는다. "Promise Chaining" 이란 보다 간편한 방식이 있기 때문이다.

Promise Chaining에 대한 기본은 이전 포스팅을 참조하자.

Promise와 비동기 처리

그럼 "Promise Chaining"으로 작성한 코드를 보도록 하자.

function promiseOne() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("promiseOne success!");
    }, 2000);
  });
}
function promiseTwo() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("promiseTwo success!");
    }, 2000);
  });
}
// promiseOne().then((data) => {         --> Nested 방식
//   console.log("data1", data);
//   promiseTwo().then((data) => {
//     console.log("data2", data);
//   });
// });
promiseOne()                          // --> Chaining 방식
  .then((data) => {
    console.log("data1", data);
    return promiseTwo();
  })
  .then((data) => {
    console.log("data2", data);
  });

promiseOne( ).then 안에서 promiseTwo( ).then을 작성하는 것이 아닌 첫번째 then 메서드안에서는 promiseTwo를 return만 시키고 그에 해당하는 then메서드는 병렬적으로 첫번째 then 외부에서 작성하였다.

then메서드 안에서 return시키는 값 또한 Promise를 띄므로 병렬적으로 .then( )을 이을 수 있는 것이다.
( 이 원리를 쉽게 설명하는게 힘든 점 양해바란다. )

위에 코드 예시의 경우엔 then의 실행이 두 번 밖에 없으므로 Nested 방식에 비해서 Promise Chaining 방식이 확 와닿지 않을 수 있지만 then안에 then 또 그 안에 then 이렇게 then 메서드가 많이 필요로 하는 상황이 오게 된다면 Nested 방식은 코드가 끊임없이 오른쪽으로 치우치게 될 것이고 마치 "Callback Hell (콜벡 지옥)" 을 방불케 할 것이다.
그에 비해 Chaining 방식은 수직적으로 각각의 then메서드가 의미상으로만 연결되어있지 따로 실행되니까 훨씬 깔끔해보이는 것이 사실이다. ( 물론 .then.then.then.then .... then 지옥도 곧 느낄 것이다. )

Error Handling

위의 Promise Chaining 코드에서 promiseOne 함수의 결과를 reject로 호출해보았다. 이외의 다른 코드진행은 전부 동일하다.

function promiseOne() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("promiseOne fail!");   //reject
    }, 2000);
  });
}
function promiseTwo() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("promiseTwo success!");
    }, 2000);
  });
}
promiseOne()
  .then((data) => {
    console.log("data1", data);
    return promiseTwo();
  })
  .then((data) => {
    console.log("data2", data);
  });

결과를 확인해보자.

2초 뒤, promiseOne fail이라는 reject안의 값이 에러 메시지로 호출되었다. 그리고 promiseTwo 함수로의 진행은 되지 않은 것 또한 확인할 수 있다.

이렇게 Error Message가 뜨게 하기 보단 조금 더 유연하게 error처리를 하기 위해선 어떻게 해야할까?

- Catch ( )

바로 catch 메서드를 사용하면 된다.

catch( ) : Promise 구문 예외 상황 처리에 있어 유용하고도 확실한 method이다.

function promiseOne() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("promiseOne fail!");
    }, 2000);
  });
}
function promiseTwo() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("promiseTwo success!");
    }, 2000);
  });
}
promiseOne()
  .then((data) => {
    console.log("data1", data);
    return promiseTwo();
  })
  .catch((reason) => {               //catch
    console.log("reason", reason);
  })
  .then((data) => {
    console.log("data2", data);
  });

결과를 확인해보자.

통신에 실패하였으므로 즉, promiseOne 함수의 통신에 rejected 상태가 되었으므로 data1은 출력이 될 수 없고 이어지는 data2의 resolve값 또한 undefined로 출력된다.
그렇지만 catch( )를 사용함으로써 Uncaught(잡히지 않은) Error 메시지가 뜬 것보다 조금 더 유연하게 처리할 수 있다는 점이 매력이다.

그런데 , 첫 번째 함수. 즉, 첫 번째 then안에서 에러가 호출되었는데 다음 작업이 수행되는 부분이 그렇게 좋아보이지는 않는다. ( 2번째 then안의 data2 값이 undefined 이지만 일단은 console.log 작업이 수행되었으므로... )

방법 1 으로는 Promise를 chaining하는 과정에 있어 catch 메서드를 가장 끝에 넣어주면 된다.
위 코드를 기준으로 하면

promiseOne()
  .then((data) => {
    console.log("data1", data);
    return promiseTwo();
  })
  .then((data) => {
    console.log("data2", data);
  })
  .catch((reason) => {             //마지막에 catch
    console.log("reason", reason);
  });

그럼 결과 값이 다음과 같이 나온다.

실행 2초 뒤 catch구문안의 console만 실행되고 다른 줄은 실행되지 않는다.

하지만 이 방법마저도 뭔가 썩 내키지 않는다. 두 번째 then안의 data에 있어서 어떻게 처리가 되었느냐는 알려주는게 맞다고 생각한다. 위 예시처럼 단순한 코드는 눈으로 봐도 바로 알 수 있지만 복잡한 통신에 있어선 호출이 안된 구문은 이유가 보여지는 것이 좋을 것이다.

그렇게 하기 위해선 catch 구문안에서 Promise.reject( )를 사용하면 된다.

- Promise.reject( )

Promise.reject(reason) 메서드는 주어진 이유(reason)로 거부된 Promise 객체를 반환한다.

위 내용 그대로, 구문은 아래와 같은 형식이고

Promise.reject(reason);

reason은 해당 Promise를 거부한 이유가 나오게 된다.

그럼, 코드로써 확인해보자.

promiseOne()
  .then((data) => {
    console.log("data1", data);
    return promiseTwo();
  })
  .catch((reason) => {
    console.log("reason", reason);
    return Promise.reject(reason);         //Promise.reject( );
  })
  .then((data) => {
    console.log("data2", data);
  });

결과를 바로 확인해보면 다음과 같다.

잇달아 오는 then 메서드안의 코드는 실행되지 않고 에러메시지가 호출되며 reason으로 거부된 (rejected) Promise 객체인 reject ("promiseOne fail!")을 반환한다.

이번 포스팅에서는 다루지 않겠지만 Promise객체에는 Promise.reject( ) 메서드 외에도 다른 유용한 메서드들이 있다. 조만간 다룰 예정이다.
또한 then, catch 메서드와 더불어 finally 메서드또한 최근에 많이 쓰이는데 비교적 모두가 금방 이해할 것이라 믿고, 굳이 언급하지는 않겠다.

마무리.....

이렇게 지난 포스팅에 이어 javascript의 Promise에 대해 알아보았고 특히 이번 포스팅에서는 남이 만든 Promise가 아닌 우리가 간단한 예제를 통해서나마 직접 Promise를 만들어보았다.
비동기 처리를 요하는 hard한 작업을 직접 짜보진 못해서 크게 못 와닿을 수도 있지만 setTimeout API를 이용해 비동기 처리 흉내도 내보았고 catch 구문을 이용해 error handling 또한 알아보았다.

사실 이번 포스팅은 오로지 Promise를 만드는 아주 간단한 과정에 기반을 두고 있으므로 깊숙히 들어가 다양한 메서드들을 익히는 시간은 아니었다. 하지만 이렇게 Promise 생성자의 기본 주춧돌이 잘 박혀있어야 다음으로 나아갈 수 있다고 생각한다.

더 많은 내용을 적기에는 포스팅 내용이 너무 지루해질것으로 판단해 여기서 마무리 짓고 다음 포스팅에서 이어가겠다. (말이 주절주절 길었다... 죄송합니다.)

profile
You better cool it off before you burn it out / 티스토리(Kotlin, Android): https://nemoo-dev.tistory.com

0개의 댓글