JS) Promise

kangdari·2020년 4월 10일
1

소개

Callback Hell

아래 코드는 ajax를 이용한 비동기 요청 처리입니다.

버튼 클릭 => 서버에 users 리스트 요청 => 성공 시 list의 첫 번째 user의 정보 요청 => 성공 시 user의 img url을 사용하여 img 요소 추가 => img 클릭 시 img 삭제하는 간단한 코드이다.

콜백 함수가 나올 때마다 코드의 깊이가 깊어지고 있다.

const script = document.createElement('script')
script.src = 'https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js'
document.body.appendChild(script);

document.body.innerHTML += `<button id="btn">클릭</button>`
document.getElementById('btn').addEventListener('click', () => {
    $.ajax({
        method: 'GET',
        url: 'https://api.github.com/users?since=1000',
        success: (data) => {
            const target = data[2]
            $.ajax({
                method: 'GET',
                url: `https://api.github.com/user/${target.id}`,
                success: (data) => {
                    const _id = 'img_' + data.id
                    document.body.innerHTML = `<img id=${_id} src=${data.avatar_url} />`
                    document.getElementById(_id).addEventListener('click', (e) => {
                        e.target.remove()
                    })
                },
                error: (err) => {
                    console.log(err)
                }
            })
        },
        error: (err) => {
            console.log(err)
        }
    })
}) 

Promise 사용

Promise 문법을 사용하면 ajax를 사용했을 때 보다 적은 depth.

document.body.innerHTML = `<button id="btn">클릭</button>`
document.getElementById('btn').addEventListener('click', () => {
    fetch('https://api.github.com/users?since=1000') // html5 API
    .then(res => res.json()) // data 형식을 json으로 변환
    .then(res => {
        const target = res[0]
        return fetch(`https://api.github.com/user/${target.id}`)
    })
    .then(res => res.json())
    .then(res => {
        const _id = `img_${res.id}`
        document.body.innerHTML = `<img id=${_id} src=${res.avatar_url} />`
        document.getElementById(_id).addEventListener('click', (e) => {
            e.target.remove()
        })
    }).catch(err => {
        console.log(err);
    })
})

Axios: Promise를 반환하면서 Json parsing을 자동으로 해주는 라이브러리

.then(res => res.json()) 부분을 자동으로 해줌.

const script = document.createElement('script')
script.src = `https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js`
document.body.appendChild(script);

document.body.innerHTML = `<button id="btn">클릭</button>`
document.getElementById('btn').addEventListener('click', () => {
    axios.get('https://api.github.com/users?since=1000') // html5 api    .then(res => res.json()) // data 형식을 json으로 변환
    .then(res => {
        const target = res.data[0]
        return axios.get(`https://api.github.com/user/${target.id}`)
    })
    .then(res => {
        const _id = `img_${res.data.id}`
        document.body.innerHTML = `<img id=${_id} src=${res.data.avatar_url} />`
        document.getElementById(_id).addEventListener('click', (e) => {
            e.target.remove()
        })
    }).catch(err => {
        console.log(err);
    })
})

Promise의 상태값

Promise의 구조를 살펴보면 prototype에 then, catch, finally 등 메소드가 있습니다.
이러한 메소드들은 Promise를 생성자(클래스)로 만든 인스턴스에서 접근이 가능하며
all 메소드는 static 메소드르 Promise에서 바로 접근이 가능하다.

status

  • unnsettled(미확정) 상태: pending, thenable하지 않다. 비동기 처리가 끝나기 전 단계

  • settled(확정) 상태 : resolved, thenable한 상태.

    • rejected( 요청에 대한 응답 실패 )
    • fullfilled ( 요청에 대한 응답 성공 )

ex) 브라우저마다 사용 단어가 다를 수 있음.

const promiseTest = (param, delay) => new Promise((resolve, reject) => {
    // resolve: 성공 시 함수, reject: 실패 시 함수
    setTimeout(() => {
        if(param){
            resolve('성공')
        }else{
            reject('실패')
        }
    }, delay);
})

const testRun = (param, delay) => promiseTest(param, delay)
    .then(res => console.log(res))
    .catch(err => console.log(err))

const a = testRun(true, 2000) // 성공
const b = testRun(false, 3000) // 실패

문법

  • new Promise(func)
  • .then(), .catch()는 언제나 promise를 반환합니다.
new Promise((resolve, reject) => {...})
    .then(res => { ... })
    .catch(err => { ... })

const simplePromise = value => {
    return new Promise((resolve, reject) => {
        if(value) { resolve(value) }
        else { reject(value) }
    })
    .then(res => console.log(res))
    .catch(err => console.log(err))
}

simplePromise(1) // 1
simplePromise(0) // 0

// then, catch는 promise를 반환하기 때문에 then을 이어 사용이 가능하다.
const a = simplePromise(1)
a.then(res => {console.log('then 이어 쓰기')})
.then(res => console.log('then 더 이어 쓰기'))
.then(res => console.log('then 더더 이어 쓰기'))

확장 Promise

  • Promise.resolve, Promise.reject
// 전체적인 함수를 실행하고 결과를 반환
new Promise((resolve, reject) => {
    // 내용 ~~~
    resolve(42)
}).then(res => console.log(res)) // 42

// 처음부터 식이나 값을 바로 반환, 내용은 올 수 없음.
Promise.resolve(10)
.then(res => console.log(res)) // 10
.catch(err => console.log(err))

const a = val => Promise.resolve(val)
    .then(res => console.log(res));

a(10) // 10

Promise.reject도 마찬가지로 식이나 값을 바로 반환

Promise.reject(20)
.then(res => {console.log(res)})
.catch(err => {console.error(err)}) // 20

  • thenable 객체

thenable 객체가 then 메소드를 가지고 그 메소드가 resolve, reject 함수를 호출 할 수 있게 구현이 되어있는 객체라면 이 객체는 thenable하다. (Duck typing)

const thenable = {
    then( resolve, reject) {
        resolve(10)
    }
}

const a = Promise.resolve(thenable) // 10 , resolved
const b = Promise.resolve(20) // 20 , resolved

const thenable2 = {
    then( resolve, reject) {
        reject(10)
    }
}
const c = Promise.resolve(thenable) // 10 , rejected

Promise Chaining (then, catch에서 return)

.then, .catch 에서
1. return promise 인스턴스: pomise 인스턴스가 리턴됨 // return new Promise() or return Promise.resolve()
2. return 일반값: promise 객체에 resolved 상태로 반환. 그 안에 값이 담김. // Promise {<<resolved: 값>>}
3. return 안하면 : return undefined
4. Promise.resovle() or Promise.reject(): return 해주지 않으면 의미가 없음.
// 별개의 Promise 객체가 생성될뿐, 현재 process상의 Promise 플로우에 영향을 주지 않음.
// return 해준다면 1번과 같음.

    new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('첫번째 프로미스')
        }, 1000);
    })
    .then(res => {
        console.log(res) // 첫번째 프로미스
        return '두번째 프로미스' // return 값은 resolve의 결과로 나옴.
                            // 즉 다음 then에서 res 값
    })
    .then(res => {
        console.log(res) // 두번째 프로미스
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('세번째 프로미스')
            }, 1000);
        })
    })
    .then(res => {
        console.log(res) // 세번째 프로미스
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                reject('네번째 프로미스')
            }, 1000);
        })
    })
    .then(res => { // reject이므로 pass 
        console.log(res) 
    })
    .catch(err =>{
        console.error(err) // 네번째 프로미스
        return new Error('이 에러는 then에 잡힘') // 일반값 반환, thenable한 상태
    })
    .then(res => {
        console.log(res) // 이 에러는 then에 잡힘
        throw new Error('이 에러는 catch에 잡힘')
    })
    .then(res => {
        console.log('출력 안됨')
    })
    .catch(err => {
        console.error(err) // 이 에러는 catch에 잡힘
    })
    	// 첫번째 프로미스
	// 두번째 프로미스
	// 세번째 프로미스
	// 네번째 프로미스
	// Error: 이 에러는 then에 잡힘 at <anonymous>:32:16
	// Error: 이 에러는 catch에 잡힘 at <anonymous>:36:15

Multi Handling

promise.all(iterable)

all or one error

Array.prototype.every()와 비슷: 배열 내 모든 값이 true일 때 true 반환

  • 일반값은 그냥 resolved 됨.

  • iterable의 모든 요소가 fullfilled 되는 경우: 전체 결과값들을 배열 형태로 then에 전달.

  • iterable의 요소 중 일부가 rejected 되는 경우: 가장 먼저 rejected 되는 요소 하나를 catch에 전달.

const arr = [
    1,
    new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('resolved after 1s')
        }, 1000);
    }),
    '123',
    () => 'not called function',
    (()=> 'IIFE') // 즉시 실행 함수
]

Promise.all(arr)
.then(res => console.log(res))
.catch(err => console.error(err))

// [1, "resolved after 1s", "123", ƒ, ƒ]

  • iterable 내의 요소 중 하나가 rejected 되는 경우
const arr = [
    new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('resolved after 1s')
        }, 1000);
    }),
    new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('resolved after 2s')
        }, 2000);
    }),
    '출력 될까???'
]

Promise.all(arr)
.then(res => console.log(res))
.catch(err => console.error(err))

// resolved after 2s

Promise.reac(iterable)

only the fastest one

Array.prototype.some()과 비슷: 배열의 요소 중 하나의 값이 true이면 true 반환

  • 일반값은 그냥 resolved된 값으로 간주.

  • iterable 내의 요소 중에 가장 먼저 resolved(then), rejected(catch) 된 값을 반환.

resolved, rejected 상관없이 가장 빠른 값이 반환된다.

const arr = [
    new Promise(resolve => {
        setTimeout(() => {
            resolve('1번 요소, 1s')
        }, 1000);
    }),
    new Promise(resolve => {
        setTimeout(() => {
            resolve('2번 요소, 0.5s')
        }, 500);
    }),
    new Promise(resolve => {
        setTimeout(() => {
            resolve('3번 요소, 0.1s')
        }, 100);
    }),
]

Promise.race(arr)
.then(res => console.log(res))
.catch(err => console.error(err))

// 3번 요소, 0.1s 

Promise.race()를 사용하면 setTimout을 0s로 설정한 것과 일반 값 중 어떤 값이 출력이 될까?

기본 값이 출력된다. setTimout 함수를 사용하면 이벤트 큐에 들어가므로 사실 상 0s는 아님.

const arr =[
    new Promise(resolve => {
        setTimeout(() => {
            resolve('1번, 0s')
        }, 0);
    }),
    'no queue'
]

Promise.race(arr)
.then(res => console.log(res))
.catch(err => console.error(err))

// no queue

Async / Await

서버 응답 값을 then으로 받을 필요가 없음.

const script = document.createElement('script')
script.src = `https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js`
document.body.appendChild(script);

document.body.innerHTML = `<button id="btn">클릭</button>`
document.getElementById('btn').addEventListener('click', async () => {
    try{
        const res = await axios.get('https://api.github.com/users?since=1000')
        const user = res.data[0]
        const _id = `img_${user.id}`
        document.body.innerHTML = `<img id=${_id} src=${user.avatar_url} />`
        document.getElementById(_id).addEventListener('click', (e) => {
            e.target.remove()
        })    
    }catch(err){
        console.log(err)
    }
})

0개의 댓글