당신이 최고의 가수이고 팬들이 당신의 다음 노래에 대해 밤낮으로 묻는다고 상상해보십시오.
약간의 안도감을 얻기 위해 출판되면 보내겠다고 약속합니다. 팬에게 목록을 제공합니다. 노래를 사용할 수 있게 되면 모든 구독자가 즉시 이를 받을 수 있도록 이메일 주소를 입력할 수 있습니다. 그리고 예를 들어 스튜디오에 화재가 발생하여 노래를 게시할 수 없는 등의 문제가 발생하더라도 여전히 알림을 받게 됩니다.
모두가 행복합니다. 사람들이 더 이상 붐비지 않기 때문에 당신도 행복하고, 팬들도 노래를 놓치지 않을 것이기 때문에 행복합니다.
이것은 우리가 프로그래밍에서 자주 접하는 것들에 대한 실제 비유입니다:
어떤 작업을 수행하고 시간이 걸리는 "생성 코드"입니다. 예를 들어, 네트워크를 통해 데이터를 로드하는 일부 코드입니다. 바로 '가수'입니다.
준비가 되면 "생산 코드"의 결과를 원하는 "소비 코드"입니다. 많은 함수에 해당 결과가 필요할 수 있습니다. 이들은 '팬'이다.
Promise 는 "생산 코드"와 "소비 코드"를 함께 연결하는 특수 JavaScript 개체입니다. 비유하자면 이것은 "구독 목록"입니다. "생성 코드"는 약속된 결과를 생성하는 데 필요한 시간이 걸리며 "약속"은 준비가 되면 구독하는 모든 코드가 해당 결과를 사용할 수 있도록 합니다.
JavaScript Promise는 단순한 구독 목록보다 더 복잡하기 때문에 비유가 그다지 정확하지 않습니다. 즉, 추가 기능과 제한 사항이 있습니다. 하지만 시작하는 것은 괜찮습니다.
Promise 객체의 생성자 구문은 다음과 같습니다.
약속하자 = 새로운 약속(함수(해결, 거부) { // 실행자(프로듀서 코드, "singer") });
new Promise
에 전달된 함수를 실행자(executor) 라고 합니다. new Promise
생성되면 Executor가 자동으로 실행됩니다. 여기에는 결국 결과를 생성해야 하는 생성 코드가 포함되어 있습니다. 위의 비유에 따르면 집행자는 "가수"입니다.
해당 인수인 resolve
및 reject
JavaScript 자체에서 제공하는 콜백입니다. 우리 코드는 실행자 내부에만 있습니다.
실행자가 결과를 얻으면 빠르든 늦든 상관없이 다음 콜백 중 하나를 호출해야 합니다.
resolve(value)
— 작업이 성공적으로 완료되면 결과 value
입니다.
reject(error)
— 오류가 발생한 경우 error
는 오류 객체입니다.
요약하자면, 실행자는 자동으로 실행되어 작업 수행을 시도합니다. 시도가 끝나면 성공한 경우 resolve
호출하고 오류가 있는 경우 reject
호출합니다.
new Promise
생성자가 반환한 promise
객체에는 다음과 같은 내부 속성이 있습니다.
state
— 처음에는 "pending"
이고, resolve
호출되면 "fulfilled"
로 변경되고, reject
호출되면 "rejected"
로 변경됩니다.
result
— 처음에는 undefined
, resolve(value)
호출되면 value
로 변경되고, reject(error)
호출되면 error
변경됩니다.
따라서 실행자는 결국 promise
다음 상태 중 하나로 이동합니다.
나중에 "팬"이 이러한 변경 사항을 어떻게 구독할 수 있는지 살펴보겠습니다.
다음은 시간이 걸리는 "생성 코드"가 포함된 Promise 생성자와 간단한 실행기 함수의 예입니다( setTimeout
통해).
약속하자 = 새로운 약속(함수(해결, 거부) { // Promise가 생성되면 함수가 자동으로 실행됩니다. // 1초 후에 작업이 완료되고 결과가 "done"임을 알립니다. setTimeout(() => 해결("완료"), 1000); });
위의 코드를 실행하면 두 가지를 볼 수 있습니다.
실행자는 자동으로 즉시 호출됩니다( new Promise
에 의해).
실행자는 두 개의 인수 resolve
및 reject
를 받습니다. 이러한 함수는 JavaScript 엔진에 의해 사전 정의되므로 생성할 필요가 없습니다. 준비가 되면 그 중 하나만 호출해야 합니다.
1초의 "처리" 후에 실행 프로그램은 결과를 생성하기 위해 resolve("done")
호출합니다. 그러면 promise
객체의 상태가 변경됩니다.
그것은 성공적인 업무 완수, '이행된 약속'의 예였습니다.
이제 실행자가 오류로 인해 Promise를 거부하는 예는 다음과 같습니다.
약속하자 = 새로운 약속(함수(해결, 거부) { // 1초 후에 작업이 오류와 함께 완료되었다는 신호를 보냅니다. setTimeout(() => recognition(new Error("앗!")), 1000); });
reject(...)
호출은 Promise 객체를 "rejected"
상태로 이동합니다:
요약하자면, 실행자는 작업(보통 시간이 걸리는 작업)을 수행한 다음, 해당 Promise 개체의 상태를 변경하기 위해 resolve
또는 reject
호출해야 합니다.
처음에 "보류 중인" Promise와 달리 해결되거나 거부된 Promise를 "settled"라고 합니다.
단일 결과 또는 오류만 있을 수 있습니다.
실행자는 하나의 resolve
또는 하나의 reject
만 호출해야 합니다. 모든 상태 변경은 최종적입니다.
이후의 모든 resolve
및 reject
호출은 무시됩니다.
약속하자 = 새로운 약속(함수(해결, 거부) { 해결("완료"); 거절(새로운 오류("…")); // 무시됨 setTimeout(() => 해결("…")); // 무시됨 });
실행자가 수행한 작업에는 결과가 하나만 있거나 오류가 있을 수 있다는 개념입니다.
또한 resolve
/ reject
하나의 인수만(또는 없음) 예상되며 추가 인수는 무시됩니다.
Error
객체로 거부
문제가 발생하는 경우 실행자는 reject
호출해야 합니다. 이는 모든 유형의 인수로 수행될 수 있습니다( resolve
와 마찬가지로). 하지만 Error
객체(또는 Error
에서 상속받은 객체)를 사용하는 것이 좋습니다. 그 이유는 곧 밝혀질 것입니다.
즉시 resolve
/ reject
호출
실제로 실행자는 일반적으로 비동기식으로 작업을 수행하고 일정 시간이 지난 후 resolve
/ reject
호출하지만 반드시 그럴 필요는 없습니다. 다음과 같이 즉시 resolve
또는 reject
호출할 수도 있습니다.
약속하자 = 새로운 약속(함수(해결, 거부) { // 작업을 수행하는 데 시간을 투자하지 않음 해결(123); // 즉시 결과 제공: 123 });
예를 들어, 작업을 시작했지만 모든 것이 이미 완료되고 캐시된 것을 확인할 때 이런 일이 발생할 수 있습니다.
괜찮습니다. 우리는 즉시 해결된 약속을 갖게 됩니다.
state
와 result
내부적입니다.
Promise 객체의 속성 state
와 result
내부적입니다. 우리는 그것들에 직접 접근할 수 없습니다. 이를 위해 .then
/ .catch
/ .finally
메소드를 사용할 수 있습니다. 아래에 설명되어 있습니다.
Promise 객체는 실행자(“생산 코드” 또는 “가수”)와 결과나 오류를 수신하는 소비 함수(“팬”) 사이의 링크 역할을 합니다. 소비하는 함수는 .then
및 .catch
메서드를 사용하여 등록(구독)할 수 있습니다.
가장 중요하고 기본적인 것은 .then
입니다.
구문은 다음과 같습니다.
약속.그러면( function(result) { /* 성공적인 결과 처리 */ }, function(error) { /* 오류 처리 */ } );
.then
의 첫 번째 인수는 Promise가 해결되고 결과를 받을 때 실행되는 함수입니다.
.then
의 두 번째 인수는 Promise가 거부되고 오류를 수신할 때 실행되는 함수입니다.
예를 들어, 성공적으로 해결된 Promise에 대한 반응은 다음과 같습니다:
약속하자 = 새로운 약속(함수(해결, 거부) { setTimeout(() => 해결("완료!"), 1000); }); // 해결은 .then의 첫 번째 함수를 실행합니다. 약속.그러면( result => Alert(result), // "완료!"가 표시됩니다. 1초 후 error => Alert(error) // 실행되지 않습니다. );
첫 번째 기능이 실행되었습니다.
거절의 경우 두 번째는 다음과 같습니다.
약속하자 = 새로운 약속(함수(해결, 거부) { setTimeout(() => recognition(new Error("앗!")), 1000); }); // 거부는 .then의 두 번째 함수를 실행합니다. 약속.그러면( result => Alert(result), // 실행되지 않습니다. error => Alert(error) // "오류: 앗!"이 표시됩니다. 1초 후 );
성공적인 완료에만 관심이 있다면 .then
에 하나의 함수 인수만 제공할 수 있습니다.
약속하자 = 새로운 약속(해결 => { setTimeout(() => 해결("완료!"), 1000); }); promise.then(alert); // "완료!"를 표시합니다. 1초 후
오류에만 관심이 있다면 null
첫 번째 인수로 사용할 수 있습니다: .then(null, errorHandlingFunction)
. 또는 .catch(errorHandlingFunction)
사용할 수도 있습니다. 이는 완전히 동일합니다.
let promise = new Promise((해결, 거부) => { setTimeout(() => recognition(new Error("앗!")), 1000); }); // .catch(f)는 promise.then(null, f)와 동일합니다. 약속.catch(경고); // "오류: 이런!"이 표시됩니다. 1초 후
.catch(f)
호출은 .then(null, f)
와 완전히 유사하며 단지 약어일 뿐입니다.
일반적인 try {...} catch {...}
에 finally
절이 있는 것처럼 Promise에도 finally
가 있습니다.
.finally(f)
호출은 Promise가 해결되거나 거부될 때 f
항상 실행된다는 점에서 .then(f, f)
와 유사합니다.
finally
의 아이디어는 이전 작업이 완료된 후 정리/마무리를 수행하기 위한 핸들러를 설정하는 것입니다.
예: 로딩 표시기 중지, 더 이상 필요하지 않은 연결 닫기 등.
파티 마무리라고 생각하세요. 파티가 좋든 나쁘든, 파티에 친구가 몇 명이든 상관없이, 파티가 끝난 후에는 여전히 정리가 필요합니다(또는 적어도 해야 합니다).
코드는 다음과 같습니다.
new Promise((해결, 거부) => { /* 시간이 걸리는 작업을 수행한 다음 해결 또는 거부를 호출합니다 */ }) // 약속이 확정되면 실행됩니다. 성공 여부는 중요하지 않습니다. .finally(() => 로딩 표시 중지) // 계속 진행하기 전에 로딩 표시기가 항상 중지됩니다. .then(result => 결과 표시, err => 오류 표시)
하지만 finally(f)
는 then(f,f)
의 별칭이 아닙니다.
중요한 차이점이 있습니다:
finally
핸들러에는 인수가 없습니다. finally
우리는 약속이 성공했는지 여부를 알 수 없습니다. 괜찮습니다. 우리의 임무는 일반적으로 "일반" 마무리 절차를 수행하는 것이기 때문입니다.
위의 예를 살펴보십시오. 보시다시피 finally
핸들러에는 인수가 없으며 약속 결과는 다음 핸들러에 의해 처리됩니다.
finally
핸들러는 결과나 오류를 다음 적합한 핸들러로 "전달"합니다.
예를 들어, 여기서 결과는 finally
를 통해 then
으로 전달됩니다.
new Promise((해결, 거부) => { setTimeout(() => 해결("값"), 2000); }) .finally(() => Alert("Promise Ready")) // 먼저 트리거됩니다. .then(결과 => 경고(결과)); // <-- .그런 다음 "값"을 표시합니다.
보시다시피, 첫 번째 Promise에서 반환된 value
finally
통해 다음 then
으로 전달됩니다.
finally
약속 결과를 처리하기 위한 것이 아니기 때문에 매우 편리합니다. 말했듯이, 결과가 어떻든 일반적인 정리를 수행하는 곳입니다.
다음은 오류의 예입니다. 오류가 finally
으로 어떻게 전달되어 catch
되는지 확인할 수 있습니다.
new Promise((해결, 거부) => { 새로운 오류를 발생시킵니다("오류"); }) .finally(() => Alert("Promise Ready")) // 먼저 트리거됩니다. .catch(err => 경고(err)); // <-- .catch는 오류를 표시합니다.
finally
핸들러도 아무것도 반환해서는 안 됩니다. 그렇다면 반환된 값은 자동으로 무시됩니다.
이 규칙의 유일한 예외는 finally
핸들러가 오류를 발생시키는 경우입니다. 그런 다음 이 오류는 이전 결과 대신 다음 처리기로 이동합니다.
요약하자면:
finally
핸들러는 이전 핸들러의 결과를 가져오지 않습니다(인수 없음). 대신 이 결과는 다음 적합한 핸들러로 전달됩니다.
finally
핸들러가 무언가를 반환하면 무시됩니다.
finally
오류가 발생하면 가장 가까운 오류 처리기로 실행이 이동합니다.
이러한 기능은 도움이 되며 finally
사용되어야 하는 방법, 즉 일반 정리 절차를 사용하면 작업이 올바른 방식으로 작동하도록 합니다.
확정된 약속에 핸들러를 연결할 수 있습니다.
Promise가 보류 중인 경우 .then/catch/finally
핸들러는 해당 결과를 기다립니다.
때로는 핸들러를 추가할 때 Promise가 이미 확정된 경우도 있습니다.
이러한 경우 다음 핸들러는 즉시 실행됩니다.
// Promise는 생성 즉시 해결됩니다. let promise = new Promise(resolve => 해결("완료!")); promise.then(alert); // 완료! (지금 바로 나타남)
이는 실제 "구독 목록" 시나리오보다 약속을 더 강력하게 만든다는 점에 유의하세요. 가수가 이미 노래를 발표했는데 누군가가 구독 목록에 등록했다면 아마도 그 노래를 받지 못할 것입니다. 실생활 구독은 행사 이전에 완료되어야 합니다.
약속은 더 유연합니다. 언제든지 핸들러를 추가할 수 있습니다. 결과가 이미 있으면 실행됩니다.
다음으로, Promise가 비동기 코드 작성에 어떻게 도움이 되는지에 대한 보다 실용적인 예를 살펴보겠습니다.
이전 장에서 스크립트를 로드하기 위한 loadScript
함수가 있습니다.
다음은 이를 상기시키기 위한 콜백 기반 변형입니다.
함수 loadScript(src, 콜백) { let script = document.createElement('script'); script.src = src; script.onload = () => 콜백(null, 스크립트); script.onerror = () => callback(new Error(`${src}에 대한 스크립트 로드 오류`)); document.head.append(스크립트); }
Promise를 사용하여 다시 작성해 보겠습니다.
새로운 함수 loadScript
콜백이 필요하지 않습니다. 대신, 로딩이 완료되면 확인되는 Promise 객체를 생성하고 반환합니다. 외부 코드는 .then
사용하여 핸들러(구독 함수)를 추가할 수 있습니다.
함수 로드스크립트(src) { return new Promise(함수(해결, 거부) { let script = document.createElement('script'); script.src = src; script.onload = () => 해결(스크립트); script.onerror = () => accept(new Error(`${src}에 대한 스크립트 로드 오류`)); document.head.append(스크립트); }); }
용법:
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); 약속.그러면( script => 경고(`${script.src}가 로드되었습니다!`), 오류 => 경고(`오류: ${error.message}`) ); promise.then(script => Alert('다른 핸들러...'));
콜백 기반 패턴에 비해 몇 가지 이점을 즉시 확인할 수 있습니다.
약속 | 콜백 |
---|---|
약속을 통해 우리는 자연스러운 순서로 일을 할 수 있습니다. 먼저 loadScript(script) 실행하고 .then 결과로 무엇을 할지 작성합니다. | loadScript(script, callback) 호출할 때 사용할 수 있는 callback 함수가 있어야 합니다. 즉, loadScript 호출되기 전에 결과를 어떻게 처리할지 알아야 합니다. |
Promise에 대해 원하는 만큼 .then 호출할 수 있습니다. 매번 우리는 "구독 목록"에 새로운 구독 기능인 새로운 "팬"을 추가하고 있습니다. 이에 대한 자세한 내용은 다음 장: Promises Chaining에서 확인하세요. | 콜백은 하나만 있을 수 있습니다. |
따라서 약속은 더 나은 코드 흐름과 유연성을 제공합니다. 하지만 더 많은 것이 있습니다. 우리는 다음 장에서 그것을 보게 될 것입니다.
아래 코드의 출력은 무엇입니까?
약속하자 = 새로운 약속(함수(해결, 거부) { 해결(1); setTimeout(() => 해결(2), 1000); }); promise.then(alert);
출력은 다음과 같습니다. 1
.
reject/resolve
의 첫 번째 호출만 고려되기 때문에 두 번째 resolve
호출은 무시됩니다. 추가 호출은 무시됩니다.
내장 함수 setTimeout
콜백을 사용합니다. 약속 기반 대안을 만듭니다.
delay(ms)
함수는 약속을 반환해야 합니다. 해당 약속은 ms
밀리초 후에 해결되어야 하므로 다음과 같이 .then
추가할 수 있습니다.
함수 지연(ms) { // 귀하의 코드 } Delay(3000).then(() => Alert('3초 후에 실행'));
함수 지연(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } Delay(3000).then(() => Alert('3초 후에 실행'));
이 작업에서는 resolve
인수 없이 호출된다는 점에 유의하세요. delay
에서 어떤 값도 반환하지 않으며 지연만 확인하세요.
콜백을 수락하는 대신 약속을 반환하도록 콜백이 포함된 애니메이션 원 작업 솔루션의 showCircle
함수를 다시 작성합니다.
새로운 사용법:
showCircle(150, 150, 100).then(div => { div.classList.add('message-ball'); div.append("안녕하세요, 세상!"); });
콜백을 기반으로 하는 애니메이션 서클 작업의 솔루션을 사용하세요.
샌드박스에서 솔루션을 엽니다.