Skip to content

Latest commit

 

History

History
245 lines (183 loc) · 16.6 KB

Chapter3.md

File metadata and controls

245 lines (183 loc) · 16.6 KB

3.1 프라미스란?

개발자들은 새로운 것을 학습할 때 기술/패턴을 먼저 보는 경향이 있다. 하지만 API가 지양하고자 하는 것이 무엇인지 알고 쓰는 것과 아닌 것은 차이가 크고 그 차이를 느끼게 해주는 것이 프라미스다.

3.1.1 미랫값

예를 들어 식당에서 우리는 주문 후 영수증을 받게 된다. 이후 음식을 기다리는 과정에서 우리는 영수증에서 아직 받지 못한 음식이 나올 것이라고 사고 한다. 이 과정에서 영수증이 약속이고 음식이 미랫값이다. 하지만 음식의 제고가 떨어져 못 나오는 경우도 발생할 수 있듯이 미랫값은 성공 아니면 실패가 될 수 있다.

이와 같이 프라미스는 미래에 얻을 수 있는 값을 나타내며, 그 값은 아직 결정되지 않았을 수도 있습니다. 이 값이 결정되면 프라미스는 '이행' 상태 또는 '거부' 상태가 됩니다.

다시 코드로 돌아와서 우리는 개발을 할 때 항상 값이 미리 존재한다는 가정한 후 개발을 한다. 하지만 만약 존재하지 않으면 어떻게 될까?

var x, y = 2; 
console.log(x+y) // 4s

위의 코드에서 만약 x가 비동기 코드라면 c의 동작은 혼돈 그 자체일 것이다.

그럴 때 우리는 프라미스를 사용할 수 있다. 물론 콜백도 사용이 가능하지만, 코드가 매우 더러워진다. 프라미스는 항상 이룸( fulfillment) 아니면 버림(Rejection)으로 귀결될 수 있기에 좀 더 코드가 예상할 수 있는 방향으로 개발을 할 수 있다.

3.2 데너블 덕 타이핑

어떤 값이 진짜 프라미스인지 정말 프라미스처럼 동작하는지 어떻게 알 수 있을까?

  • 진짜 프라미스는 then() 매서드를 가진 데너블이라는 객체 는 함수를 정의하여 판별하는 걸로 규정되어 있다. then 매서드를 가지고 있으면 무조건 프라미스 규격에 맞는 것이다.
    • 평범한 객체더라도 then이라는 매서드를 가지게 되면 엔진이 데너블로 인식하여 특별한 규칙을 적용하기에 조심 해야 한다.
  • 덕타이핑은 어떤 값의 타입을 그 형태를 보고 짐작하는 타입 체크이다.

3.3 프라미스 믿음

콜백만 사용했을 때의 믿음성 문제점

  • 너무 일찍 콜백을 호출
  • 너무 늦게 콜백을 호출(또는 전혀 호출하지 않음)
  • 너무 적은 게 아니면 너무 많이 콜백을 호출
  • 필요한 환경/인자를 정상적으로 콜백에 전달 못 함
  • 발생할 수 있는 에러/예외를 무시

3.3.1 너무 빨리 호출

  • 자르고 현상: 같은 작업인데 어떨 때는 동기적으로, 어떨 때는 비동기적으로 끝나 결국 경합 조건에 이르게 되는 현상이다.

프라미스는 프라미스의 정의상 동기적으로 볼 수 는 없으니 이 문제는 영향받을 일이 없다.

따라서 프라미스의 then에 등록된 콜백은 항상 비동기적으로만 부른다.

3.3.2 너무 늦게 호출

프라미스가 귀결되면 then()에 등록된 콜백들이 그다음 비동기 기회가 찾아왔을 때 순서대로 실행되며 어느 한 콜백 내부에서 다른 콜백의 호출에 영향을 주거나 지연시킬 수는 없다.

 p.then(()=>{
	 p.then(()=>{
	 console.log(c) // c는 b보다 일찍 출력될 수 없다.
	 }
	 console.log(a)
 }
 console.log(b)

주의해야 할 점은 별개의 여러 프라미스 사이의 연쇄된 콜백사이의 상대적인 순서는 장담할 수 없기에 이런 방식의 코드는 피하는 것이 좋다.

3.3.3 한 번도 콜백을 안 호출

흔한 상황이고 프라미스로 해결할 수 있다.

경합이라는 상위 수준의 추상화를 이용하면 프라미스로 해결할 수 있다.

저자는 프라미스 타임아웃 패턴을 이용하여 해결하였는데 이 부분은 뒤에서 자세히 설명한다고 한다.

3.3.4 너무 가끔, 너무 종종 호출

  • 너무 가끔

    콜백의 호출 횟수는 1번인데 너무 가끔의 뜻은 0번 호출하는 것이기에 즉 한 번도 콜백을 안호출과 같다.

  • 너무 종종

    프라미스는 정의상 단 한 번만 귀결되기에 프라미스 생성 코드가 resolve() 나 reject() 중 하나 또는 모두를 여러 번 호출하려 할 때 최초의 귀결만 취하고 이후는 무시하게 된다.

3.3.5 인자/환경 전달 실패

프라미스 귀결 값은 딱 하나이고 명시적인 값으로 귀결되지 않으면 undefined가 된다.

주의할 점은 resolve(), reject() 함수를 부를 때 인자 두 번째 이후부터는 무시가 된다. 그래서 여러 개의 값을 넣고 싶으면 배열이나 객체를 사용해야 한다.

3.3.6 에러/예외 삼키기

프라미스가 생성중 또는 귀결을 기다리는 도중 에러가 발생한다면 예외를 잡아 주어진 프라미스를 강제로 버리게 된다.

여기서 발생한 에러는 프라미스 버림 콜백에서 잡아 대응이 가능하다.

3.3.7 미더운 프라미스?

Promise.resolve() 함수를 사용하면 값으로 이루어진 프라미스를 얻을 수 있는데. Promise.resolve() 를 거치게 되면 정규화를 하기에 안전한 결과를 얻을 수 있다.

3.4 연쇄 흐름

  • 흐름 제어를 연쇄 할 수 있는 프라미스 고유의 특징
    1. then()을 호출하면 그 결과 자동으로 새 프라미스를 생성하여 봔환한다.
    2. 이룸/버림 처리기 안에서 어떤 값을 반환하거나 예외를 던지면 이에 따라 새 프라미스가 귀결된다.
    3. 이룸/버림 처리기가 반환한 프라미스는 풀린 상태로 그 귀결 값이 무엇이든 간데 결국 현재의 then()에서 반환된 연쇄 프라미스의 귀결값이 된다.

하지만 then 과 function을 남발하는 것은 콜백에 비해 나아졌지만 여전히 문제이다. 2부 4장 제네레이터에서 순차적으로 표현하는 법을 알려준다.

3.4.1 용어 정의: 귀결, 이룸, 버림

var p = new Promise((X,Y)=>{
	//X() 는 이룸
	//Y() 는 버림 
})

이 책에서의 저자는 Promise의 콜백인자의 명칭으로 resolve와 refect를 추천했고 then에 제공할 콜백명으로는 fulfilled 와 rejected 라고 부르는 것을 추천하였다.

3.5 에러 처리

  • 동기적인 try ... catch 구문은 개발자들이 대부분 익숙한 가장 일반적인 에러 처리 형태다.
  • 아쉽게도 try ... catch 문은 동기적으로만 사용 가능하므로 비동기 코드 패턴에서는 무용지물이다.
    function foo() {
      setTimeout(function() {
        baz.bar();
      }, 100);
    }
    try {
      foo();
      // 나중에 baz.bar() 에서 전역 에러 발생
    } catch {
      // 아무 에러도 실행되지 않음
    }

3.5.1 절망의 구덩이

  • 프라미스에서 에러가 파묻히는 걸 막으려면 반드시 프라미스 연쇄 끝부분에 catch()를 써야 한다고 주장하는 개발자들이 있다.
    var p = Promise.resolve(42);
    
    p.then(
      function fulfilled(msg) {
        // 42는 숫자이므로 다음 구문에서 에러가 발생함
        console.log(msg.toLowerCase());
      }
    )
    .catch(handleErrors);
    • p로 유입된 에러 및 p 이후 귀결 중 발생한 에러 (예를 들어 msg.toLowerCase()) 모두 handleErrors로 들어온다.
    • 만약 handleErrors() 함수에서 에러가 난다면? 여기서 방치된 프라미스가 하나 더 있는데, 바로 catch()가 반환한 프라미스다.
    • 그렇다고 무작정 연쇄 끝에 catch()를 하나 더 붙일 수도 없다. 이 함수 역시 실패할 수 있고, 프라미스 연쇄의 마지막 단계에 방치된 프라미스에서 에러가 나면 그 에러가 잡하지 않을 가능성은 (점점 낮아지긴 하겠지만) 항상 존재한다.

3.5.2 잡히지 않은 에러 처리

  • 프라미스에서 잡히지 않은 에러를 처리할 수 있다며 사람들이 이런저런 방안을 제시했는데,
  1. 일부 프라미스 라이브러리는 '전역 미처리 버림 Global Unhandled Rejection' 처리기 같은 것을 등록하는 메서드를 추가하여 전역 범위로 에러를 던지는 대신 이 메서드가 대신 호출되도록 해놓았다.
    • 그러나 잡히지 않은 에러인지 식별하기 위해 버림 직후 임의의 시간 동안 타이머를 걸어놓는 식으로 구현한 것이다. 여기서 임의의 시간 자체가 주관적이거니와 가끔은 일정 시간 프라미스가 버림 상태로 유지시켜야 할 때도 있기 때문에 잡하지 않은 에러 처리기가 모든 긍정 오류 (미처리 상태의 잡히지 않은 에러) 발생 시 호출되기를 바라는 사람은 없을 것이다.
  2. 프라미스 연쇄 끝에 done()을 붙여 완료 사실을 천명해야 한다고 조언하는 사람들도 있다. done()은 프라미스를 생성, 반환하는 함수가 아니므로 done() 버림 처리기 내부에서 에러가 발생하면 잡히지 않은 전역 에러로 던져진다.
    • 이 방식의 문제점은 done()ES6 표준에 들어있지 않다는 것이다. 따라서 믿을 만한 보편적인 해결 방안과는 거리가 멀다.
  3. 브라우저는 언제 어떤 객체가 휴지통으로 직행하여 가비지 콜렉션될지 정확히 알고 추적할 수 있다. 따라서 브라우저는 프라미스 객체를 추적하면서 언제 가비지를 수거하면 될지 분명히 알고 있으며, 프라미스가 버려지면 그 사유가 논리적인, 잡히지 않은 에러이므로 개발자 콘솔창에 표시해야 할지 여부를 확실하게 결정할 수 있다.
    • 그러나 프라미스가 제대로 가비지 콜렉션되지 않으면 (코딩 패턴이 뒤죽박죽이다보면 그렇게 되기 쉽다) 브라우저의 가비지 콜렉션 감지 기능은 도처에 널려있는 버림 프라미스를 파악/진단하는 데에 도움이 되지 않는다.

3.5.3 성공의 구덩이

  • 기본적으로 프라미스는 그다음 잡/이벤트 루프 틱 시점에 에러 처리기가 등록되어 있지 않을 경우 모든 버림을 개발자 콘솔창에 알리도록 되어 있다.
  • 감지되기 전까지 버림 프라미스의 버림 상태를 계속해서 유지하려면 defer()를 호출해서 해당 프라미스에 관한 자동 에러 알림 기능을 끈다.

3.6 프라미스 패턴

3.6.1 Promise.all()

  • 복수의 병렬/동시 작업이 끝날 때까지 진행하지 않고 대기하는 패턴이다. 예를 들면,
    var p1 = request("http://some.url.1/");
    var p2 = request("http://some.url.2/");
    
    Promise.all([p1, p2])
    .then(function(msgs) {
      return request(
        "http://some.url.3/?v=" + msgs.join(",")
      );
    })
    .then(function(msg) {
      console.log(msg);
    });
    • Promise.all([ ])는 보통 프라미스 인스턴스들이 담긴 배열 하나를 인자로 받고, 호출 결과 반환된 프라미스는 이룸 메세지 msg를 수신한다. 이 메시지는 배열에 나열한 순서대로 프라미스들을 통과하면서 얻어진 이룸 메시지의 배열이다. Promise.all([ ])이 반환한 메인 프라미스는 자신의 하위 프라미스들이 모두 이루어져야 이루어질 수 있다. 단 한 개의 프라미스라도 버려지면 Promise.all([ ]) 프라미스 역시 곧바로 버려지며 다른 프라미스 결과도 덩달아 무효가 된다.

3.6.2 Promise.race()

  • Promise.race([ ])는 가장 먼저 이루어진 프라미스의 결과값을 그대로 이행하고, 한편 하나라도 버려지는 프라미스가 있으면 버려진다. (아래 예제는 mdn web docs의 Promise.race()에 대한 설명에서 가져왔다.)
    const promise1 = new Promise((resolve, reject) => {
      setTimeout(resolve, 500, 'one');
    });
    
    const promise2 = new Promise((resolve, reject) => {
      setTimeout(resolve, 100, 'two');
    });
    
    Promise.race([promise1, promise2]).then((value) => {
      console.log(value);
      // Both resolve, but promise2 is faster
    });
    // Expected output: "two"

3.6.3 all(), race()의 변형

  • 프라미스 패턴 중에 자주 쓰이는 것들이 있다.
    • Promise.any()
      • 이터러블 가운데 어느 하나의 프라미스라도 성공하면 프라미스를 반환한다.

3.7 프라미스 API 복습

3.7.1 new Promise() 생성자

  • Promise() 생성자는 항상 new와 함께 사용한다.

3.7.2 Promise.resolve()Promise.reject()

  • Promise는 매개변수로 두가지 함수를 받는데, 첫 번째 함수인 resolve는 비동기 작업을 성공적으로 완료해 결과를 값으로 반환할 때 호출해야 하고, 두 번째 함수인 reject는 작업이 실패하여 오류의 원인을 반환할 때 호출한다.

3.7.3 then()catch()

  • then()은 하나 또는 두 개의 인자를 받는데 첫 번째는 프라미스가 이행됐을 때, 두 번째는 프라미스가 거부됐을 때를 위한 콜백 함수다. 어느 한쪽은 누락하거나 함수가 아닌 값으로 지정하며 각각 기본 콜백으로 대체된다. 기본 resolve는 그냥 메시지를 전달하기만 하고, 기본 reject는 단순히 전달받은 에러 사유를 전파한다.
  • catch()는 프라미스가 거부된 경우에 대한 콜백 함수를 매개변수로 받는다.

3.7.4 Promise.all()Promise.race()

  • Promise.all()은 주어진 모든 프라미스들이 이루어져야 메인 프라미스도 이루어지고 단 하나라도 버려지게 되면 메인 반환 프라미스 역시 곧바로 폐기된다.
  • Promise.race()는 오직 최초로 귀결된 프라미스만 이룸이든 버림이든 반환된다.

3.8 프라미스 한계

3.8.1 시퀀스 에러 처리

  • 프라미스 연쇄에서 에러가 나면 그냥 조용히 묻혀버리기 쉽다.
  • 프라미스 연쇄는 구성원들을 한데 모아놓은 사슬에 불과하기 때문에 전체 연쇄를 하나로 가리킬 Entity가 마땅치 않다. 즉 일어날지 모를 에러를 밖에서는 감지할 도리가 없다.
    • 에러 처리기가 없는 프라미스 연쇄에서 에러가 발생하면 나중에 어딘가에서 감지될 때까지 그 에러는 연쇄를 따라 쭉 하위로 전파된다. 이런 경우, 연쇄의 마지막에 에러 처리기 catch()를 등록하면 전파되어 내려온 에러를 처리할 수 있다.
    p.catch(handleErrors);
    • 하지만 연쇄의 어느 단계에서 나름대로 에러 처리를 하면 handleErrors()는 에러를 감지할 방법이 없다. 이게 당초 의도한 바(Handled Rejection)일 수도 있지만, 그렇지 않을 수도 있다. 이것은 try ... catch 문에 기본적으로 존재하는 한계로, 예외가 잡혀도 그냥 묻혀버릴 가능성은 얼마든지 있다.

3.8.2 단일값

  • 프라미스는 정의상 하나의 이룸값, 아니면 하나의 버림 사유를 가진다.
  • 메시지를 여러 개 담아둘 객체나 배열을 만들면 되고 잘 작동하긴 하겠지만, 프라미스 연쇄의 단계마다 메시지를 감싸고 푸는 일은 무척 불편할 것이다.

3.8.3 단일 귀결

  • 프라미스가 단 1회만 귀결된다는 점은 프라미스의 가장 중요한 본질이다.

3.8.5 프라미스는 취소 불가

  • 일단 프라미스를 생성하여 resolve reject를 등록하면, 도중에 작업 자체를 의미없게 만드는 일이 발생하더라도 외부에서 프라미스 진행을 멈출 방법이 없다.

3.8.6 프라미스 성능

  • 콜백식 비동기 작업 연쇄와 프라미스 연쇄의 움직이는 코드 조각이 얼마나 되는지 살펴보면 아무래도 프라미스가 처리량이 많고 그래서 속도 역시 약간 더 느린 게 사실이다.
  • 프라미스가 모든 것을 비동기화한다는 논란도 있다. 어느 정도 즉시 완료된 단계들이 여전히 다음 단계의 잡 진행을 지연시킨다는 것이다. 즉 콜백식 시퀀스에 비해 일련의 프라미스 작업이 아주 미미하나마 더 늦게 끝날 가능성이 있다는 소리다.

3.9 정리하기

  • 프라미스는 훌륭하다. 콜백식 코드에서 줄곧 목의 가시였던 제어의 역전 문제를 프라미스가 한방에 해결했다.
  • 프라미스가 콜백을 완전히 없애는 건 아니지만, 기존의 콜백 코드를 믿을만한 중계자 역할을 수행하는 유틸리티를 통해 잘 조정하여 서로 조화롭게 작동할 수 있도록 유도한 것이다.
  • 프라미스 연쇄는 비동기 흐름을 순차적으로 표현하는 더 나은 방법이다. 덕분에 우리 두뇌가 비동기 자바스크립트를 더 효율적으로 계획/관리할 수 있다.