new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
(*)
.then
핸들러가 호출 = (**)
.then
핸들러에 전달 = (****)
프라미스 체이닝이 가능한 이유는 promise.then
을 호출하면 프라미스가 반환되기 때문이다
반환된 프라미스에는 당연히 .then
을 호출할 수 있고 핸들러가 값을 반환할 때엔 이 값이 프라미스의 result
가 된다
👉 다음 .then
은 이 값을 이용하여 호출된다
하나의 프라미스에 .then
을 여러 개 추가하는 것은 체이닝이 아니다
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
예시의 프라미스는 하나이지만 여기에 등록된 핸들러는 여러 개이다
이 핸들러들은 result
를 순차적으로 전달하지 않고 독립적으로 처리한다
then(handler)
에 사용된 핸들러가 프라미스를 생성하거나 반환하는 경우가 있다
이 경우 이어지는 핸들러는 프라미스가 처리될 때 까지 기다리다가 처리가 완료되면 그 결과를 받는다
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return new Promise((resolve, reject) => { // (*)
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) { // (**)
alert(result); // 2
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) {
alert(result); // 4
});
첫 번째 .then
은 1
을 출력하고 new Promise(..)
를 반환한다 = (*)
1초 후 이 프라미스가 이행되고 그 결과((**)
)는 2
를 출력하고 동일한 과정이 반복된다
👉 핸들러 안에서 프라미스를 반환하는 것도 비동기 작업 체이닝을 가능하게 한다
loadScript("/article/promise-chaining/one.js")
.then(function(script) {
return loadScript("/article/promise-chaining/two.js");
})
.then(function(script) {
return loadScript("/article/promise-chaining/three.js");
})
.then(function(script) {
// 불러온 스크립트 안에 정의된 함수를 호출해
// 실제로 스크립트들이 정상적으로 로드되었는지 확인
one();
two();
three();
});
순차적으로 스크립트를 불러오는 위 코드를 화살표 함수를 사용하면 아래와 같이 줄일 수 있다
loadScript("/article/promise-chaining/one.js")
.then(script => loadScript("/article/promise-chaining/two.js"))
.then(script => loadScript("/article/promise-chaining/three.js"))
.then(script => {
// 스크립트를 정상적으로 불러왔기 때문에
// 스크립트 내의 함수를 호출가능
one();
two();
three();
});
loadScript
를 호출할 때마다 프라미스가 반환되고 다음 .then
은 프라미스가 이행되었을 때 실행된다
이후 다음 스크립트를 로딩하기 위한 초기화가 진행되고 스크립트는 이런 과정을 거쳐 순차적으로 로드된다
체인에 더 많은 비동기 동작을 추가할 수도 있는데, 추가 작업이 많아져도 코드가 오른쪽으로 길어지지 않고 아래로만 증가해서 활 모양의 코드가 만들어지지 않는다
loadScript("/article/promise-chaining/one.js").then(script1 => {
loadScript("/article/promise-chaining/two.js").then(script2 => {
loadScript("/article/promise-chaining/three.js").then(script3 => {
// 여기서 script1, script2, script3에 정의된 함수를 사용가능
one();
two();
three();
});
});
});
혹은 이와 같이 각 loadScript
에 .then
을 바로 붙일 수도 있지만 코드가 오른쪽으로 길어지는 문제가 발생한다
핸들러는 프라미스가 아닌 thenable
이라는 객체를 반환하기도 한다
.then
이라는 메서드를 가진 객체는 모두 thenable
객체라고 부르는데, 이 객체는 프라미스와 같은 방식으로 처리된다
'thenable' 객체에 대한 아이디어는 서드파티 라이브러리가 '프라미스와 호환 가능한' 자체 객체를 구현할 수 있다는 점에서 나왔다
이 객체들엔 자체 확장 메서드가 구현되어 있겠지만 .then
이 있으므로 네이티브 프라미스와도 호환 가능하다
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve); // function() { 네이티브 코드 }
// 1초 후 this.num*2와 함께 이행됨
setTimeout(() => resolve(this.num * 2), 1000); // (**)
}
}
new Promise(resolve => resolve(1))
.then(result => {
return new Thenable(result); // (*)
})
.then(alert); // 1000밀리 초 후 2를 보여줌
프론트에서는 네트워크 요청 시 프라미스를 자주 사용한다
let promise = fetch(url);
위 코드를 실행하면 url
에 네트워크 요청을 보내고 프라미스를 반환한다
원격 서버가 헤더와 함께 응답을 보내면, 프라미스는 response
객체와 함께 이행된다
그런데 이때 response 전체가 완전히 다운로드 되기 전에 프라미스는 이행 상태가 되어버린다
fetch('/article/promise-chaining/user.json')
// 원격 서버가 응답하면 .then 아래 코드가 실행
.then(function(response) {
// response.text()는 응답 텍스트 전체가 다운로드되면
// 응답 텍스트를 새로운 이행 프라미스를 만들고, 이를 반환
return response.text();
})
.then(function(text) {
// 원격에서 받아온 파일의 내용
alert(text); // {"name": "jinju", "isAdmin": true}
});
응답이 완전히 종료되고, 응답 전체를 읽으려면 메서드 response.text()
를 호출해야 한다
response.text()
는 원격 서버에서 전송한 텍스트 전체가 다운로드되면, 이 텍스트를 result
값으로 갖는 이행된 프라미스를 반환한다
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => alert(user.name));
메서드 response.json()
을 쓰면 원격에서 받아온 데이터를 읽고 JSON으로 파싱할 수 있다
// user.json에 요청을 보냄
fetch('/article/promise-chaining/user.json')
// 응답받은 내용을 json으로 불러옴
.then(response => response.json())
// GitHub에 요청을 보냄
.then(user => fetch(`https://api.github.com/users/${user.name}`))
// 응답받은 내용을 json 형태로 불러옴
.then(response => response.json())
// 3초간 아바타 이미지(githubUser.avatar_url)를 보여줌
.then(githubUser => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => img.remove(), 3000); // (*)
});
만약 아바타가 잠깐 보였다가 사라진 이후에 무언가를 하고 싶다면 아래 코드와 같이
체인을 확장할 수 있도록 아바타가 사라질 때 이행 프라미스를 반환해주어야 한다
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise(function(resolve, reject) { // (*)
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser); // (**)
}, 3000);
}))
// 3초 후 동작함
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
(*)
의 .then
핸들러는 이제 setTimeout
안의 resolve(githubUser)
를 호출했을 때 ((**))
만 처리상태가 되는 new Promise
를 반환하고 체인의 다음 .then
은 이를 기다린다
비동기 동작은 항상 프라미스를 반환하도록 하는 것이 좋다
당장은 체인을 확장할 계획이 없더라도 이렇게 구현해 놓으면 나중에 체인 확장이 필요한 경우 손쉽게 체인을 확장할 수 있다
위 함수를 재사용 가능한 함수 단위로 분리할 수 있다
function loadJson(url) {
return fetch(url)
.then(response => response.json());
}
function loadGithubUser(name) {
return fetch(`https://api.github.com/users/${name}`)
.then(response => response.json());
}
function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
});
}
// 함수를 이용하여 다시 동일 작업 수행
loadJson('/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
// ...
.then
이나 .catch
, .finally
의 핸들러가 프라미스를 반환하면, 나머지 체인은 프라미스가 처리될 때 까지 대기한다
처리가 완료되면 프라미스의 result
(값 또는 에러)가 다음 체인으로 전달된다