[JS] callback 그리고 Promise를 사용하는 이유(2)

jiseong·2022년 5월 20일
3

T I Learned

목록 보기
250/291

이전 포스팅에서 비동기성을 표현하고 동시성을 관리하기 위해 콜백을 사용하는 것과 관련하여 두 가지 주요 범주의 결함을 확인했었다. (순서와 믿음의 결여)

가장 먼저 제어의 역전(신뢰성)을 생각해보자.

이전에는 서드파티 라이브러리함수와 같은 다른 파트에게 콜백함수를 전달해주고 이것이 잘 작동하기만을 바라는 방법밖에 없었다. 그런데 만약 우리가 이런 제어권을 다시 되찾아 올 수 있다면 어떨까? 프로그램의 진행을 다른 파트에게 넘겨주는 대신, 개발자가 작업이 언제 완료되었는지 알 수만 있고 그 이후에 무엇을 할지를 결정할 수 있다면 어떨까?

이 패러다임으로 인해 나타난 것이 Promise 인 것이다.

You don't Know JS에서 보여줬던 예시상황

햄버거 가게에 가서 치즈버거를 주문하고 결제한다. 그러나 종종 치즈버거는 바로 나오지 못할수도 있기 때문에 캐셔는 우리에게 주문번호가 적혀있는 영수증을 준다. 이 주문번호는 최종적으로 내가 치즈버거를 받을 수 있도록 보증하는 Promise(약속)인 것이다.

그래서 나는 즐거운 마음으로 영수증을 손에 쥐고 순서를 기다리면서 다른일을 할 수도 있다.
기다리다보면 결국 캐셔가 나의 번호를 부르면 영수증에 건내주고 그 대가로 치즈버거를 받을 수 있다.

그런데... 치즈버거가 다 떨어져 받지 못하는 실패의 상황이 있을 수 있다. 슬프겠지만 잠시 감정은 숨겨두고, 우리는 future values이 성공 또는 실패로 나뉜다는 것을 알 수 있었다.

순서

다음의 예시를 봐보자.

function add(xPromise, yPromise) {
  return Promise.all([xPromise, yPromise]).then(function (values) {
    return values[0] + values[1]
  });
}

add(fetchX(), fetchY()).then(function (sum) {
  console.log(sum)
});

여기에는 두 계층의 프로미스가 존재한다.

먼저 fetchX()와 fetchY()를 직접 호출하여 이들의 반환 값(프로미스)을 add()에 전달한다. 두 프로미스는 원래 값은 지금 아니면 나중에 준비되겠지만 각각의 프로미스들은 시점과는 상관없이 동일한 결과를 정규화한다. 그래서 시간 독립적(time-independent) 방식으로 X와 Y값에 대해 추론할 수 있는 것이다.

그다음 계층으로 add()가 만들어 반환한 프로미스로 then()을 호출하고 대기(pending)한다. 이후에 add()작업이 완료되면 덧셈을 마친 future value이 준비되어 출력할 수 있게 된다. (여기서 add()안에 X와 Y의 미래 값을 기다리기 위한 로직이 숨겨져있다.)

add(fetchX(), fetchY()).then(
  // fullfillment handler
  function (sum) {
    console.log(sum)
  },
  // rejection handler
  function (err) {
    console.error(err)
  }
)

프로미스를 사용하면 then 함수에 첫번째 인자로 fulfillment함수(resolve()로 생각해도 무방할 것 같다.), 두번째 인자로 rejection함수(마찬가지로 reject())를 넘겨받는다. 치즈버거의 예시상황과 마찬가지로 성공과 실패에 대해서 다루고자 하는 것인데 X 또는 Y를 가져오는 동안에 오류가 발생할 경우 rejected되면서 두번째 인자로 전달된 콜백 에러 핸들러가 프로미스로부터 거부 값을 수신하게 된다. (반대로 fulfilled의 경우에는 첫번째인자로)

프로미스는 시간 의존적인(time-dependent) 상태를 외부로부터 캡슐화하기 때문에 프로미스 자체는 시간 독립적(time-independent)이게 되면서 타이밍 또는 결과에 상관없이 예측 가능한 방식으로 구성할 수 있다.

게다가 프로미스는 한번 귀결되면 그, 프로미스는 영원히 유지(불변성, immutable)되기 때문에 필요할때마다 꺼내 쓸 수 있다.

믿음

이전 포스팅에서 우리가 봤던 콜백에서 일어날 수 있는 경우들이다.

  • 콜백을 너무 일찍 부르는 경우
  • 콜백을 너무 늦게 부르는 경우
  • 콜백을 너무 적게 또는 많이 부르는 경우
  • 발생할지 모르는 에러/예외를 무시하는 경우

콜백을 너무 일찍 부르는 경우, 콜백을 너무 늦게 부르는 경우

때때로 동일한 task가 어떠한 경우에는 동기적으로 수행되고 어떠한 경우에는 비동기적으로 수행되서 발생하는 경우이다.

프로미스는 then()을 호출할 때 이미 해당 task가 귀결되었더라도 그 때 제공하는 콜백은 항상 비동기식으로 호출되도록 설계되어 있기 때문에 너무 일찍 부르는 경우는 발생하지 않는다.

(그래서 setTimeout(..,0)와 같은 작업들이 더이상 필요하지 않는다.)

또한 프로미스가 귀결되면 then()에 등록된 모든 콜백들이 그 다음 비동기 기회가 찾아왔을 때 순서대로 호출되며 이러한 콜백 중 하나에서 발생하는 어떤 것도 다른 콜백의 호출에 영향을 미치거나 지연시킬 수 없다.

p.then(function() {
  p.then(function() {
    console.log('C');
  });
  console.log('A');
});

p.then(function() {
  console.log('B');
});
// A B C

즉, 다음과 같은 코드에서 C가 B보다 먼저 호출될일은 없다는 것이다.

콜백을 너무 적게 또는 많이 부르는 경우

먼저 한번도 콜백을 호출하지 않는 경우이다.
기본적으로 프로미스는 fulfill, reject 콜백이 프라미스에 모두 등록된 상태라면 프라미스 귀결 시 둘 중 하나는 반드시 호출된다. 그런데 만약 중간에 네트워크 상태가 좋지않거나 로직이 잘못 작성되어있는 경우 등등, 여러가지 이유로 프로미스가 귀결되지 않는 경우가 발생할 수도 있다.

이 경우 race라고 불리는 더 높은 수준의 추상화를 사용하면 된다. 다음과 같이 Promise.race()를 사용하면 어느 한쪽으로 귀결되지 않는 상황을 해결할 수 있게 된다. 그렇기 때문에 무조건 한번은 불리게 되는 것이다.

function timeoutPromise(delay) {
  return new Promise( function(res, rej) {
    setTimeout(function() {
      reject('타임아웃!')
    }, delay)
  })
};

Promise.race([
  foo(),
  timeoutPromise(3000)
]).then(
  function() {
    // foo가 제시간안에 fullfilled 된 경우
  },
  function(err) {
    // foo가 rejected 되었거나 시간안에 끝내지 못한 경우
    // 'err'을 확인하여 원인 파악
  }
)

다음은 너무 많이 호출되는 경우이다.

프로미스는 정의상 단 한번만 호출되도록 되어있다. 어떤 이유로든 resolve 또는 reject가 여러번 호출되거나 둘다 호출하려고 하면 최초의 귀결만 취하고 이후의 시도는 조용히 무시된다.

발생할지 모르는 에러/예외를 무시하는 경우

어떤 이유로 프로미스를 거부되면 그 값은 rejection callback에 전달된다. 또한 프로미스의 생성 또는 귀결을 기다리는 도중 TypeError 와 ReferenceError와 같은 JS 실행 에러가 발생하더라도 예외를 포착하여 거부시켜버린다.

var p = new Promise(function(resolve, reject) {
  foo.bar(); // foo는 정의되어있지 않아 error가 발생한다!
  resolve(42); // 여기까지 도달할 수 없다 :(
})

p.then(
  function fulfilled() {
    // 실행되지 않는다.
  },
  function rejected(err) {
    // 'foo.bar()' 에서 발생한 에러로 'err'는 타입 에러일 것이다.
  }
)

정리하면 Promise는 callback에서 가졌던 순서, 믿음 문제를 보장하는 고마운 존재이며 JS/DOM에 추가되는 대부분의 새로운 비동기 API는 Promise를 기반으로 구축되는 만큼 Promise를 믿고 사용해도 좋을 것 같다.


Reference

0개의 댓글