Promise 객체는 비동기 작업이 최종적으로 완료되거나 실패한 것, 그리고 그 결과값을 나타내요.
이 주제는 많은 사람에게 어려워요. 특히 완전히 동기식 언어로 프로그래밍을 해 본 적이 있다면 더 어렵죠. 부담스럽게 느껴지거나 동시성과 병렬성에 대해 더 알고 싶다면, 훌륭한 강연 "Concurrency is not parallelism"을 go.dev를 통해 보거나 vimeo에서 직접 보고, 슬라이드를 읽어 보세요.
Promise에는 세 가지 상태가 있어요:
프로미스는 생성될 때 대기 중이에요. 미래의 어느 시점에 _이행_되거나 _거부_될 수 있어요. 한 번 이행되거나 거부된 프로미스는 다시 이행되거나 거부될 수 없고, 상태도 바뀌지 않아요.
다시 말해서:
프로미스는 여러 가지 방법으로 이행될 수 있어요:
// Creates a promise that is immediately resolved
Promise.resolve(value);
// Creates a promise that is immediately resolved
new Promise((resolve) => {
resolve(value);
});
// Chaining a promise leads to a resolved promise
somePromise.then(() => {
// ...
return value;
});
위 예시에서 value는 오류, undefined, null, 다른 프로미스 등 무엇이든 될 수 있어요.
보통은 오류가 아닌 값으로 이행하고 싶을 거예요.
프로미스는 여러 가지 방법으로 거부될 수 있어요:
// Creates a promise that is immediately rejected
Promise.reject(reason)
// Creates a promise that is immediately rejected
new Promise((_, reject) {
reject(reason)
})
// Chaining a promise with an error leads to a rejected promise
somePromise.then(() => {
// ...
throw reason
})
위 예시에서 reason은 오류, undefined, null 등 무엇이든 될 수 있어요.
보통은 오류로 거부하고 싶을 거예요.
프로미스는 이행되거나 거부된 뒤에, 이후 동작으로 이어갈 수 있어요.
promise가 이행되면 promise.then()이 호출돼요promise가 거부되면 promise.catch()가 호출돼요promise가 이행되거나 거부되면 promise.finally()가 호출돼요모든 프로미스는 "thenable"이에요.
즉, 원래 프로미스가 이행되면 실행되는 then 함수가 있다는 뜻이에요.
promise.then(onResolved)에서 콜백 onResolved는 원래 프로미스가 이행된 값을 받아요.
그러면 항상 새로운 "연결된" 프로미스가 반환돼요.
then에서 value를 반환하면 "연결된" 프로미스가 이행돼요.
then에서 reason을 던지면 "연결된" 프로미스가 거부돼요.
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
});
const promise2 = promise1.then(function (value) {
console.log(value);
// expected output: "Success!"
return true;
});
약 1000ms 후에 "Success!"가 출력돼요.
promise1의 상태와 값은 resolved와 "Success!"가 돼요.
promise2의 상태와 값은 resolved와 true가 돼요.
원래 프로미스가 거부될 때 실행되는 두 번째 인자도 있어요.
promise.then(onResolved, onRejected)에서 콜백 onResolved는 원래 프로미스가 이행된 값을 받고, 콜백 onRejected는 프로미스가 거부된 이유를 받아요.
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
function log(value) {
console.log(value);
return true;
}
function shout(reason) {
console.error(reason.toUpperCase());
return false;
}
const promise2 = promise1.then(log, shout);
"Success!"가 출력돼요.
promise1의 상태와 값은 resolved와 "Success!"가 돼요.promise2의 상태와 값은 resolved와 true가 돼요."NOPE!"가 출력돼요.
promise1의 상태와 값은 rejected와 Nope!가 돼요.promise2의 상태와 값은 resolved와 false가 돼요.생명 주기 규칙 때문에, 프로미스가 reject하면 약 1000ms 뒤에 오는 resolve는 조용히 무시된다는 점을 이해하는 게 중요해요. 한 번 거부되거나 이행된 뒤에는 내부 상태가 바뀔 수 없기 때문이에요.
프로미스에서 값을 반환하면 그 프로미스가 이행되고, 값을 던지면 거부된다는 점도 이해하는 게 중요해요.
promise1이 이행되고 연결된 onResolved가 있으면, 즉 then(onResolved)라면 그 후속 작업은 이행되거나 거부될 수 있는 새로운 프로미스예요.
promise1이 거부되고 연결된 onRejected가 있으면, 즉 then(, onRejected)라면 그 후속 작업은 이행되거나 거부될 수 있는 새로운 프로미스예요.
때로는 오류를 잡아 두고 원래 프로미스가 reject할 때만 이어가고 싶을 때가 있어요.
promise.catch(onCatch)에서 콜백 onCatch는 원래 프로미스가 거부된 이유를 받아요.
그러면 항상 새로운 "연결된" 프로미스가 반환돼요.
catch에서 value를 반환하면 "연결된" 프로미스가 이행돼요.
catch에서 reason을 던지면 "연결된" 프로미스가 거부돼요.
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
function log(value) {
console.log(value);
return 'done';
}
function recover(reason) {
console.error(reason.toUpperCase());
return 42;
}
const promise2 = promise1.catch(recover).then(log);
약 1/2의 경우, 약 1000ms 후에 "Success!"가 출력돼요.
나머지 1/2의 경우, 즉시 42가 출력돼요.
promise1이 이행되면 catch는 건너뛰고 then에 도달해서 값을 출력해요.
promise1의 상태와 값은 resolved와 "Success!"가 돼요.promise2의 상태와 값은 resolved와 "done"이 돼요;promise1이 거부되면 catch가 실행되는데, 이때 _값을 반환_하므로 연결이 이제 resolved가 되고, then에 도달해서 값을 출력해요.
promise1의 상태와 값은 rejected와 "Nope!"가 돼요.promise2의 상태와 값은 resolved와 "done"이 돼요;때로는 프로미스가 이행되든 거부되든 상관없이, 프로미스가 끝난 뒤에 코드를 실행하고 싶을 때가 있어요.
promise.finally(onSettled)에서 콜백 onSettled는 아무것도 받지 않아요.
그러면 항상 새로운 "연결된" 프로미스가 반환돼요.
finally에서 value를 반환하면 원래 프로미스의 상태와 값을 그대로 복사하고, value는 무시해요.
finally에서 reason을 던지면 "연결된" 프로미스를 거부하고, 원래 프로미스의 상태와 값 또는 이유를 덮어써요.
여러 메서드를 함께 사용한 예시예요:
const myPromise = new Promise(function (resolve, reject) {
const sampleData = [2, 4, 6, 8];
const randomNumber = Math.round(Math.random() * 5);
if (sampleData[randomNumber]) {
resolve(sampleData[randomNumber]);
} else {
reject('Sampling did not result in a sample');
}
});
const finalPromise = myPromise
.then(function (sampled) {
// If the random number was 0, 1, 2, or 3, this will be
// reached and the number 2, 4, 6, or 8 will be logged.
console.log(`Sampled data: ${sampled}`);
return 'yay';
})
.catch(function (reason) {
// If the random number was 4 or 5, this will be reached and
// reason will be "An error occurred". The entire chain will
// then reject with an Error with the reason as message.
throw new Error(reason);
})
.finally(function () {
// This will always log after either the sampled data is
// logged or the error is raised.
console.log('Promise completed');
});
randomNumber가 0-3인 경우:
myPromise는 2, 4, 6, or 8 값으로 이행돼요finalPromise는 'yay' 값으로 이행돼요Sampled data: ...Promise completedrandomNumber가 4-5인 경우:
myPromise는 'Sampling did not result in a sample'이라는 이유로 거부돼요finalPromise는 Error('Sampling did not result in a sample')이라는 이유로 거부돼요Promise completed"uncaught rejected promise: Error('Sampling did not result in a sample')" 로그가 남아요위에서 보았듯이 reject는 문자열과 함께 동작하고, 프로미스는 Error로도 거부할 수 있어요.
프로미스 연결이나 일반적인 사용법이 명확하지 않다면, MDN의 튜토리얼이 좋은 자료예요.
이 연습 문제에서는 무료 회원에게 기본 번역 서비스를 제공하고, 프리미엄 회원에게는 품질 보증이 포함된 고급 번역을 제공하는 TranslationService를 만들 거예요.
API
합리적인 시간 안에 어떤 번역 request든 처리해 주는 우주 번역 API를 찾았어요.
이걸 잘 활용해 보고 싶어요.
우주 번역가들은 변덕이 아주 심하고 중복을 싫어해서, 과거 번역을 그들에게 부담 주지 않고 fetch할 수 있는 API 저장소 위성도 제공해요.
번역 가져오기
api.fetch(text)는 _API 저장소_에서 text의 번역을 가져와서 두 값을 제공하는 promise를 반환해요.
translation: 실제 번역quality: 숫자로 표현된 품질_API 저장소_에서 번역을 찾지 못하면 API는 NotAvailable 오류를 던져요.
번역은 api.request 메서드로 추가할 수 있어요.
'text'가 번역할 수 없는 텍스트라면 API는 Untranslatable 오류를 던져요.
api.fetch('jIyaj');
// => Promise({ resolved: 'I understand' })
번역 요청하기
어떤 번역은 분명히 존재하지만 아직 _API 저장소_에 추가되지 않았을 수 있어요. 이게 NotAvailable(저장소에는 없지만 요청할 수는 있는 경우)과 Untranslatable(번역할 수 없는 경우)의 차이예요.
api.request(text, callback)은 text의 번역을 수행해서 _API 저장소_에 추가해 달라고 요청해요.
완료되면 callback 함수가 호출돼요.
callback에 undefined가 전달돼요. 번역이 성공했고 api.fetch 메서드로 접근할 수 있다는 뜻이에요.callback에 error가 전달돼요. 뭔가 잘못됐다는 뜻이에요.
우주 API는 _불안정_해서 자주 실패해요.
그럴 때는 api.request를 다시 호출해도 괜찮아요.api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
// because it was successful.
⚠ 경고! ⚠
API는 request가 들어오면 여러 번역가를 순간이동시켜서 마법을 부려요.
이건 비용이 아주 큰 동작이라서, 번역이 이미 있는데도 호출해서는 안 돼요.
안타깝게도 모든 사람이 설명서를 읽는 건 아니라서, 문제를 일으키는 사용자를 쫓아내는 시스템이 마련돼 있어요.
번역이 이미 있는 text에 대해 api.request를 호출하면, API는 이 호출에 대해 AbusiveClientError를 던지고, 그 이후의 모든 호출에도 던져요.
이미 번역된 것이 있다면 번역을 요청하지 않도록 꼭 주의해요.
무료 서비스는 현재 _API 저장소_에 있는 번역만 제공해요.
_API 저장소_에 이미 있는 번역을 무료 회원에게 제공하는 free(text) 메서드를 구현해요.
품질은 무시하고, API가 던지는 오류는 그대로 전달해요.
api.fetch 메서드를 사용해요 (api.fetch는 promise를 반환해요)service.free('jIyaj');
// => Promise<...> resolves "I understand."
service.free("jIyajbe'");
// => Promise<...> rejects Error("Not yet translated")
무료 회원을 위해 텍스트 배열을 무료 서비스로 번역해서, 모든 번역을 반환하거나 오류 하나를 반환하는 batch([text, text, ...]) 메서드를 구현해요.
BatchIsEmpty 오류로 거부돼요service.batch(['jIyaj', "majQa'"]);
// => Promise<...> resolves ["I understand.", "Well done!"]
service.batch(['jIyaj', "jIyajbe'"]);
// => Promise<...> rejects new Error("Not yet translated")
service.batch([]);
// => Promise<...> rejects BatchIsEmpty()
_API 저장소_에 번역을 추가해 달라고 _요청_하는 프리미엄 사용자용 메서드 request(text)를 구현해요.
요청은 실패하면 자동으로 재시도해야 해요.
같은 요청에 대해 3번 넘게 호출해서는 안 돼요 (우주 번역가들을 화나게 하지 마세요!!!).
api.request가 오류를 반환하지 않으면 undefined로 이행해요api.request가 오류를 반환하면 최대 두 번 재시도해요service.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API
번역을 가져오는 프리미엄 사용자용 메서드 premium(text, quality)를 구현해요.
번역이 NotAvailable이면 번역을 요청하고, _API 저장소_에 추가된 뒤에 가져와요.
이 메서드는 특정 quality 기준을 충족할 때만 번역을 반환해야 해요.
api.fetch가 이행되면 품질을 확인한 뒤 이행해요api.fetch가 거부되면 대신 번역을 _요청_해요api.request가 거부되면 오류를 그대로 전달해요service.premium("jIyajbe'", 100);
// => Promise<...> resolves "I don't understand."
service.premium("'arlogh Qoylu'pu'?", 100);
// => Promise<...> rejects QualityThresholdNotMet()
service.premium("'arlogh Qoylu'pu'?", 40);
// => Promise<...> resolves "What time is it?"
참고
'arlogh Qoylu'pu'?를 올바르게 번역하면 **How many times has it been heard?**예요.
Exercism에 가입하고 JavaScript 트랙을 개념 37개연습 문제 159개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.