JS setTimeout
같은 함수를 통해 비동기 처리를 할 수 있다.
비동기 함수가 실행 된 이후를 보장하기 위해 callback
을 사용한다.
function loadScript(src, callback){
const script = document.createElement('script')
script.src = src;
script.onload = () => callback(null, script)
script.onerror = () => callback(new Error(`Failed to load ${src}`))
document.head.append(script)
}
위처럼 callback
함수로 loadScript
완료 이후의 실행을 보장할 수 있다.
하지만 그런 비동기 작업이 연속적으로 이어져야 하는 경우 다음과 같은 콜백 지옥을 만날 수 있다.
loadScript(srcA, () => {
loadScript(srcB, () => {
loadScript(srcC, () => {
loadScript(srcD, () => {
loadScript(srcE, () => {
// ...
})
})
})
})
})
위의 방법을 해결하는 여러 방법 중 한가지가 Promise 이다.
Promise 는 기본적으로 아래와 같이 사용한다.
const promise = new Promise((resolve, reject) => {
// ...
})
/*
promise : {
state: "pending",
result: undefined
}
*/
위에서 Promise에 전달되는 함수를 실행자 함수 라고 표현하고, 인수는 resolve
와 reject
를 함수로 받는다.
실행자 함수에서는 결과를 언제 얻든 무조건 두 인수 중 하나를 실행해야 한다.
성공적으로 실행된 경우 resolve(value)
를, 실패한 경우 reject(error)
를 실행하면 된다.
new Promise
를 실행하게 되면 주석 속 promise
객체를 받게 된다.
이는 resolve
나 reject
실행 여부에 따라 값이 변한다.
//성공한 경우
const resolved = {
state: "fullfilled",
result: value
}
//실패한 경우
const rejected = {
state: "rejected",
result: error
}
Promise에서 실행사 함수의 결과를 받는 방법으로는 then
, catch
, finally
같은 소비 함수를 필요로 한다.
resolve
가 호출되었을 때 호출된다.
인자는 최대 두 개 까지 받을 수 있으며 인자 순서는 성공 시 호출 함수, 실패 시 호출 함수 순이다.
reject
가 호출되었을 때 단순히 에러만 받고 싶은 경우 호출된다.
.then(null, callback)
은 .catch(callback)
과 동일하게 동작한다.
try {...} catch {...}
의 finally
같은 역할을 한다.
.then(callback, callback)
은 .finally(callback)
과 비슷하게 동작하지만 차이점이 있다.
finally
에서는 성공 여부를 알 수 없으며, 다음 핸들러에 결과와 에러를 전달한다.
new Promise((resolve) => {
setTimeout(() => resolve('done'), 1000)
})
.finally(() => console.log('finally'))
.then(console.log)
/*
finally
done
위 처럼 출력되며 'done' 이 finally에서 then으로 전달된다.
*/
Promise에서는 then
에서 thenable
을 반환하기 때문 then
을 계속 이어 사용할 수 있다.
return
을 주게 되면 다음 then
에서는 인자도 받을 수 있다.
return
에는 값이나 새로운 Promise가 올 수 있다.
이 때 모든 Promise는 thenable
이지만 모든 thenable
이 Promise는 아니다.
// 0, 1, 2 를 출력한다.
new Promise((res) => {
let e = 0
console.log(e++)
res(e)
}).then((e ) => {
console.log(e++)
return e
}).then(e => {
console.log(e++)
return e
})
// 0, 1, 2 를 1초 간격으로 호출한다.
new Promise((resolve) => {
console.log(0)
setTimeout(() => resolve(1), 1000);
}).then((e) => {
console.log(e)
return new Promise((resolve) => {
setTimeout(() => resolve(e+1), 1000);
})
}).then((e) => {
console.log(e)
return new Promise((resolve) => {
setTimeout(() => resolve(e+1), 1000);
})
})
Promise 에서 에러를 핸들링 하기 위해 catch
를 사용한다.
catch
는 항상 첫 핸들러일 필요가 없으며, 여러 개의 then
뒤에 올 수 있다.
여러 then
체인 뒤에 catch
가 있다면, 하나라도 then
이 실패했을 때 catch
는 호출된다.
function someFunction(){} // new Promise를 리턴하는 함수
// 아래에서는 someFunction1-3 중에 하나라도 오류가 나면 catch의 catchFunction이 호출된다.
someFunction()
.then(someFunction1())
.then(someFunction1())
.then(someFunction1())
.catch(catchFunction())
Promise의 실행자와 핸들러 코드에는 암시적 try/catch
가 있다.
// 아래 두 코드는 동일하게 동작한다.
new Promise((resolve, reject) => {
throw new Error("에러 발생!");
}).catch(console.log); // Error: 에러 발생!
new Promise((resolve, reject) => {
reject(new Error("에러 발생!"));
}).catch(console.log); // Error: 에러 발생!
암시적 try/catch
는 명시적인 오류 뿐만 아니라, 핸들러 위에서 발생한 다른 오류도 잡는다.
new Promise((resolve, reject) => {
resolve("OK");
}).then((result) => {
blabla(); // 존재하지 않는 함수
}).catch(console.log); // ReferenceError: blabla is not defined
catch
에 의해서 오류를 다시 수정하고, 그 결과에 따라 다시 then
이나 catch
로 받을 수 있습니다.
const rethrowTest = (fixable) => new Promise((resolve, reject) => reject(fixable))
// 오류가 여기서 한번 처리된다.
.catch(e => {
if(e) {
return 'resolved'
}
throw new Error('rejected')
}) // 수정 여부에 따라 return 이 되면 then이, throw가 되면 catch가 실행된다.
.then(console.log)
.catch(console.log)
rethrowTest(true) // resolved
rethrowTest(false) // Error: rejected...
만약 catch
또는 then
의 두 번째 인자로 오류를 처리하지 않는 경우 Promise는 전역 오류를 생성한다.
그 자리에서 catch
를 넣어 잡는 것이 가장 최선이겠지만 아래 방법으로 처리할 수 있다.
window.addEventListener('unhandledrejection', function(event) {
console.log(event.promise); // 에러가 생성된 Promise
console.log(event.reason); // 처리되지 못한 오류
});
// 핸들러가 없는 오류 발생
new Promise(function() {
throw new Error("에러 발생!");
});