[Javascript] 10강 Promise

 

1. Using promises

 

Javascript에서 Promise는 비동기 작업의 최종 완료 또는 실패를 구현하는 객체로 기본 적으로 promise는 콜백은 전달하는 대신에, 콜백을 첨부하는 방식의 개체입니다. 예를 들어 비동기로 음성 파일을 생성해주는 createAudioFileAsync()라는 함수가 있다 가정하고. 해당 함수는 음성 설정에 대한 정보를 받고, 두 가지 콜백 함수를 받습니다.

function successCallback(result) {
  console.log("오디오 파일 주소: " + result);
}

function failureCallback(error) {
  console.log("재생 실패:  " + error);
}

createAudioFileAsync(audioSettings, successCallback, failureCallback);

 

모던한 함수들은 위와 같이 콜백들을 전달하지 않고 콜백을 붙여 사용할 수 있게 Promise를 바환해줍니다. 만약 createdAudioFileAsync() 함수가 Promise를 반환하도록 수정하려면 아래와 같이 사용하세요

createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
const promise = createAudioFileAsync(audioSettings);
promise.then(successCallback, failureCallback);

 

Guarantees

콜백 함수를 전달해주는 고전적 방식과 달리, Promise는 아래와 같은 특징을 보장합니다.

  • 콜백은 Javascript Event Loop가 현재 실행중인 콜 스택을 완료하지 이전에 절대 호출 X
  • 비동기 작업이 성공하거나 실패한 뒤 then()을 이용하여 추가한 콜백의 경우도 위와 같음
  • then()을 여러번 사용하여 여러개의 콜백을 추가할 수 있음. 각각의 콜백은 주어진 순서대로 하나 하나 실행

 

Chaining

 

보통 두 개 이상의 비동기 작업을 순차적 실행 시 흔히 보게 되며, 순차적으로 각각의 작업이 이전 단계 비동기 작업이 성공하고 나서 그 결과값을 이용해 다음 비동기 작업을 실행해야 하는 경우를 의미합니다. 이런 상황에 promise chain을 이용해 해결하기도 합니다. then() 함수는 새로운 promise를 반환합니다. 처음에 만들었던 promise와 다른 새로운 promise

const promise = doSomething();
const promise3 = promise.then(successCallback, failureCallback);

// or 

const promise2 = doSomething.then(successCallback, failureCallback);

 

promise2에 doSomething()뿐만 아니라 successCallback or failureCallback의 완료를 의미합니다. successCallback or failureCallback 또한 promise를 반환하는 비동기 함수일 수 있습니다. 이 경우 promise2에 추가된 콜백은 successCallback또는 failureCallback에 의해 반환된 promise 뒤에 대기합니다.

 

기본적으로, 각각의 promise는 체인 안에서 서로 다른 비동기 단계의 완료를 나타냅니다. 예전에는 여러 비동기 작업을 연속적으로 수행하면 고전적인 '지옥의 콜백 피라미드'가 만들어 졌습니다.

 

doSomething(function (result) {
  doSomethingElse(
    result,
    function (newResult) {
      doThirdThing(
        newResult,
        function (finalResult) {
          console.log("Got the final result: " + finalResult);
        },
        failureCallback
      );
    },
    failureCallback
  );
}, failureCallback);

 

모던한 방식으로 접근한다면, 콜백 함수들을 변환된 promise에 promise chain을 형성시켜 추가할 수 있습니다.

function doSomething(callback) {
  setTimeout(() => callback("firstResult"), 1000);
}

function doSomethingElse(input, callback, failureCallback) {
  if (!input) {
    return failureCallback("Error in doSomethingElse");
  }
  setTimeout(() => callback(input + " -> secondResult"), 1000);
}

function doThirdThing(input, callback, failureCallback) {
  if (!input) {
    return failureCallback("Error in doThirdThing");
  }
  setTimeout(() => callback(input + " -> finalResult"), 1000);
}

function failureCallback(error) {
  console.error("Error:", error);
}
 
doSomething(function (result) {
  doSomethingElse(
    result,
    function (newResult) {
      doThirdThing(
        newResult,
        function (finalResult) {
          console.log("Got the final result: " + finalResult);
        },
        failureCallback
      );
    },
    failureCallback
  );
}, failureCallback);

 

then 에 넘겨지는 인자는 선택적(optional)입니다. 그리고 catch(failureCallback)는 then(null, failureCallback)의 축약입니다. 이 표현식을 화살표 함수로 나타내면 다음과 같습니다.

doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult))
  .then((finalResult) => {
    console.log(`Got the final result: ${finalResult}`);
  })
  .catch(failureCallback);

 

Chaining after a catch

chain에서 작업이 실패한 후에도 다음 작업을 수행하는 것이 가능합니다.

new Promise((resolve, reject) => {
  console.log("Initial");
  resolve();
})
  .then(() => {
    throw new Error("Something failed");
    console.log("Do this");
  })
  .catch(() => {
    console.log("Do that");
  })
  .then(() => {
    console.log("Do this, whatever happened before");
  });

/*
Initial
Do that
Do this, whatever happened before
*/

 

 

Error propagation

'콜백 지옥'에서 failureCallback이 3번 발생한 것을 기억합니다. promise chain에서 단 한번만 발생하는 것과 다릅니다.

doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(result))
  .then((finalResult) => console.log(`Got the final result: ${finalResult}`))
  .catch(failureCallback);

 

기본적으로 promise chain은 예외가 발생하면 멈추고 chain의 아래에서 catch를 찾습니다. 이것은 동기 코드가 어떻게 작동하는지 모델링 한 것입니다.

try {
  const result = syncDoSomething();
  const newResult = syncDoSomethingElse(result);
  const finalResult = syncDoThirdThing(newResult);
} catch (error) {
  failureCallback(error);
}

 

비동기 코드를 사용한 이러한 대칭성은 ECMAScript 2017 에서 async/await 구문(Syntactic sugar)에서 구현할 수 있습니다.

async function foo() {
  try {
    const result = await doSomething();
    const newResult = await doSomethingElse(result);
    const finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch (error) {
    failureCallback(error);
  }
}

 

이것은 promise를 기반으로 하고, doSomething()은 이전 함수와 같습니다. Promise는 모든 오류를 잡아내, 예외 및 프로그래밍 오류가 발생해도 콜백 지옥의 근복적 결함을 해결합니다.

 

Promise rejection events

Promise가 reject될 때마다 두 가지 이벤트 중 하나가 전역 범위에 발생합니다(일반적으로, 전역 범위는 window 거나, 웹 워커에서 사용되는 경우, worker 혹은 워커 기반 인터페이스) 두 가지 이벤트는 다음과 같습니다.

  • rejectionhandled
    • executor의 reject 함수에 의해 reject가 처리 된 후 promise가 reject 될 때 발생합니다.
  • unhandledrejection
    • promise가 reject되었지만 사용할 수 있는 reject 핸들러가 없을 때 발생합니다.

(PromiseRejectionEvent 유형인) 두 이벤트에는 맴버 변수 promisereason 속성이 있습니다. promise는 reject된 promise를 가리키는 속성이고, reason은 promise가 reject된 이유를 알려주는 속성입니다.

 

이들을 이용해 프로미스에 대한 에러 처리를 대체(fallback)하는 것이 가능해지며, 또한 프로미스 관리 시 발생하는 이슈들을 디버깅하는 데 도움을 얻을 수 있습니다. 이 핸들러들은 모든 맥락에서 전역적(global)이기 때문에, 모든 에러는 발생한 지점(source)에 상관없이 동일한 핸들러로 전달됩니다.

 

Node.js로 코드를 작성할때, 흔히 프로젝트에서 사용되는 모듈이 reject된 프로미스를 처리하지 않을 수 있습니다. 이런 경우 노드 실행 시 콘솔에 로그가 남습니다. 이를 수집에서 분석하고 직접 처리할 수도 있습니다. 

 

window.addEventListener(
  "unhandledrejection",
  (event) => {
    event.preventDefault();
  },
  false
);

 

이벤트 preventDefault(). 메서드를 호출하면 reject 된 프로미스가 처리되지 않았을 때 Javascript 런타임이 기본 동작을 수행하지 않스비낟. 이 기본 동작은 대게 콘솔에 오류를 기록하는 것 이기 때문에, 이것은 확실히  NodeJs를 위한 것입니다.

 

오래된 콜백 API를 사용한 Promise 만들기

 

Promise는 생성자를 사용하여 처음부터 생성 될 수 있습니다. 오래된 API를 감쌀 때만 필요하고 이상적 프로그래밍 세계에서는 모든 비동기 함수는 promise를 반환해야 하나, 불행이도 일부 API는 여전히 success 및 / 또는 failure 콜백을 전달하는 방식일거라 생각합니다.

setTimeout((() => saySomething("10 seconds passed"), 10000));

 

예전 스타일의 콜백과 Promise를 합치는 것은 문제가 있습니다. 함수 saySomething()이 실패하거나 프로그래밍 오류가 있다면 아무 것도 잡아 내지 않습니다. setTimeout의 문제점 입니다. setTimeout을 Promise에 감쌀 수 있습니다. 가장 좋은 방법으로는 문제가 되는 함수를 감싼 다음 다시는 직접 호출 하지 않는 것 입니다.

function saySomething(message) {
  console.log(message);
}

function failureCallback(error) {
  console.error("Error:", error);
}

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

wait(1000)
  .then(() => saySomething("10 seconds"))
  .catch(failureCallback);

 

Composition

Promise.resolve()와 Promise.reject()는 각각 이미 resolve되거나 reject된 promise를 직접 생성하기위하 바로 가기입니다. Promise.all()Promise.race()는 비동기 작업을 병렬로 실행하기 위한 두 가지 구성 도구 입니다.

 

병렬로 작업을 시작하고 다음과 같이 모두 완료될 때까지 기달릴 수 있습니다.

Promise.all([func1(), func2(), func3()]).then(([result1, result2, result3]) => {
  /* use result1, result2, result3 */
});

 

순차적 구성도 가능합니다.

[func1, func2, func3]
  .reduce((p, f) => p.then(f), Promise.resolve())
  .then((result3) => {
    console.log(result3);

    /* use result3 */
  });

 

기본적으로, 우리는 비동기 함수 배열을 다음과 같은 Promise 체인으로 줄입니다. 

Promise.resolve().then(func1).then(func2).then(func3);

 

이것을 재사용 가능한 합성 함수로 만들 수 있는데, 이는 함수형 프로그래밍에서 일반적인 방식입니다.

const applyAsync = (acc, val) => acc.then(val);
const composeAsync =
  (...funcs) =>
  (x) =>
    funcs.reduce(applyAsync, Promise.resolve(x));

 

composeAsnyc() 함수는 여러 함수를 인수로 받아들이고 composition 파이프 라인을 통해 전달되는 초기 값을 허용하는 새 함수를 반환합니다.

const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);

 

ECMAScript 2017에서 async/await 를 사용하여 순차적 구성을 보다 간단하게 수행할 수 있습니다.

let result;
for (const f of [func1, func2, func3]) {
  result = await f(result);
}

 

2.Timing

놀라움을 피하기 위해 then()에 전달된 함수는 already-resolved promise에 있는 경우에도 동기적으로 호출되지 않습니다.

Promise.resolve().then(() => console.log(2));
console.log(1); 

// 1
// 2

 

즉시 실행되는 대신 전달된 함수는 마이크로 태스크 대기열에 저장됩니다. 즉, 자바 스크립트 이벤트 루프의 현재 실행이 끝나며, 대기열도 비어있을 때 제어권이 이벤트 루프로 반환되기 직전에 실행됩니다.

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

 

3.Nesting

간단한 promise 체인은 평평하게 유지하는 것이 가장 좋습니다. 중첩된 체인은 부주의한 구성의 결과일 수 있습니다. common mstakes를 참조하면 됩니다. 중첩은 catch 문 범위를 제한하는 제어 구조입니다. 특히, 중첩된 catch는 중첩된 범

 

위 외부의 체인에 있는 오류가 아닌 범위 및 그 이하의 오류만 잡습니다. 올바르게 사용하면 오류 복구 시 더 정확한 결과를 얻을 수 있습니다.

 

function doSomethingCritical() {
  return new Promise((resolve, reject) => {
    resolve("결과");
  });
}

doSomethingCritical()
  .then((result) =>
    doSomethingOptional(result)
      .then((optionalResult) => doSomethingExtraNice(optionalResult))
      .catch((e) => {})
  )
  .then(() => moreCriticalStuff())
  .catch((e) => console.log("Critical failure: " + e.massage));

 

여기에 있는 선택적 단계는 들여 쓰기가 아닌 중첩되어 있지만 주위의 바깥쪽 () 의 규칙적이지 않은 배치를 하지않도록 주의 해야 합니다. 

 

4. Common mistakes

promise chains을 작성할 때 주의해야 할 몇 가지 일반적인 실수는 다음과 같습니다.

doSomething()
  .then(function (result) {
    doSomethingElse(result).then((newResult) => doThirdThing(newResult));
  })
  .then(() => doFourthThing());

 

첫 번째 실수는 제대로 체인을 연결하지 않는 것, 이것은 우리가 새로운 promise를 만들었지만 그것을 반환하는 것을 잊었을 때 일어납니다. 결과적으로 체인이 끊어지거나 오히려 두 개의 독립적인 체인이 경쟁하게 됩니다. 즉 doFourthThing()은 doSomethingElse() 또는 doThirdThing()이 완료될 때까지 기다리지 않고 우리가 의도하지 않았지만 이들과 병렬로 실행됩니다. 또한 별도의 오류 처리 기능을 가지고 있어 잡기 어려운 오류가 발생합니다. 

 

두 번째 실수는 불필요하게 중첩되어 첫 번째 실수를 가능하게 만드는 것으로, 중첩은 내부 오류 처리기의 범위를 제한하여, 의도하지 않는 에러가 캐치되지 않는 오류가 발생할 수 있습니다. 이 변형은 promise constructor anti-pattern입니다. 이 패턴은 이미 약속을 사용하는 코드를 감싸기 위해 promise 생성자의 중복 사용과 중첩을 결합합니다.

 

세 번째 실수는 catch로 체인을 종료하는 것을 잊는 것입니다. 종료되지 않는 promise 체인은 대부분의 브라우저에서 예상하지 못한 promise rejection을 초래합니다. 좋은 방법은 promise 체인을 반환하거나 종결하는 것이며, 새로운 promise를 얻자마자 즉시 변환하여 복잡도를 낮추는 것입니다.

 

function doSomething() {
  return new Promise((resolve, reject) => {
    resolve("결과");
  });
}

function doSomethingElse(result) {
  return new Promise((resolve, reject) => {
    console.log("doSomethingElse:", result);
    resolve(" 새로운 결과");
  });
}

function doThirdThing(newResult) {
  return new Promise((resolve, reject) => {
    console.log("doThirdThing", newResult);
    resolve("세 번째 결과");
  });
}

function doFourthThing() {
  return new Promise((resolve, reject) => {
    console.log("doFourthThing");
    resolve("네 번째 결과");
  });
}

doSomething()
  .then((result) => {
    return doSomethingElse(result);
  })
  .then((newResult) => {
    return doThirdThing(newResult);
  })
  .then(() => {
    return doFourthThing();
  })
  .catch((error) => {
    console.log(error);
  });

 

() => x 은 () => { return x; }의 축약형입니다. async / await 를 사용하면 대부분의 문제를 해결할 수 있습니다. 이러한 문법의 가장 흔한 실수로 await 키워드를 빼먹는 것입니다.

 

참고 자료

 

 

Using promises - JavaScript | MDN

page(Doc) not found /ko/docs/Web/JavaScript/Guide/Details_of_the_Object_Model

developer.mozilla.org

 

 

GitHub - javascript-only/javascript-bloging

Contribute to javascript-only/javascript-bloging development by creating an account on GitHub.

github.com

 

LIST