
이전 포스트에서 콜백 함수의 콜백 지옥을 보았다.
이 콜백 지옥을 해결하기 위해 프로미스를 사용한다고 하였다.
프로미스에 대해서 알아보도록 하자.

처음 보는 사람이라면 그리고 자바스크립트에 아직 익숙치 않다면 구조가 좀 어지러울 수도 있다.
new Promise로 생성하는 프로미스 객체의 인터페이스를 보자.
(interface는 타입스크립트에서 클래스/객체의 구조를 정의하는 키워드인데, 추상자료형이라고 생각하면 편하다.)

주석의 첫 번째 줄을 보면 프로미스에는 프로미스가 완료(fulfilled 또는 rejected) 될 때 호출되는 콜백 함수를 붙여라 라고 되어있다.
fullfilled 와 rejected 는 프로미스의 상태인데, 아래의 표를 보자.
| state | description | callback |
|---|---|---|
| pending | 작업이 아직 실행 중이며 Promise가 아직 보류 중 | - |
| fulfilled | 작업이 완료되었으며 성공 | .then() |
| rejected | 작업이 완료되었지만 오류가 발생 | .catch() |
| settled | Promise가 resolved(해결) 되어지든 rejected(거부) 되어지든 이 콜백을 호출 | .finally() |
실제로 프로미스 객체가 가지는 상태는 pending, fulfilled, rejected 로 3가지다.
위의 표를 한 번 읽어보면 프로미스의 상태는 pending에서 fulfilled 혹은 rejected로 바뀐 다는 것을 알 수 있다.
표에서 말하는 작업이라는 것은 Promise를 생성할 때 전달해주는 콜백 함수를 말한다.
즉,
new Promise(() => {
// 여기가 작업
})
이다. 이 작업은 Promise 객체를 생성하는 순간 바로 실행한다.
위의 표를 다시 한 번 보면 이해가 간다.
- Promise 객체에 전달한 작업이 성공적으로 완료된다면 상태가 fulfilled가 되면서 .then() 에 전달한 콜백함수가 실행된다.
- Promise 객체에 전달한 작업이 성공적으로 완료되지 않는다면 상태가 rejected가 되면서 .catch() 에 전달한 콜백함수가 실행된다.
- 그리고 fulfilled, rejected 여부에 관계없이 .finally() 에 전달한 콜백 함수가 실행된다.
이제 위의 코드를 다시 한 번 보자.
- randomNumber 가 5보다 크다면 성공으로 간주해서 .then에 전달한 콜백함수가 실행된다.
- randomNumber 가 5보다 작다면 실패로 간주해서 .catch에 전달한 콜백함수가 실행된다.
...?
randomNumber가 5보다 크다면 성공이고 randomNumber가 5보다 작으면 실패다...
그 기준을 누가 정해서 .then을 실행하고 .catch를 실행하는거지?
그것을 바로 new Promise((resolve, reject)=>{}) 에서 resolve 와 reject가 해준다.
resolve() 를 실행했다는 것은 성공했다는 뜻이고, reject()는 반대로 실패했다는 뜻이다.
위의 코드를 다시 보자.
프로미스
.then((결과)=>console.log(결과))
라고 되어 있다. 눈치챈 사람도 있겠지만 이 결과는 resolve 나 reject에 전달해준 값이다.
이제 저 위의 코드를 이해할 수 있게됐다.
- new Promise() 객체를 생성하면서 동시에 전달한 콜백 함수(작업)를 실행한다.
- 콜백 함수(작업)가 성공적으로 완료되었다면, resolve() 함수를 실행했다면 .then의 콜백 함수를 실행하며, 성공적으로 완료되지 않았다면 reject() 함수를 실행하여 .catch의 콜백 함수를 실행한다.
- finally에 전달된 콜백 함수를 실행한다.
위에서 프로미스 객체를 생성하는 순간 작업이 시작하고, 작업의 결과에 따라 .then .catch 를 실행한다고 했다.
프로미스 객체를 생성하기만 하면 되기에 이런 방법으로도 사용할 수 있다.

이렇게 함수에서 반환과 동시에 Promise를 생성하면 프로미스.then() 이 아니라 프로미스().then()을 이용해서 사용할 수 있다.
이것 역시 위의 코드와 같고 결과는 아래와 같다.

두 번 실행해서 값을 뽑아낸 것이다.
보통은 Promise를 사용할 때 이렇게 함수로 감싸서 사용하는 편이다.
변수에 저장할 때와 달리 함수로 감쌀 때의 장점이 있다.
- 함수로 감싼다면 우리가 원할때마다 호출해서 재사용성이 높아진다.
- 함수로 감싼다면 매개변수를 전달할 수 없는 변수에 저장할 때랑 달리 함수를 호출할 때 마다 필요한 매개변수를 전달할 수 있어 동적인 프로그래밍이 가능해진다.
프로미스 객체는 3가지의 상태를 가진다는 것을 알았다.
위의 표를 다시 가져와보자.
| state | description | callback |
|---|---|---|
| pending | 작업이 아직 실행 중이며 Promise가 아직 보류 중 | - |
| fulfilled | 작업이 완료되었으며 성공 | .then() |
| rejected | 작업이 완료되었지만 오류가 발생 | .catch() |
| settled | Promise가 resolved(해결) 되어지든 rejected(거부) 되어지든 이 콜백을 호출 | .finally() |
다시 말하지만 프로미스 객체는 실제로 pending, fulfiiled, rejected의 3가지 상태만을 가진다.
이 상태는 아래와 같이 확인할 수 있었다.


이렇게 .then() 을 연결시키지 않고 곧바로 반환된 프로미스 객체를 출력하면
현재의 상태를 알 수 있다.
왜인지 Fulfilled 상태는 출력되지 않았지만 pending과 rejected가 출력되어서 다행이었다.
만약 프로미스가 정상적으로 동작하지 않을 경우 이 상태를 확인함으로써 오류를 고쳐나갈 수 있을 것 같다.
그런데 놀랍게도 이 .then()을 체이닝, 즉 연쇄적으로 연결시킬 수 있었다.

위의 코드를 한 번 천천히 읽어보자
- 프로미스1() 이 성공적으로 1을 반환했다. res(1)
- 그래서 첫 번째 .then()에 전달된 콜백인 num => return sum += num 을 실행한다.
- 두 번째 .then()의 인자인 num 은 첫 번째 .then() 에서 반환한 sum 이다.
- 세 번째 .then() 역시 두 번째 .then()의 반환값을 받아와서 sum += num을 계산한다.
- 최종 결과 값을 출력한다.
여기서 알 수 있는 점은 .then() 이 반환하는 값을 다음 .then() 에서 사용할 수 있다는 점이다.
그리고 그 값은 .then((num) => {return sum += num}) 에서 붉게 표시한 부분이다.
아래처럼 체이닝도 가능하더라.

공부를 하다보니 새로운 걸 하나 또 알게됐다.
프로미스에 관한 이야기는 아니지만 위에서 .then() 을 보면 콜백함수가 아니라 함수명만 전달했다.
화살표 함수는 만약
.then((data) => 프로미스2(data)) 라면
.then(프로미스2) 로 생략할 수 있다.
즉, 하나의 함수만 호출하고 그 함수의 인자가 화살표 함수의 인자라면 생략이 가능하다.
어쨌든 위의 코드를 보자.
- 프로미스1() 을 호출했고 1을 성공적으로 반환했다. res(1)
- 첫 번째 then에서 프로미스1 에서 반환받은 1을 받아서 2초후에 1(num) + 2 를 반환한다.
- 두 번째 then에서 프로미스2 에서 반환받은 3을 받아서 3(num) + 3 을 반환한다.
- 6을 출력한다.
중간에 프로미스2 에서 2초 후에 데이터를 받는 작업을 했지만 작업이 꼬이지 않고 성공적으로 6을 출력하는 것을 알 수 있다.
즉, 두 번째 then에서 첫 번째 then의 콜백함수가 값을 반환할 때 까지 기다린 후 콜백 함수를 실행한다.
또 프로미스3이 반환하는 것은 Promise 객체가 아니라 그냥 num+3 이다.
그럼에도 .then()을 추가로 연결해준 것을 알 수 있다.
즉, 프로미스 체이닝에서는 반환하는 값이 프로미스가 아니더라도 값을 반환만 한다면 다음 then에다가 넘겨줄 수 있다는 것을 알았다.
물론 체이닝 도중 .then() 안에서 값을 반환하지 않아도 다음 .then() 으로 연결은 가능하다.
.then((data) => {console.log(data)})
.then(() => {console.log("배고프다...")})
처럼
.then()에서 반환하는 값 없이 console.log 만 실행했지만 다음 .then()으로 체이닝 할 수 있었다.
하지만 첫 번째 then 은 반드시 프로미스 객체에만 연결할 수 있다.
콜백지옥이라는 콜백 함수의 큰 단점을 보완하기 위해 Promise가 나왔다.
그렇다면 이전 포스트에서 사용했던 콜백 지옥 코드를 Promise를 이용해서 가독성이 좋게 만들어보자
우선 아래는 콜백 지옥이다.


이제 프로미스를 사용하여 이 지옥에서 벗어나보자.


보기만해도 말도 안되게, 굉장히 읽기 편해졌다.
프로미스를 이용하여 햄버거만들기와 햄버거포장 함수를 만들 때도
단순히 new Promise() 에 전달하는 콜백함수에 모든 로직을 집어넣기만 하면 된다.
그리고 성공, 실패 함수를 햄버거만들기와 포장 함수에 전달할 필요가 없어졌다.
res(resolve), rej(reject) 가 그 역할을 대신 할테니까
위의 프로미스로햄버거만들기 호출 코드만 보고도 로직을 쉽게 유추할 수 있게되었다.
- 블랙바비큐 콰트로치즈 버거를 만들기 위해 햄버거만들기 함수를 호출하는 구나
- 햄버거만들기가 완성되면 햄버거를 포장하는구나
- 햄버거를 전부 포장하면 햄버거 포장완료 했다고 알려주는 구나.
- 혹시나 중간에 잘못되면 잘못되는 이유를 알려주는구나.
콜백 지옥에서 벗어나서 속이 시원하다 ㅠㅠ
사실 프로미스 다음으로 async/await을 배우는 걸 아는 입장에서
프로미스로는 부족한가? 라는 궁금증이 생긴다.
사실 위의 코드에서 then() 에게 전달하는 콜백이 매우 간단하니까 괜찮아보이지
then()도 콜백지옥 처럼 체이닝이 길어지면 길어질수록 가독성이 떨어질 수 밖에 없다.
하지만 async/await 은 프로미스보다도 가독성이 더 뛰어날 수 있다.
어떻게 뛰어날 수 있을까?
그것을 다음 포스트에서 공부해본다.