16. Promise
16-1. 소개
Callback Hell
- id가 ‘btn’인 button을 클릭하면 서버에 users 리스트를 가져오는 요청을 하고,
- 성공하면 list의 세번째 user의 정보를 다시 요청하여
- 성공하면 user의 profileImage url값을 가져다가 image 태그로 표현하고,
- 이 image를 클릭하면 해당 이미지를 제거.
const script= document.createElement('script')
script.src= 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js'
document.body.appendChild(script)
document.body.innerHTML += '<button id="btn">클릭</button>'
document.getElementById('btn').addEventListener('click', function (e) {
$.ajax({
method: 'GET',
url: 'https://api.github.com/users?since=1000',
success: function (data) {
var target = data[2]
$.ajax({
method: 'GET',
url: 'https://api.github.com/user/' + target.id,
success: function (data) {
var _id = 'img' + data.id
document.body.innerHTML += '<img id="' + _id + '" src="' + data.avatar_url + '"/>'
document.getElementById(_id).addEventListener('click', function (e) {
this.remove()
})
},
error: function (err) {
console.error(err)
}
})
},
error: function (err) {
console.error(err)
}
})
})
이렇게 옆으로 파고드는 구조보다는 순차적인 시퀀스가 좋다.
그래서 등장한 게 Promise
Promise
document.body.innerHTML = '<button id="btn">클릭</button>'
document.getElementById('btn').addEventListener('click', function (e) {
fetch('https://api.github.com/users?since=1000')
.then(function (res) { return res.json() })
.then(function (res) {
var target = res[2]
return fetch('https://api.github.com/user/' + target.id)
})
.then(function (res) { return res.json() })
.then(function (res) {
var _id = 'img' + res.id
document.body.innerHTML += '<img id="' + _id + '" src="' + res.avatar_url + '"/>'
document.getElementById(_id).addEventListener('click', function (e) {
this.remove()
})
})
.catch(function (err) {
console.error(err)
})
})
.then을 사용해서 동작이 끝나면 다음 동작으로, 순차적인 흐름으로 변경
Promise를 반환하면서 JSON parsing을 자동으로 해주는 library (axios) 활용시
const script= document.createElement('script')
script.src= 'https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js'
document.body.appendChild(script)
document.body.innerHTML += '<button id="btn">클릭</button>'
document.getElementById('btn').addEventListener('click', function (e) {
axios.get('https://api.github.com/users?since=1000')
.then(function (res) {
var target = res.data[2]
return axios.get('https://api.github.com/user/' + target.id)
})
.then(function (res) {
var _id = 'img' + res.data.id
document.body.innerHTML += '<img id="' + _id + '" src="' + res.data.avatar_url + '"/>'
document.getElementById(_id).addEventListener('click', function (e) {
this.remove()
})
})
.catch(function (err) {
console.error(err)
})
})
.then(function (res) { return res.json() })
반복되는 이 부분을 axios로 한번에 처리
then으로 콜백함수를 계속 넘기는 패턴은 아직 남아있지만, then 사용 전보다는 콜백지옥을 해결.
16-2. 상세
then과 catch 메소드가 prototype
안에 있다.
promise를 하나의 클래스로 생각을 하면,
promise는 생성자 함수로 (Class로) 만든 인스턴스로서 prototype 상으로 then과 catch 메소드에 접근이 된다.
그 외의 all, reject, resolve 이런 메소드는 promise 자체에 내장된 static 메소드
!
Promise.all (O)
new Promise().all (X) 인스턴스로 만들면 접근 불가능
16-2-1. Promise Status
비동기 처리를 해두면 알아서 상태 처리를 해서 돌려준다.
-
unnsettled (미확정) 상태: pending.
thenable
하지 않다. 어떤 요청이 있어서 이게 비동기적으로 처리가 될건데, 그 비동기처리 과정이 끝나기 전 상태 -
settled (확정) 상태: resolved.
thenable
한 상태. 비동기 처리가 끝나고 나면 상태가 settled로 바뀐다- fulfilled (성공)
- rejected (실패)
const a = new Promise()
이렇게만 해도 인스턴스를 만들 수 있지만 매개변수를 넘길 수 있음.
const a = new Promise(function(성공시호출함수, 실패시호출함수) {
// 실제 동작을 구현하고,
// 이 동작이 성공하면 성공시호출함수,
// 실패하면 실패시호출함수를 내부에서 구현해두면 된
})
그렇게 보면
const promiseTest = param => new Promise((resolve, reject) => {
setTimeout(() => {
if (param) {
resolve("해결 완료")
} else {
reject(Error("실패!!"))
}
}, 1000)
})
promiseTest라는 함수는 param을 받아서,
Promise의 인스턴스를 만든 걸 반환하고 있다
그럼 promiseTest에 param을 넘겨서 실행한 결과값은 아래와 동일.
const a = new Promise(function(성공시호출함수, 실패시호출함수) {
// 실제 동작을 구현하고,
// 이 동작이 성공하면 성공시호출함수,
// 실패하면 실패시호출함수를 내부에서 구현해두면 된
})
const promiseTest = param => new Promise((resolve, reject) => {
setTimeout(() => {
if (param) {
resolve("해결 완료")
} else {
reject(Error("실패!!"))
}
}, 1000)
})
const a = promiseTest(true)
a
// Promise {<resolved>: "해결 완료"}
new Promise로 인스턴스를 만드는 순간에 내부함수를 실행
한다.
그럼 그 내부에서는,
setTimeout을 돌면서 1초 뒤에 안에 있는 내용을 실행
그때에 넘겨받은 true로 인해 resolve
가 호출됐고,
resolve에 의해서 promise의 전체상태가 unsettled에서 settled
로 변경되고
그 결과가 fulfilled가 됨
const promiseTest = param => new Promise((resolve, reject) => {
setTimeout(() => {
if (param) {
resolve("해결 완료")
} else {
reject(Error("실패!!"))
}
}, 1000)
})
const b = promiseTest(false)
// Uncaught (in promise) Error: 실패!!
b
// Promise {<reject>: "실패!!"}
성공시에는 resolved라고 나왔는데 왜 reject라고 나올까?
=> 브라우저마다 다름!
크롬에서는 이렇게 나오지만 파이어폭스에서는 fulfilled라고 나옴.
성공했음을 대체할 때도 있고, unnsettled에서 settled로 넘어갔음을 표시할 때도 있으니 문맥에 따라 파악하는 수밖에 없음.
const testRun = param => promiseTest(param)
// promiseTest로 만든애는 promise의 인스턴스.
// 인스턴스이기 때문에 prototype상의 then과 catch를 쓸 수 있다
.then(text => { console.log(text) })
.catch(error => { console.error(error) })
// resolve가 실행된 순간 then이 실행
// reject가 실행되면 catch로
const promiseTest = (param, delay) => new Promise((resolve, reject) => {
setTimeout(() => {
if (param) {
resolve("해결 완료")
} else {
reject(Error("실패!!"))
}
}, delay)
})
const testRun = param => promiseTest(param, delay)
.then(text => { console.log(text) })
.catch(error => { console.error(error) })
const a = testRun(true,1000)
const b = testRun(false,2000)
// 해결완료
// 1초 후
// Error: 실패!! at setTimeout
그 후 변수들을 다시 호출하면
a
// Promise{ resolved: undefiled}
b
// Promise{ resolved: undefiled}
a는 여전히 promise의 인스턴스로 resolved 상태
b도 여전히 promise의 인스턴스로 resolved 상태
b는 catch로 에러가 난 상태인데도 왜 resolved지?
반환한 것 없이 그냥 resolve처리를 시킨것.
a.then(() => { return 1; })
// Promise { <resolved>: 1 }
a.then(res => { console.log(res) })
// undefined
// Promise { <resolved>: undefined }
a.then(() => { return 1; })
.then(res => { console.log(res) })
1
// Promise { <resolved>: undefined }
끝난 것 같지만 다시 내가 원할 때 이어서 then을 또 할 수 있다.
한번 promise는 영원한 promise! 영원히 이어서 갈 수 있다
promise를 중단시킬 수 없다는 점에서 한계.
16-2-2. 문법
new Promise(function)
.then()
,.catch()
는 언제나 promise를 반환한다.
const executer = (resolve, reject) => { ... }
const prom = new Promise(executer)
const onResolve = res => { ... }
const onReject = err => { ... }
// (1)
prom.then(onResolve, onReject)
// (2)
prom.then(onResolve).catch(onReject)
(1)
prom.then(onResolve, onReject)
then안에서 성공시 함수, 실패시 함수를 만들 수 있다
const testRun = (param,delay) => promiseTest(param, delay)
.then(text => { console.log(text) },
error => { console.error(error)}
);
then catch를 나눠서 할 수도 있고, 아래처럼 한번에 할 수도 있음.
new Promise((resolve, reject) => { ... })
.then(res => { ... })
.catch(err => { ... })
const simplePromiseBuilder = value => {
return new Promise((resolve, reject) => {
if(value) { resolve(value) }
else { reject(value) }
})
}
// value를 넘겨받고 promise의 인스턴스를 반환
simplePromiseBuilder(1)
.then(res => { console.log(res) })
.catch(err => { console.error(err) })
simplePromiseBuilder(0)
.then(res => { console.log(res) })
.catch(err => { console.error(err) })
전혀 simple하지 않네! then이 똑같은 내용을 반복하고 있어.
const simplePromiseBuilder2 = value => {
return new Promise((resolve, reject) => {
if(value) { resolve(value) }
else { reject(value) }
})
.then(res => { console.log(res) })
.catch(err => { console.error(err) })
}
simplePromiseBuilder2(1)
simplePromiseBuilder2(0)
이렇게 애초에 promise를 반환할 때, 그 안에 then과 catch를 반환시키면 된다.
simplePromiseBuilder2(0).then(res => {console.log('이어서 하고싶으면 함수호출시에 적으세염')})
const a = simplePromiseBuilder2(1)
a.them(res => { console.log('리턴해주면 계속 덴덴덴 갈 수 있어요')})
const prom = new Promise((resolve, reject) => {
resolve()
reject()
console.log('Promise')
})
// Promise 출력 된다
prom.then(() => {
console.log('then')
})
prom.catch(() => {
console.log('catch')
})
console.log('Hi!')
// Promise
// Hi
// then
출력순서
- 1) Promise
- 2) Hi
- 3) then
실행 Queue : 어떤 작업을 수행하고, 그 다음 이어서 수행하고….
전체소스 실행하는 과정에서 Promise 인스턴스의 함수도 같이 실행되었다.
-> 그로인해 pending에서 fulfilled가 됨
-> fulfilled가 되면서 then함수가 queue에 추가됨.
-> 계속 전체소스실행이 끝나고 나서
(하나의 큐 끝)
-> 다음번큐에 있는 then함수가 실행됨.
=> reject는 무시되었네??
const prom = new Promise((resolve, reject) => {
reject()
resolve()
console.log('Promise')
})
prom.then(() => {
console.log('then')
})
prom.catch(() => {
console.log('catch')
})
console.log('Hi!')
// Promise
// Hi
// catch
순서를 바꾸면?
이번엔 then으로 안가고 catch로만 왔따!
=>
- then이나 catch 구문은 실행큐에 후순위로 등록되고 실행된다
- promise 인스턴스에 넘긴 함수 내부에서는, resolve나 reject 둘 중에 먼저 호출한 것만 실제로 실행된다.
- 사실은 실제로 실행 안되는 게 아니라, 실행은 둘다 되는데, pending 상태일 때만 의미가 있기 때문에 이런 결과가 나온 것
- reject를 실행하면 이 prom이라는 인스턴스의 promise 상태가
pending
=>resolve(rejected)
상태로 변경 - 이미 resolved 상태가 된 promise에게 다시 resolve를 하라고 할 수 없음
- 이미 첫번째 promise가 끝난 상태이기 때문에 어떤 명령을 받을 수 없다. 그래서 처리하지 않는 것
- reject에서 끝내서 리턴해버리지 않고 resolve가 실행이 되었기 때문에 아래에 있는 console.log가 실행됐겠지!
- reject를 실행하면 이 prom이라는 인스턴스의 promise 상태가
=> reject
든 resolve
든, pending
상태에서만 호출할 수 있다
이 promise상태를 먼저 끝내서 pending 상태가 종료되면, 다음에 오는 reject또는 resolve는 의미가 사라진다
결론 )
reject() // -실행 O, Promise 상태 종료
resolve() // - 실행 O, 종료됐으니 의미 X
console.log('Promise') // - 실행 O
아래처럼 반대의 경우도 마찬가지
결론 )
resolve() // - 실행 O, Promise 상태 종료
reject() // -실행 O, 종료됐으니 의미 X
console.log('Promise') // - 실행 O
const prom = new Promise((resolve, reject) => {
reject()
resolve()
console.log('Promise')
})
prom.then(() => {
console.log('then')
})
prom.catch(() => {
console.log('catch')
})
console.log('Hi!')
// 1)
new Promise((resolve, reject) => {
// 내용을 복잡하게
resolve(10)
})
// Promise {<resolved>:10}
// 2)
Promise.resolve(10)
// Promise {<resolved>:10}
1과 2는 동일한 결과.
1은 함수를 실행해서 전체적인 플로우를 모두 실행시킨 뒤에 반환
2는 그 자체로써 10을 보냄
왜 2처럼 쓸까?
thenable
하게 만들고 싶어서!
Promise.resolve(10).then(res => {})
처음부터 10이 resolve됨.
const a = val => Promise.resolve(val)
.then(res => {
console.log(res)
})
a(10)
// 10
// Promise{<resolve>: undefined}
16-2-3. 확장 Promise 만들기
Promise.resolve
,Promise.reject
Promise.resolve(42)
.then(res => { console.log(res) })
.catch(err => { console.error(err) })
Promise.reject(12)
.then(res => { console.log(res) })
.catch(err => { console.error(err) })
- thenable 객체
그냥 객체인데, then이라는 메소드가 있고,
resolve, reject 함수를 호출할 수 있게 되어있으면
thenable
하다
const thenable = {
then (resolve, reject) {
resolve(33)
}
}
const prom = Promise.resolve(thenable)
prom.then(res => { console.log(res) })
// Promise {<resolved>: 33}
prom에는 thenable한 객체가 와있을 것 같았는데,
객체가 와있지 않고 33이 와있네?
Promise.resolve
에 어떠한 값을 넘겨줄 때
- thenable한 객체를 넘겨주면?
- 거기있는 then 메소드를 호출해서 resolve된 것을 반환
- 거기있는 then 메소드를 호출해서 resolve된 것을 반환
- 일반적인 값을 넘겨주면?
- 그냥 그 값을 resolve 상태로 만든다
- 그냥 그 값을 resolve 상태로 만든다
그래서 then이 이미 실행되었기 때문에 33이 온다.
const thenable = {
then (resolve, reject) {
reject(33)
}
}
const prom = Promise.resolve(thenable)
prom.catch(err => { console.log(err) })
이것처럼 then을 태울 수 있는 것들은 전부 thenable하다
promise에서 반환되는 것들은 모두 thenable하다
const thenable = {
then() {
return 10;
}
}
const prom = Promise.resolve(thenable)
prom
// Promise {<pending>}
then 메소드는 있지만 resolve와 reject를 함수를 호출시키지 않으면?
then을 타려고 했는데 그 안에서 resolve를 안시키니까
계속 끝날 수가 없는 상태로 멈춰있다
=> thenable하지 ❌
then 메소드가 제대로 구현되어있지 않음
const thenable = {
then(resolve) {
resolve(10);
}
}
const prom = Promise.resolve(thenable)
prom
// Promise {<resolved>: 10}
이렇게 반드시 resolve를 만들어줘야함.
const thenable = {
then(a) {
a(10);
}
}
const prom = Promise.resolve(thenable)
prom
// Promise {<resolved>: 10}
이름 바꿔도 상관없음.
const thenable = {
then(a,b) {
b(10);
}
}
const prom = Promise.resolve(thenable)
prom
// Uncaught ( in promise) 10
실패하게 하려면 이렇게
16-2-4. Promise Chaning (then, catch에서 return)
⭐️ promise에서 제일 중요한 promise chaning! ⭐️
아까 배웠듯이
promise의 인스턴스가 resolve 되면, 그 뒤에 then을 탐.
그럼 그 이후로 영원히 then을 태울 수 있음
then함수에서의
return
거기서 뭘 return 해주면 return한 것 그 자체가
다음번 then을 탈 때resolve의 결과
로 들어간다!
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('첫번째 프라미스')
}, 1000)
// 1초 뒤 promise의 상태를 pending에서 resolved로 바꿔, 값은 '첫번째 프라미스'
}).then(res => {
console.log(res) // '첫번째 프라미스'
return '두번째 프라미스'
// 이미 promise resolve가 된 상태인데, 다음번 then을 타면
// 다시 pending이 됐다가 return한 것 그자체가 resolve의 결과로 나옴
// 경우 2번의 일반값이니까 다음번 then을 탈 수 있음
}).then(res => {
console.log(res) // '두번째 프라미스'
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('세번째 프라미스')
}, 1000)
})
// 경우 1번. 이 자체가 thenable하게 될 때 다음번 then을 탐.
// 새로운 promise의 인스턴스를 return, 1초 뒤에 resolve => thenable이 됨
}).then(res => {
console.log(res) // 1초 후, '세번째 프라미스'
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('네번째 프라미스')
}, 1000)
})
// resolve든 reject든 할 때 thenable하게 됨
// 다음번 then을 무시하고 catch로 감.
}).then(res => {
console.log(res)
}).catch(err => {
console.error(err) // 네번째 프라미스가 에러로 나옴
return new Error('이 에러는 then에 잡힙니다.')
// then, catch에서 return하면 인스턴스 리턴이 아닌 경우에는 경우2번으로 일반값
// promise의 resolve 상태로 값으로 넘어간다 => then으로
}).then(res => {
console.log(res)
throw new Error('이 에러는 catch에 잡힙니다.')
// throw : 현재상태를 중단하고 에러메시지를 반환함. => then 무시하고 catch로
}).then(res => {
console.log('출력 안됨')
}).catch(err => {
console.error(err)
})
⭐️ .then이나 catch안에서
- 경우1. return promise
인스턴스
: promise 인스턴스가 리턴된 것
- return된 애도 promise
- 실행결과가 언젠가 pending에서 resolve로 바뀜
- 그럼 다시 그 다음 then을 탈 수 있음
- 경우2. return
일반값
: promise 객체에 resolved 상태로 반환. 그 안에 값이 담김
- Promise {
: 값 } 이렇게
- Promise {
- 경우3. return 안하면 : return undefined (원래 JS 동작이 이러함)
- undefined가 곧 일반값. = 2번과 같다
- 경우4. Promise.resolve() 또는 Promise.reject()
- return하지 않는 이상 의미없음! (return하면 1번과 같다)
- 그냥 새로운 promise일 뿐, 내가 지금 잇고있는 시퀀스와는 완전히 별개.
- 다시말해 .then(() -> {}).then(() -> {}) 에 영향주지 ❌
결과!
16-2-5. Error Handling
asyncThing1()
.then(asyncThing2)
.then(asyncThing3)
.catch(asyncRecovery1)
.then(asyncThing4, asyncRecovery2)
.catch(err => { console.log("Don't worry about it") })
.then(() => { console.log("All done!") })
16-2-6. Multi Handling
1. Promise.all()
- iterable의 모든 요소가 fulfilled되는 경우: 전체 결과값들을 배열 형태로 then에 전달.
- iterable의 요소 중 일부가 rejected되는 경우: 가장 먼저 rejected 되는 요소 ‘하나’의 결과를 catch에 전달.
const arr = [
1,
new Promise((resolve, reject) => {
setTimeout(()=> {
resolve('resolved after 1000ms')
}, 1000)
}),
'abc',
() => 'not called function',
(() => 'IIFE')()
]
Promise.all(arr)
.then(res => { console.log(res) })
.catch(err => { console.error(err) })
const arr = [
1,
new Promise((resolve, reject) => {
setTimeout(()=> {
reject('rejected after 1000ms')
}, 1000)
}),
'abc',
()=> 'not called function',
(()=> 'IIFE')()
]
Promise.all(arr)
.then(res => { console.log(res) })
.catch(err => { console.error(err) })
2. Promise.race()
- iterable의 요소 중 가장 먼저 fulfilled / rejected되는 요소의 결과를 then / catch에 전달.
const arr = [
new Promise(resolve => {
setTimeout(()=> { resolve('1번요소, 1000ms') }, 1000)
}),
new Promise(resolve => {
setTimeout(()=> { resolve('2번요소, 500ms') }, 500)
}),
new Promise(resolve => {
setTimeout(()=> { resolve('3번요소, 750ms') }, 750)
})
]
Promise.race(arr)
.then(res => { console.log(res) })
.catch(err => { console.error(err) })
const arr = [
new Promise(resolve => {
setTimeout(()=> { resolve('1번요소, 0ms') }, 0)
}),
'no queue'
]
Promise.race(arr)
.then(res => { console.log(res) })
.catch(err => { console.error(err) })