[JS] Promise

Minyoung's Conference·2022년 6월 20일
0

JavaScript

목록 보기
14/28
post-thumbnail

콜백지옥을 아는가? 걱정마라 Promise가 있다.

Promise는 자바스크립트의 비동기를 돕는 객체이다. 쉽고 빠르고 직관적이다.

먼저, 비동기 작업이 가질 수 있는 상태에 대해 알아보자.

Pending는 현재 비동기가 진행중이거나, 작업이 시작할 수 없는 문제적 상황을 이야기 한다.
Fulfilled는 우리가 의도한대로 비동기 작업이 정상적으로 이루어졌음을 이야기한다.
Rejected는 비동기 작업이 어떠한 이유로 실패했음을 의미한다. (ex.서버응답x, 자동취소 등)

비동기 작업이 성공했다는 것은 "resolve되었다." 비동기 작업이 실패했다는 것은 "reject되었다." 라고 이해하면 된다.

그렇다면 resolve 작업과 reject 작업을 한 번 수행해보자.

2초뒤에 전달받은 값이 양수인지 음수인지 판별하는 비동기 함수를 만들어보자.
(결과값을 Response의 축약어로 res, Error의 축약어로 err을 쓰겠다.)

function isPositive(number, resolve, reject) {
  setTimeout(() => {
    if (typeof number === "number") {
      // 성공 -> resolve
      resolve(number >= 0 ? "양수" : "음수");
    } else {
      // 실패 -> reject
      reject("주어진 값이 숫자형 값이 아닙니다.");
    }
  }, 2000);
}

isPositive(
  10,
  (res) => {
    console.log("성공적으로 수행함 : ", res);
  },
  (err) => {
    console.log("실패 하였음 : ", err);
  }
);
// 성공적으로 수행됨 : 양수
isPositive(
  [],
  (res) => {
    console.log("성공적으로 수행함 : ", res);
  },
  (err) => {
    console.log("실패 하였음 : ", err);
  }
);
// 실패 하였음 : 주어진 값이 숫자형 값이 아닙니다.

콜백 함수를 이용해 비동기 함수의 성공, 실패를 핸들링했다. 이번에는 Promise를 이용해서
위의 비동기 함수를 다시 한 번 만들어보겠다.

function isPositiveP(number) {
  const executor = (resolve, reject) => {
    setTimeout(() => {
      if (typeof number === "number") {
        // 성공 -> resolve
        resolve(number >= 0 ? "양수" : "음수");
      } else {
        // 실패 -> reject
        reject("주어진 값이 숫자형 값이 아닙니다.");
      }
    }, 2000);
  };

실행자라는 의미의 executor라는 함수를 만들어주었고, 그 안에 비동기함수 setTimeout()을 넣었다.
isPositiveP와 isPositive의 차이는 Promise의 유무밖에 없다.

isPositiveP 함수를 Promise로 실행시키는 것은 아주 간단하다.

function isPositiveP(number) {
  const executor = (resolve, reject) => {
    setTimeout(() => {
      if (typeof number === "number") {
        // 성공 -> resolve
        console.log(number);
        resolve(number >= 0 ? "양수" : "음수");
      } else {
        // 실패 -> reject
        reject("주어진 값이 숫자형 값이 아닙니다.");
      }
    }, 2000);
  };

  const asyncTask = new Promise(executor);
  return asyncTask;
}
// 101

비동기 작업 그 자체인 Promise를 저장할 asyncTask라는 상수를 만들어주면 된다.
new 키워드를 사용해 Promise 객체를 생성하고 Promise 객체의 생성자로 비동기 함수의 실질적인
실행자 함수인 executor를 넘겨주게 되면 자동으로 바로 실행이 된다.
(실행이 되는지 확인하기 위해 console.log(number)를 넣어주었고, 101이라는 결과를 얻었다.)

그리고 리턴을 넣어주었기 때문에 isPositiveP라는 함수를 확인해보면 반환값이 Promise인 것을 확인할 수 있다.

함수가 Promise라고 적혀 있다면, 그 함수는 비동기 작업을 하고 작업 결과를 promise객체로 반환받아서 사용할 수 있다고 이해하면 된다.

const res = isPositiveP(101);

함수를 res라는 상수에 저장해보자.
res는 asyncTask 라는 프로미스 객체를 반환받고 있는 상수이기에
비동기 처리에 대한 resolve, reject를 아무대서나 사용할 수 있다.

res.then(()=>{}).catch(()=>{})

위와 같이, res, then, 콜백함수, catch, 콜백함수 이렇게 사용하면 된다.
그리고 콜백함수 내에 res와 err를 넣어주자.

res
  .then((res) => {
    console.log("작업 성공 :", res);
  })
  .catch((err) => {
    console.log("작업 실패 :", err);
  });

위처럼 코드를 작성하면 resolve에 전달한 함수를 then에서 그대로 받아오는 것을, reject에 전달한 함수는 catch 메소드에서 그대로 받아오는 것을 알 수 있다.

그러면 이전에 만들었던 콜백지옥 함수를 Promise를 사용해 변경해보자.
이전 콜백 지옥 함수는 아래와 같다.

function taskA(a, b, cb) {
  setTimeout(() => {
    const res = a + b;
    cb(res);
  }, 3000);
}

function taskB(a, cb) {
  setTimeout(() => {
    const res = a * 2;
    cb(res);
  }, 1000);
}

function taskC(a, cb) {
  setTimeout(() => {
    const res = a * -1;
    cb(res);
  }, 2000);
}

taskA(3, 4, (a_res) => {
  console.log("task A :", a_res);
  taskB(a_res, (b_res) => {
    console.log("task B :", b_res);
    taskC(b_res, (c_res) => {
      console.log("task C :", c_res);
    });
  });
}); 

// 3초 뒤 task A : 7
// 4초 뒤 task B : 14
// 6초 뒤 task C : -14

여기서 함수 실행을 일단 주석처리하고 taskA에 Promise 객체를 만들어준다.
그리고 Promise 객체 내부에 executor함수의 인자였던 resolve, reject를 넣어주고 setTimeout 함수를 넣어준다. (아래와 같다.)

function taskA(a, b, cb) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = a + b;
      cb(res);
    }, 3000);
  });
}

헷갈리는 경우, 위의 taskA함수는 아래의 함수와 같다.

function taskA(a, b, cb) {
  const executor = (resolve, reject) => {
    setTimeout(() => {
      const res = a + b;
      cb(res);
    }, 3000);
  };

  return new Promise(executor);
}

taskB, taskC도 마찬가지로 바꾸어주자.

function taskA(a, b) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = a + b;
      resolve(res);
    }, 3000);
  });
}

function taskB(a) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = a * 2;
      resolve(res);
    }, 1000);
  });
}

function taskC(a) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = a * -1;
      resolve(res);
    }, 2000);
  });
}

// taskA(3, 4, (a_res) => {
//   console.log("task A :", a_res);
//   taskB(a_res, (b_res) => {
//     console.log("task B :", b_res);
//     taskC(b_res, (c_res) => {
//       console.log("task C :", c_res);
//     });
//   });
// });

위와 같이, Promise 객체로 반환한다는 것은 비동기로 동작하고 내부 함수를 then과 catch로 이용할 수 있도록 만든다는 의미이다. 그럼 위 주석처리 되어있는 콜백함수들을 바꾸면 어떻게 되는지 확인해보자.

taskA(3, 4, (a_res) => {
  console.log("A RESULT :", a_res);
  taskB(a_res, (b_res) => {
    console.log("B RESULT :", b_res);
    taskC(b_res, (c_res) => {
      console.log("C RESULT :", c_res);
    });
  });
});

(변화 전)

taskA(5, 1).then((a_res) => {
  console.log("A RESULT :", a_res);
  taskB(a_res).then((b_res) => {
    console.log("B RESULT :", b_res);
    taskC(b_res).then((c_res) => {
      console.log("C RESULT :", c_res);
    });
  });
});

차이점이 큰가? 아니다. 콜백지옥 모양과 똑같다. 사실 Promise 객체와 then을 이런식으로 쓰지 않는다. 바꿔서 사용해보자.

taskA(5, 1).then((a_res) => {
    console.log("A RESULT : ", a_res);
    return taskB(a_res);
  })

위 함수는 then 콜백함수가 수행이 되면서 마지막에 taskB를 호출해서 그 결과값을 반환한다. 말로 설명해본다면, taskA에 (5,1)의 값을 전달하면 A의Promise 객체로 전달받고 A의 Promise 객체에 then 메소드를 전달해서 taskB를 호출한다. 결국 위 함수는 taskB를 호출해서 반환받은 B의 Promise 객체이다. 그래서 바로 then을 또 붙여주면 taskB의 결과값을 이용해서 그 내부의 코드를 다시 수행할 수 있게된다. 그러면 taskC까지 then을 붙여보자.
(중요한 것은 then을 지속적으로 붙여주면서 사용한다는 것이다.)

taskA(5, 1)
  .then((a_res) => {
    console.log("A RESULT : ", a_res);
    return taskB(a_res);
  })
  .then((b_res) => {
    console.log("B RESULT : ", b_res);
    return taskC(b_res);
  })
  .then((c_res) => {
    console.log("C RESULT : ", c_res);
  });

then체이닝으로 위와 같이 늘여서 쓰는 것이다.
taskA의 결과값인 a_res를 인자로 전달한 taskB에 then 메소드를 이용하고 그 결과값인 b_res를 인자로 전달한 taskC에 then 메소드를 이용해 그 결과값인 c_res를 호출하는 방식이다.

Promise를 사용하면 비동기로 반복되는 콜백함수를 아래로 늘여서 쓸 수 있다.
또한, then 사이에 다른 작업을 끼워 넣을 수도 있다.

const bPromiseResult = taskA(5, 1)
  .then((a_res) => {
    console.log("A RESULT : ", a_res);
    return taskB(a_res);
  });
console.log('hello');
console.log('hello');
console.log('hello');

  bPromiseResult.then((b_res) => {
    console.log("B RESULT : ", b_res);
    return taskC(b_res);
  })
  .then((c_res) => {
    console.log("C RESULT : ", c_res);
  });

위 코드는 중간에 console.log('hello') 만 추가한 동일한 코드이다.
다른 작업을 중간에 넣어 수행할 수 있음을 보여준다.
Promise를 사용하면 비동기 함수를 호출하는 코드와 결과를 처리하는 코드를 분리할 수 있기 때문에 가독성이 높은 깔끔한 코드를 작성할 수 있다.

Promise의 기본에 대해 알아보았다.

Promise 개념 및 예제 추가

// Promise is a JavaScript object for asynchronous operation.
// State : pending -> fulfilled or rejected
// Producer vs Consumer

1. Producer
// when new Promise is created, the executor runs automatically.

const promise = new Promise((resolve, reject) => {
    //doing some heavy work (network, read files)
    console.log("doing something")
    setTimeout(()=>{
        // 1. resolve("ellie")
				// 2. reject(new Error('no Network'))
    },2000)
})

// resolve, reject를 넣어줘야함. 안넣어주면 pending 상태로 남음
// async 사용시 resolve, reject 안넣어줘도 됨

//2초 후에 콜백함수가 resolve로 동작

2. Consumer : then(성공케이스), catch, finally

promise.then((value) => {
    console.log(value); 
		// 1번 resolve가 수행되는 경우, 결과값은 ellie
		// 2번 reject가 수행되는 경우, 결과값은 Error : no Network
})
// promise가 잘 수행된다면(then), value를 받아오겠다.
// promise가 reject되었기에, catch에서 불러온다. 

promise
	.then((value)=>{
		console.log(value)
	})
	.catch((error)=>{
		console.log(error)
	})
	.finally(()=>{
		console.log('finally')
	})
then() : fulfilled의 경우 실행
catch() : rejected의 경우 실행
finally() : fulfilled, rejected 여부 관계없이 마지막에 실행

3. Promise chaining
const fetchNumber = new Promise((resolve, reject)=>{
    setTimeout(()=>{
        resolve(1)
    },1000)
});

fetchNumber
    .then(num => num * 2)
    .then(num => num * 3)
    .then(num => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(num - 1)
            },1000)
        })
    })
    .then(num => console.log(num))  // 2초뒤 5


awaitasync 내에서만 사용가능

Promise.all()
- 불필요한 대기시간 제거 
- 여러 개의 Promise를 동시에 실행시키고 모든 Promise가 완료되면 결과값을 배열로 반환
- 하나라도 실패시 rejected, 실패이유도 반환, 이전 성공도 무시 
- 여러개를 별도로 관리 힘듦 -> allSettled() 사용

Promise.allSettled()
- 여러 개를 별도로 관리하기 위해 사용
- 각 Promise는 아래의 형태로 객체가 배열에 담겨 반환
{status: "fulfilled", value : result}, {status : "rejected", reason : error} 

Promise.race() 
- 가장 먼저 처리되는 Promise결과(또는 에러)를 반환.
- 가장 먼저 처리되는 것을 제외하고 나머지는 무시.
profile
안녕하세요, FE 개발자 김민영입니다.

0개의 댓글