자바스크립트 동기와 비동기 3 - 프로미스(Promise)

RN·2024년 6월 24일

자바스크립트

목록 보기
4/11
post-thumbnail

이전 포스트에서 콜백 함수의 콜백 지옥을 보았다.

이 콜백 지옥을 해결하기 위해 프로미스를 사용한다고 하였다.

프로미스에 대해서 알아보도록 하자.

1. 프로미스(Promise)


1.1 프로미스 사용법

처음 보는 사람이라면 그리고 자바스크립트에 아직 익숙치 않다면 구조가 좀 어지러울 수도 있다.

new Promise로 생성하는 프로미스 객체의 인터페이스를 보자.

(interface는 타입스크립트에서 클래스/객체의 구조를 정의하는 키워드인데, 추상자료형이라고 생각하면 편하다.)

주석의 첫 번째 줄을 보면 프로미스에는 프로미스가 완료(fulfilled 또는 rejected) 될 때 호출되는 콜백 함수를 붙여라 라고 되어있다.

fullfilled 와 rejected 는 프로미스의 상태인데, 아래의 표를 보자.

statedescriptioncallback
pending작업이 아직 실행 중이며 Promise가 아직 보류 중-
fulfilled작업이 완료되었으며 성공.then()
rejected작업이 완료되었지만 오류가 발생.catch()
settledPromise가 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에 전달해준 값이다.

이제 저 위의 코드를 이해할 수 있게됐다.

  1. new Promise() 객체를 생성하면서 동시에 전달한 콜백 함수(작업)를 실행한다.

  2. 콜백 함수(작업)가 성공적으로 완료되었다면, resolve() 함수를 실행했다면 .then의 콜백 함수를 실행하며, 성공적으로 완료되지 않았다면 reject() 함수를 실행하여 .catch의 콜백 함수를 실행한다.

  3. finally에 전달된 콜백 함수를 실행한다.



1.1.1 또 다른 사용법


문단이 길어지는 거 같아 분리하였다.

위에서 프로미스 객체를 생성하는 순간 작업이 시작하고, 작업의 결과에 따라 .then .catch 를 실행한다고 했다.

프로미스 객체를 생성하기만 하면 되기에 이런 방법으로도 사용할 수 있다.

이렇게 함수에서 반환과 동시에 Promise를 생성하면 프로미스.then() 이 아니라 프로미스().then()을 이용해서 사용할 수 있다.

이것 역시 위의 코드와 같고 결과는 아래와 같다.

두 번 실행해서 값을 뽑아낸 것이다.

보통은 Promise를 사용할 때 이렇게 함수로 감싸서 사용하는 편이다.

변수에 저장할 때와 달리 함수로 감쌀 때의 장점이 있다.

  • 함수로 감싼다면 우리가 원할때마다 호출해서 재사용성이 높아진다.

  • 함수로 감싼다면 매개변수를 전달할 수 없는 변수에 저장할 때랑 달리 함수를 호출할 때 마다 필요한 매개변수를 전달할 수 있어 동적인 프로그래밍이 가능해진다.



1.2 프로미스 상태(Promise State)


프로미스 객체는 3가지의 상태를 가진다는 것을 알았다.

위의 표를 다시 가져와보자.

statedescriptioncallback
pending작업이 아직 실행 중이며 Promise가 아직 보류 중-
fulfilled작업이 완료되었으며 성공.then()
rejected작업이 완료되었지만 오류가 발생.catch()
settledPromise가 resolved(해결) 되어지든 rejected(거부) 되어지든 이 콜백을 호출.finally()

다시 말하지만 프로미스 객체는 실제로 pending, fulfiiled, rejected의 3가지 상태만을 가진다.

이 상태는 아래와 같이 확인할 수 있었다.

이렇게 .then() 을 연결시키지 않고 곧바로 반환된 프로미스 객체를 출력하면
현재의 상태를 알 수 있다.

왜인지 Fulfilled 상태는 출력되지 않았지만 pending과 rejected가 출력되어서 다행이었다.

만약 프로미스가 정상적으로 동작하지 않을 경우 이 상태를 확인함으로써 오류를 고쳐나갈 수 있을 것 같다.




1.3 프로미스 체이닝(Promise Chaining)


프로미스를 이용하여 .then() .catch() 등등을 호출했다.

그런데 놀랍게도 이 .then()을 체이닝, 즉 연쇄적으로 연결시킬 수 있었다.

위의 코드를 한 번 천천히 읽어보자

  1. 프로미스1() 이 성공적으로 1을 반환했다. res(1)
  2. 그래서 첫 번째 .then()에 전달된 콜백인 num => return sum += num 을 실행한다.
  3. 두 번째 .then()의 인자인 num 은 첫 번째 .then() 에서 반환한 sum 이다.
  4. 세 번째 .then() 역시 두 번째 .then()의 반환값을 받아와서 sum += num을 계산한다.
  5. 최종 결과 값을 출력한다.

여기서 알 수 있는 점은 .then() 이 반환하는 값을 다음 .then() 에서 사용할 수 있다는 점이다.

그리고 그 값은 .then((num) => {return sum += num}) 에서 붉게 표시한 부분이다.


아래처럼 체이닝도 가능하더라.

공부를 하다보니 새로운 걸 하나 또 알게됐다.

프로미스에 관한 이야기는 아니지만 위에서 .then() 을 보면 콜백함수가 아니라 함수명만 전달했다.

화살표 함수는 만약

.then((data) => 프로미스2(data)) 라면

.then(프로미스2) 로 생략할 수 있다.

즉, 하나의 함수만 호출하고 그 함수의 인자가 화살표 함수의 인자라면 생략이 가능하다.

어쨌든 위의 코드를 보자.

  1. 프로미스1() 을 호출했고 1을 성공적으로 반환했다. res(1)
  2. 첫 번째 then에서 프로미스1 에서 반환받은 1을 받아서 2초후에 1(num) + 2 를 반환한다.
  3. 두 번째 then에서 프로미스2 에서 반환받은 3을 받아서 3(num) + 3 을 반환한다.
  4. 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 은 반드시 프로미스 객체에만 연결할 수 있다.




1.4 이제 콜백 지옥에서 벗어나보자.


콜백지옥이라는 콜백 함수의 큰 단점을 보완하기 위해 Promise가 나왔다.

그렇다면 이전 포스트에서 사용했던 콜백 지옥 코드를 Promise를 이용해서 가독성이 좋게 만들어보자

우선 아래는 콜백 지옥이다.

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

보기만해도 말도 안되게, 굉장히 읽기 편해졌다.

프로미스를 이용하여 햄버거만들기와 햄버거포장 함수를 만들 때도

단순히 new Promise() 에 전달하는 콜백함수에 모든 로직을 집어넣기만 하면 된다.

그리고 성공, 실패 함수를 햄버거만들기와 포장 함수에 전달할 필요가 없어졌다.
res(resolve), rej(reject) 가 그 역할을 대신 할테니까


위의 프로미스로햄버거만들기 호출 코드만 보고도 로직을 쉽게 유추할 수 있게되었다.

  1. 블랙바비큐 콰트로치즈 버거를 만들기 위해 햄버거만들기 함수를 호출하는 구나
  2. 햄버거만들기가 완성되면 햄버거를 포장하는구나
  3. 햄버거를 전부 포장하면 햄버거 포장완료 했다고 알려주는 구나.
  4. 혹시나 중간에 잘못되면 잘못되는 이유를 알려주는구나.

콜백 지옥에서 벗어나서 속이 시원하다 ㅠㅠ




1.5 프로미스가 최선인가?


사실 프로미스 다음으로 async/await을 배우는 걸 아는 입장에서

프로미스로는 부족한가? 라는 궁금증이 생긴다.

사실 위의 코드에서 then() 에게 전달하는 콜백이 매우 간단하니까 괜찮아보이지

then()도 콜백지옥 처럼 체이닝이 길어지면 길어질수록 가독성이 떨어질 수 밖에 없다.

하지만 async/await 은 프로미스보다도 가독성이 더 뛰어날 수 있다.

어떻게 뛰어날 수 있을까?

그것을 다음 포스트에서 공부해본다.

0개의 댓글