Promise

Jinseong Park·2025년 6월 10일
post-thumbnail

콜백 지옥

자바스크립트에서 콜백 함수는 함수에 인수로 전달되어 함수 내부 동작에서 특정 조건에 맞춰 사용되며, 특히 비동기 함수에서 특정 시점에 맞춰 다양한 동작을 하는데 유용하게 쓰인다.

하지만 콜백 함수로만 작성하면 코드의 가독성이 떨어지는 경우가 발생한다.

다음 코드를 보자.

function operator(a, b, callback) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof a === Number && typeof b === Number) {
        resolve(callback(a, b));
      } else {
        reject(a, b);
      }
    }, 2000);
  });
}

const promise = operator(1, 2, (a, b) => a + b);
promise.then((value) => {
  console.log(value);

  const promise = operator(value, 4, (a, b) => a * b);
  promise.then((value) => {
    console.log(value);

    const promise = operator(value, 5, (a, b) => a - b);
    promise.then((value) => {
      console.log(value);

      const promise = operator(value, 3, (a, b) => a * b);
      promise.then((value) => {
        console.log(value);

        //...
      });
    });
  });
});

여기서 비동기 함수의 결과를 가지고 콜백 함수를 실행하려고 하면, 콜백 함수를 계속 추가하면서 indent(들여쓰기)가 깊어지며 가독성이 안 좋아진다.

만약에 위와 같은 방식이 15개 정도 추가된다고 하면 불편함이 좀 다가올 것이다.

이를 해결하기 위해 Promise를 사용할 수 있다.

Promise 객체

Promise 객체는 생성자에 인자로 전달된 비동기 함수의 결과를 가진다.

const promise = new Promise(executor);

여기서 생성자에 전달되는 비동기 함수를 “실제 비동기 작업을 실행하는 함수”라는 의미로 executor라고 부른다.

Promise 객체는 Pending(대기), Fulfulled(완료), Rejected(거절)이라는 3가지 상태를 가진다.

Pending(대기)는 executor의 결과를 기다리고 있는 상태,

Fulfilled(완료)는 executor가 성공적으로 수행되어 결과값을 받은 상태,

Rejected(거절)는 executor가 예상치 못한 에러를 만나 정상적으로 수행되지 않은 상태이다.

여기서 executor가 성공적으로 수행되었을 때(Pending → Fulfilled)를 resolve라고 표현하고,

실패했을 때(Pending → Rejected)를 reject라고 표현한다.

executor는 두가지 인자를 가지는데, 순서대로 resolve, reject이다.

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

resolve 함수를 실행하면 해당 Promise 객체의 상태는 fulfilled가 되고 인수로 넘겨준 값을 결과값으로 갖는다.

아래는 2초 후에 실행되는 비동기 함수의 결과를 기다리고 Promise 객체를 출력하기 위해 3초 후에 객체를 출력하는 코드다.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("값");
  }, 2000);
});
// 2초 후에 resolve된 Promise 객체를 출력하기 위해 3초 기다림
setTimeout(() => {
  console.log(promise);
}, 3000);

reject 함수를 실행하면 Promise 객체의 상태는 rejected가 되고 인자로 넘겨준 값을 결과값을 갖는다.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("error message");
  }, 2000);
});
// 2초 후에 reject된 Promise 객체를 출력하기 위해 3초 기다림
setTimeout(() => {
  console.log(promise);
}, 3000);

콘솔 창에서 보이는 것과 같이 직접 전달한 값과 에러메시지가 출력되는 것을 확인할 수 있다.

하지만 이렇게 3초 뒤에 실행하는 건, 비동기 함수에 2초 후에 실행된다는 것을 알고 있어서 가능한 로직이다.

실제로는 Promise가 언제 resolve or reject 되는지 모르기 때문에 결과를 기다리고 Promise 객체의 상태가 변경되면 결과값을 처리해야 할 무언가가 필요하다.

then, catch 메서드

then과 catch는 Promise 객체의 메서드이다. then 메서드는 두 개의 인수를 받는데 첫 번째는 resolve 일 때의 콜백 함수이고, 두 번째는 reject 일 때의 콜백 함수이다.

여기서 두 번째 콜백 함수는 생략해도 된다.

그리고 catch 메서드의 인수는 reject 일 때의 콜백 함수여서, then 메서드에서 두 번째 인수만 전달된 것의 압축된 표현이라고 할 수 있다.

그래서 위에서 실행한 코드를 바꿔서 결과값을 출력하는 코드로 수정하면,

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("값");
  }, 2000);
})

promise.then((value) => {
    console.log(value);
  });
  
promise.catch((error) => {
    console.log(error);
  });

여기서 정말 편리한 점이 있는데, 바로 then과 catch는 Promise 객체를 반환한다는 점이다.

이 말이 무슨 말이냐면, then이나 catch 실행 후 뒤에 다시 then과 catch를 바로 실행할 수 있다.

promise.then((value) => {
    console.log(value);
  }).catch((error) => {
    console.log(error);
  });

이를 체인처럼 연속해서 표현한다고 해서 체이닝이라고 말한다.

콜백 지옥 제거

그래서 처음에 만난 콜백 지옥을 Promise를 사용해 제거해보자.

// promise를 사용한 코드
function operator(a, b, callback) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof a === Number && typeof b === Number) {
        resolve(callback(a, b));
      } else {
        reject(a, b);
      }
    }, 2000);
  });
}

const promise = operator(1, 2, (a, b) => a + b);
promise.then((value) => {
  console.log(value);

  const promise = operator(value, 4, (a, b) => a * b);
  promise.then((value) => {
    console.log(value);

    const promise = operator(value, 5, (a, b) => a - b);
    promise.then((value) => {
      console.log(value);

      const promise = operator(value, 3, (a, b) => a * b);
      promise.then((value) => {
        console.log(value);

        //...
      });
    });
  });
});
// 이전 코드
function operator(a, b, callback) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof a === Number && typeof b === Number) {
        resolve(callback(a, b));
      } else {
        reject(a, b);
      }
    }, 2000);
  });
}

const promise = operator(1, 2, (a, b) => a + b);
promise.then((value) => {
  console.log(value);

  const promise = operator(value, 4, (a, b) => a * b);
  promise.then((value) => {
    console.log(value);

    const promise = operator(value, 5, (a, b) => a - b);
    promise.then((value) => {
      console.log(value);

      const promise = operator(value, 3, (a, b) => a * b);
      promise.then((value) => {
        console.log(value);

        //...
      });
    });
  });
});

뭔가 이상하다.

문제가 해결되려면 indent가 들어가지 않고 가독성이 좋아져야 하지만 그렇지 않다.

이를 해결하기 위해 제대로 Promise를 쓰려면 위에서 메서드가 객체를 반환한다는 것을 이용해야 한다.

그래서 다시 수정하면,

const promise = operator(1, 2, (a, b) => a + b);
promise
  .then((value) => {
    console.log(value);

    return operator(value, 4, (a, b) => a * b); // Promise 객체를 반환해서 체이닝
  })
  .then((value) => {
    console.log(value);

    return operator(value, 5, (a, b) => a - b);
    promise;
  })
  .then((value) => {
    console.log(value);

    return operator(value, 3, (a, b) => a * b);
    promise;
  })
  .then((value) => {
    console.log(value);

    //...
  });

이렇게 콜백 지옥을 해결하고 더 나은 가독성을 확보했다.

Promise는 실제로 비동기 함수를 처리할 때 많이 사용하기도 하고, then 메서드 내부에서 Promise 객체를 반환하는 방식은 다른 코드에서도 가독성을 올리는데 유용하게 사용 할 수 있을 것 같다.

[참고]

https://www.inflearn.com/courses/lecture?courseId=328340&unitId=210869

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise

profile
헤맨 만큼 내 땅이다

0개의 댓글