1. Promise란?
Promise는 비동기 작업의 상태를 나타내는 객체다.
Promise가 생성되고, 비동기 연산이 종료된 후에 결과값 또는 실패에 대한 처리를 연결할 수 있다.
Promise는 3가지 상태를 가진다.
- pending: 이행하지도, 거부하지도 않은 초기 상태.
- fulfilled: 연산이 성공적으로 완료됨.
- rejected: 연산이 실패함.
세가지 상태는 각각 아래와 같이 구현할 수 있다.
const pendingPromise = new Promise(() => { console.log('pending...') }) const fulfilledPromise = new Promise((resolve) => { resolve('fulfilled') }) const rejectedPromise = new Promise((resolve, reject) => { reject('rejected') }) console.log('pendingPromise: ', pendingPromise) console.log('fulfilledPromise: ', fulfilledPromise) console.log('rejectedPromise: ', rejectedPromise)
여기서 fulfilled와 refected상태의 Promise객체에서 각각
then과 catch매소드를 사용할 수 있다.
(사실 then에서 rejected도 처리할수 있다.)fulfiledPromise.then(value => { console.log('여기서 promise가 fulfilled 된 다음에 수행할 작업을 합니다.', value) }) rejectedPromise.catch(error => { console.log('여기서 promise가 rejected된 다음에 수행할 작업을 합니다.', error) })
1-1. Promise 한번 사용해보기
Promise를 사용하지 않고 콜백을 사용하는 방식과, Promise를 사용한 방식을 비교해볼 수 있다.
- Promise 없이 사용
function successCallback(result) { console.log("Audio file ready at URL: " + result); } function failureCallback(error) { console.log("Error generating audio file: " + error); } createAudioFileAsync(audioSettings, successCallback, failureCallback);
- Promise 사용
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
함수에 콜백을 전달하지 않고
then 매소드를 통해 콜백을 붙이는 것으로 간단하게 사용될 수 있다.2. Chaining
chaining은 Promise의 핵심 장점 중 하나이다.
보통 두 개 이상의 비동기 작업을 순차적으로 실행해야하는 상황에서 사용된다.
아래는 chaining을 사용하지 않고 비동기 작업을 연속적으로 수행하는 예제코드이다.
doSomething(function (result) { doSomethingElse( result, function (newResult) { doThirdThing( newResult, function (finalResult) { console.log("Got the final result: " + finalResult); }, failureCallback, ); }, failureCallback, ); }, failureCallback);
이를 모던한 방식인 chaining을 사용하여 간단히 구성할 수 있다.
doSomething() .then((result) => doSomethingElse(result)) .then((newResult) => doThirdThing(newResult)) .then((finalResult) => { console.log(`Got the final result: ${finalResult}`); }) .catch(failureCallback);
catch 이후에도 작업을 추가로 수행할 수 있다.new Promise((resolve, reject) => { console.log("Initial"); // 1 resolve(); }) .then(() => { throw new Error("Something failed"); console.log("Do this"); // 실행되지 않음 }) .catch(() => { console.log("Do that"); // 2 }) .then(() => { console.log("Do this, whatever happened before"); // 3 }); /* Initial Do that Do this, whatever happened before */

3. Error propagation
Promise는 에러 핸들링 처리에서도 유리하다.
앞선 [[Promise#2. Chaining]]에서의 콜백 지옥 예제에서는 실패 콜백인
failureCallback이 총 3번 발생을 했다.
하지만 chaining을 사용하면 이 또한 개선할 수 있다.doSomething() .then((result) => doSomethingElse(result)) .then((newResult) => doThirdThing(newResult)) .then((finalResult) => console.log(`Got the final result: ${finalResult}`)) .catch(failureCallback);
- 값을 반환하면 다음
then으로 resolved 값이 간다.
- 에러를 throw하면 다음
catch로 rejected가 간다.
- Promise를 반환하면 체인이 평탄화(flatten)되어 그 Promise의 상태/값을 따른다.
중요한점은, 중간의 체인 어디에서나 에러가 발생하면
catch로 이동한다는 점이다.
따라서 각 체인마다 catch를 작성할 필요없이 마지막에만 catch를 추가하여도 모든 에러를 잡을 수 있다.
이는 콜백 지옥의 근본적인 결함을 해결하며, 비동기 작업 구성에 필수적이다.4. Promise rejection events
rejectionhandled와 unhandledrejection은 Promise와 관련된 이벤트이다.
unhandledrejection은 Promise가 reject되었는데, catch메소드로 처리하지 않았을때 발생하는 이벤트이다.const p = Promise.reject(new Error("문제 발생!")); window.addEventListener("unhandledrejection", (event) => { console.log("unhandledRejection:", event.reason); });
rejectionhandled는 처리되지 않은 Promise reject를 뒤늦게 처리한 경우 발생하는 이벤트이다.const p = Promise.reject(new Error("문제 발생!")); setTimeout(() => { // reject된 뒤 나중에 catch를 붙임 p.catch((err) => { console.log("나중에 처리:", err.message); }); }, 2000); window.addEventListener("unhandledrejection", (event) => { console.log("unhandledrejection:", event.reason); }); window.addEventListener("rejectionhandled", (event) => { console.log("rejectionhandled:", event.reason); });
5. Timing
then에 전달된 함수는 동기적으로 호출되지 않는다.Promise.resolve().then(() => console.log(2)); console.log(1); // 출력: // 1 // 2
전달된 함수는 마이크로 태스크 큐에 추가되기 때문에, 현재 콜스택이 비워진 다음에야 실행될 수 있다.
6. Nesting
Promise chaining은 가능한 평평하게 유지하는것이 좋다.
중첩된 체인은 대게 안티 패턴을 만들 가능성이 높다.
doSomethingCritical() .then((result) => doSomethingOptional(result) .then((optionalResult) => doSomethingExtraNice(optionalResult)) .catch((e) => {}), ) // Ignore if optional stuff fails; proceed. .then(() => moreCriticalStuff()) .catch((e) => console.log("Critical failure: " + e.message));
중첩된 체인 구조를 사용할 경우
catch는 내부 체인에 대한 에러만 처리가 가능하다.
따라서 위의 예제에서는 doSomethingCritical에서 오류가 발생할 시에 가장 하단의 catch에서만 오류를 잡을 수 있다.유명한 안티패턴으로는 아래 예제가 있다.
function getStuffDone(param) { return new Promise(function(resolve, reject) { myPromiseFn(param+1) .then(function(val) { resolve(val); }).catch(function(err) { reject(err); }); }); }
위 예제에서는
myPromiseFn을 호출하면서 작업에 대한 fulfilled와 reject에 대한 처리를 새로운 Promise로 감싸서 처리하고있다.
하지만 위 getStuffDone함수는 아래와 같이 작성해도 차이가 없다.function getStuffDone(param) { return myPromiseFn(param+1) }
이미
myPromiseFn에서 Promise를 반환하기 때문에 새로운 Promise로 감쌀 필요가 없는것이다.
