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로만 왔따!

=>

  1. then이나 catch 구문은 실행큐에 후순위로 등록되고 실행된다
  2. promise 인스턴스에 넘긴 함수 내부에서는, resolve나 reject 둘 중에 먼저 호출한 것만 실제로 실행된다.
  3. 사실은 실제로 실행 안되는 게 아니라, 실행은 둘다 되는데, pending 상태일 때만 의미가 있기 때문에 이런 결과가 나온 것
    • reject를 실행하면 이 prom이라는 인스턴스의 promise 상태가 pending => resolve(rejected) 상태로 변경
    • 이미 resolved 상태가 된 promise에게 다시 resolve를 하라고 할 수 없음
    • 이미 첫번째 promise가 끝난 상태이기 때문에 어떤 명령을 받을 수 없다. 그래서 처리하지 않는 것
    • reject에서 끝내서 리턴해버리지 않고 resolve가 실행이 되었기 때문에 아래에 있는 console.log가 실행됐겠지!

=> rejectresolve든, 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 만들기

  1. 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) })
  1. 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된 것을 반환
  • 일반적인 값을 넘겨주면?
    • 그냥 그 값을 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 { : 값 } 이렇게
  • 경우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) })

참고: ES2017 Async Function