소개: 콜백 장에서 언급한 문제로 돌아가 보겠습니다. 예를 들어 스크립트 로드와 같이 차례로 수행되는 일련의 비동기 작업이 있습니다. 어떻게 코딩을 잘 할 수 있을까요?
Promise는 이를 수행하기 위한 몇 가지 방법을 제공합니다.
이번 장에서는 Promise Chaining을 다룹니다.
다음과 같습니다:
new Promise(함수(해결, 거부) { setTimeout(() => 해결(1), 1000); // (*) }).then(함수(결과) { // (**) 경고(결과); // 1 결과 * 2 반환; }).then(함수(결과) { // (***) 경고(결과); // 2 결과 * 2 반환; }).then(함수(결과) { 경고(결과); // 4 결과 * 2 반환; });
아이디어는 결과가 .then
핸들러 체인을 통해 전달된다는 것입니다.
흐름은 다음과 같습니다.
초기 Promise는 1초 (*)
안에 해결됩니다.
그런 다음 .then
핸들러가 (**)
호출되어 새로운 Promise를 생성합니다( 2
값으로 해결됨).
다음 then
(***)
은 이전 결과의 결과를 가져와 처리하고(doubles) 다음 핸들러에 전달합니다.
…등.
결과가 핸들러 체인을 따라 전달되면 1
→ 2
→ 4
라는 일련의 alert
호출을 볼 수 있습니다.
.then
을 호출할 때마다 새로운 Promise가 반환되어 다음 .then
호출할 수 있기 때문에 모든 것이 작동합니다.
핸들러가 값을 반환하면 해당 Promise의 결과가 되므로 다음 .then
이 해당 값과 함께 호출됩니다.
고전적인 초보자 오류: 기술적으로 단일 Promise에 많은 .then
추가할 수도 있습니다. 이것은 체인이 아닙니다.
예를 들어:
약속하자 = 새로운 약속(함수(해결, 거부) { setTimeout(() => 해결(1), 1000); }); promise.then(함수(결과) { 경고(결과); // 1 결과 * 2 반환; }); promise.then(함수(결과) { 경고(결과); // 1 결과 * 2 반환; }); promise.then(함수(결과) { 경고(결과); // 1 결과 * 2 반환; });
여기서 우리가 한 일은 단지 하나의 Promise에 여러 개의 핸들러를 추가한 것 뿐입니다. 그들은 결과를 서로에게 전달하지 않습니다. 대신 독립적으로 처리합니다.
그림은 다음과 같습니다(위의 체인과 비교).
모든 .then
동일한 Promise에서 동일한 결과, 즉 해당 Promise의 결과를 얻습니다. 따라서 위의 코드에서는 모든 alert
동일하게 표시됩니다: 1
.
실제로 하나의 Promise에 여러 핸들러가 필요한 경우는 거의 없습니다. 체인 연결은 훨씬 더 자주 사용됩니다.
.then(handler)
에 사용되는 핸들러는 약속을 생성하고 반환할 수 있습니다.
이 경우 추가 핸들러는 안정화될 때까지 기다린 다음 결과를 얻습니다.
예를 들어:
new Promise(함수(해결, 거부) { setTimeout(() => 해결(1), 1000); }).then(함수(결과) { 경고(결과); // 1 return new Promise((해결, 거부) => { // (*) setTimeout(() => 해결(결과 * 2), 1000); }); }).then(함수(결과) { // (**) 경고(결과); // 2 return new Promise((해결, 거부) => { setTimeout(() => 해결(결과 * 2), 1000); }); }).then(함수(결과) { 경고(결과); // 4 });
여기서 첫 번째 .then
1
표시하고 (*)
줄에 new Promise(…)
반환합니다. 1초 후에 해결되고 결과( resolve
의 인수, 여기서는 result * 2
)가 두 번째 .then
핸들러에 전달됩니다. 해당 핸들러는 (**)
줄에 있으며 2
표시하고 동일한 작업을 수행합니다.
따라서 출력은 이전 예와 동일합니다(1 → 2 → 4). 그러나 이제 alert
호출 사이에 1초 지연이 있습니다.
Promise를 반환하면 비동기 작업 체인을 구축할 수 있습니다.
이전 장에서 정의한 약속된 loadScript
와 함께 이 기능을 사용하여 스크립트를 하나씩 순서대로 로드해 보겠습니다.
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(함수(스크립트) { return loadScript("https://javascript.info/article/promise-chaining/two.js"); }) .then(함수(스크립트) { return loadScript("https://javascript.info/article/promise-chaining/3.js"); }) .then(함수(스크립트) { // 스크립트에 선언된 함수를 사용합니다. // 실제로 로드되었음을 보여주기 위해 하나(); 둘(); 삼(); });
이 코드는 화살표 기능을 사용하여 조금 더 짧게 만들 수 있습니다.
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(script => loadScript("https://javascript.info/article/promise-chaining/two.js")) .then(script => loadScript("https://javascript.info/article/promise-chaining/ three.js")) .then(스크립트 => { // 스크립트가 로드되면 거기에 선언된 함수를 사용할 수 있습니다. 하나(); 둘(); 삼(); });
여기서 각 loadScript
호출은 약속을 반환하고 다음 .then
은 해결될 때 실행됩니다. 그런 다음 다음 스크립트의 로딩을 시작합니다. 따라서 스크립트가 차례로 로드됩니다.
체인에 더 많은 비동기 작업을 추가할 수 있습니다. 코드는 여전히 "플랫"입니다. 즉, 오른쪽이 아닌 아래쪽으로 늘어납니다. "파멸의 피라미드"의 흔적은 없습니다.
기술적으로 다음과 같이 각 loadScript
에 .then
직접 추가할 수 있습니다.
loadScript("https://javascript.info/article/promise-chaining/one.js").then(script1 => { loadScript("https://javascript.info/article/promise-chaining/two.js").then(script2 => { loadScript("https://javascript.info/article/promise-chaining/3.js").then(script3 => { // 이 함수는 변수 script1, script2 및 script3에 액세스할 수 있습니다. 하나(); 둘(); 삼(); }); }); });
이 코드는 동일한 작업을 수행합니다. 즉, 3개의 스크립트를 순서대로 로드합니다. 그러나 그것은 "오른쪽으로 자랍니다". 따라서 콜백과 동일한 문제가 있습니다.
Promise를 사용하기 시작하는 사람들은 때때로 Chaining에 대해 모르기 때문에 이렇게 작성합니다. 일반적으로 체인 연결이 선호됩니다.
때로는 중첩된 함수가 외부 범위에 액세스할 수 있으므로 .then
직접 작성하는 것이 좋습니다. 위의 예에서 가장 많이 중첩된 콜백은 모든 변수 script1
, script2
, script3
에 액세스할 수 있습니다. 그러나 그것은 규칙이 아니라 예외입니다.
Thenables
정확하게 말하자면, 핸들러는 약속을 정확히 반환하지 않고 소위 "thenable" 개체, 즉 .then
메서드를 가진 임의의 개체를 반환할 수 있습니다. 약속과 같은 방식으로 처리됩니다.
아이디어는 제3자 라이브러리가 자체적으로 "promise 호환" 개체를 구현할 수 있다는 것입니다. 확장된 메소드 세트를 가질 수 있지만 .then
구현하기 때문에 기본 Promise와도 호환됩니다.
다음은 thenable 객체의 예입니다:
클래스 Thenable { 생성자(숫자) { this.num = 숫자; } then(해결, 거부) { 경고(해결); // 함수() { 네이티브 코드 } // 1초 후 this.num*2로 해결 setTimeout(() => 해결(this.num * 2), 1000); // (**) } } 새로운 약속(해결 => 해결(1)) .then(결과 => { 새로운 Thenable(결과)를 반환합니다; // (*) }) .then(경고); // 1000ms 후에 2를 표시합니다.
JavaScript는 라인 (*)
에서 .then
핸들러가 반환한 객체를 확인합니다. then
이라는 호출 가능한 메소드가 있는 경우 해당 메소드를 호출하여 기본 함수인 메소드를 호출 resolve
(실행기와 유사) 인수로 reject
하고 그 중 하나가 나올 때까지 기다립니다. 호출됩니다. 위의 예에서는 1초 (**)
후에 resolve(2)
호출됩니다. 그런 다음 결과는 체인 아래로 더 전달됩니다.
이 기능을 사용하면 Promise
에서 상속하지 않고도 사용자 정의 개체를 Promise 체인과 통합할 수 있습니다.
프론트엔드 프로그래밍에서는 네트워크 요청에 Promise가 자주 사용됩니다. 그럼 이에 대한 확장된 예를 살펴보겠습니다.
fetch 메소드를 사용하여 원격 서버에서 사용자에 대한 정보를 로드합니다. 별도의 장에서 다루는 많은 선택적 매개 변수가 있지만 기본 구문은 매우 간단합니다.
약속하자 = fetch(url);
그러면 url
에 대한 네트워크 요청이 이루어지고 약속이 반환됩니다. 원격 서버가 헤더로 응답할 때 전체 응답이 다운로드되기 전에 Promise는 response
개체로 확인됩니다.
전체 응답을 읽으려면 response.text()
메소드를 호출해야 합니다. 이는 원격 서버에서 전체 텍스트를 다운로드할 때 해당 텍스트를 결과로 포함하여 확인하는 약속을 반환합니다.
아래 코드는 user.json
에 요청하고 서버에서 해당 텍스트를 로드합니다.
가져오기('https://javascript.info/article/promise-chaining/user.json') // .then 원격 서버가 응답하면 아래가 실행됩니다. .then(함수(응답)) { // response.text()는 전체 응답 텍스트로 해결되는 새로운 약속을 반환합니다. // 로드할 때 return response.text(); }) .then(함수(텍스트) { // ...그리고 여기에 원격 파일의 내용이 있습니다 경고(텍스트); // {"name": "iliakan", "isAdmin": true} });
fetch
에서 반환된 response
개체에는 원격 데이터를 읽고 이를 JSON으로 구문 분석하는 response.json()
메서드도 포함되어 있습니다. 우리의 경우에는 이것이 훨씬 더 편리하므로 전환해 보겠습니다.
또한 간결성을 위해 화살표 기능을 사용합니다.
// 위와 동일하지만 response.json()은 원격 콘텐츠를 JSON으로 구문 분석합니다. 가져오기('https://javascript.info/article/promise-chaining/user.json') .then(응답 => response.json()) .then(사용자 => 경고(사용자.이름)); // iliakan, 사용자 이름을 얻었습니다.
이제 로드된 사용자에 대해 작업을 수행해 보겠습니다.
예를 들어 GitHub에 한 번 더 요청하고 사용자 프로필을 로드하고 아바타를 표시할 수 있습니다.
// user.json을 요청합니다. 가져오기('https://javascript.info/article/promise-chaining/user.json') // json으로 로드 .then(응답 => response.json()) // GitHub에 요청하기 .then(사용자 => 가져오기(`https://api.github.com/users/${user.name}`)) // 응답을 json으로 로드합니다. .then(응답 => response.json()) // 아바타 이미지(githubUser.avatar_url)를 3초 동안 표시합니다(애니메이션으로 만들 수도 있음) .then(githubUser => { img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "약속-아바타-예제"; document.body.append(img); setTimeout(() => img.remove(), 3000); // (*) });
코드가 작동합니다. 세부 사항에 대한 의견을 참조하십시오. 그러나 거기에는 잠재적인 문제가 있습니다. Promise를 사용하기 시작하는 사람들에게 전형적인 오류입니다.
라인 (*)
을 보십시오. 아바타 표시가 완료되고 제거된 후에 어떻게 할 수 있습니까? 예를 들어, 해당 사용자나 다른 항목을 편집하기 위한 양식을 표시하고 싶습니다. 지금으로서는 방법이 없습니다.
체인을 확장 가능하게 만들려면 아바타 표시가 끝나면 해결되는 약속을 반환해야 합니다.
이와 같이:
가져오기('https://javascript.info/article/promise-chaining/user.json') .then(응답 => response.json()) .then(사용자 => 가져오기(`https://api.github.com/users/${user.name}`)) .then(응답 => response.json()) .then(githubUser => new Promise(function(해결, 거부) { // (*) img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "약속-아바타-예제"; document.body.append(img); setTimeout(() => { img.remove(); 해결(githubUser); // (**) }, 3000); })) // 3초 후에 트리거됩니다. .then(githubUser => Alert(`${githubUser.name} 표시가 완료되었습니다`));
즉, (*)
줄의 .then
핸들러는 이제 setTimeout
(**)
에서 resolve(githubUser)
호출 이후에만 해결되는 new Promise
반환합니다. 체인의 다음 .then
이를 기다립니다.
모범 사례로서 비동기 작업은 항상 약속을 반환해야 합니다. 이를 통해 이후의 조치를 계획하는 것이 가능해집니다. 지금 체인을 확장할 계획이 없더라도 나중에 필요할 수 있습니다.
마지막으로 코드를 재사용 가능한 함수로 나눌 수 있습니다.
함수 loadJson(url) { 가져오기(url) 반환 .then(응답 => response.json()); } 함수 loadGithubUser(이름) { return loadJson(`https://api.github.com/users/${name}`); } 함수 showAvatar(githubUser) { return new Promise(함수(해결, 거부) { img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "약속-아바타-예제"; document.body.append(img); setTimeout(() => { img.remove(); 해결(githubUser); }, 3000); }); } // 사용하세요: loadJson('https://javascript.info/article/promise-chaining/user.json') .then(사용자 => loadGithubUser(user.name)) .then(showAvatar) .then(githubUser => Alert(`${githubUser.name} 표시가 완료되었습니다`)); // ...
.then
(또는 catch/finally
상관 없음) 핸들러가 Promise를 반환하면 체인의 나머지 부분은 Promise가 완료될 때까지 기다립니다. 그럴 경우 해당 결과(또는 오류)가 추가로 전달됩니다.
전체 그림은 다음과 같습니다.
이 코드 조각은 동일합니까? 즉, 어떤 핸들러 기능에 대해서도 어떤 상황에서도 동일한 방식으로 작동합니까?
promise.then(f1).catch(f2);
대:
promise.then(f1, f2);
짧은 대답은 다음과 같습니다. 아니요, 동일하지 않습니다 .
차이점은 f1
에서 오류가 발생하면 여기에서 .catch
에 의해 처리된다는 것입니다.
약속하다 .그러면(f1) .catch(f2);
…하지만 여기서는 그렇지 않습니다.
약속하다 .then(f1, f2);
그 이유는 오류가 체인 아래로 전달되고 두 번째 코드 조각에는 f1
아래에 체인이 없기 때문입니다.
즉, .then
결과/오류를 다음 .then/catch
로 전달합니다. 따라서 첫 번째 예에는 아래에 catch
가 있고 두 번째 예에는 없으므로 오류가 처리되지 않습니다.