let g = 0;
setTimeout(() => { g = 100; }, 0);
console.log(g); // 0 : setTimeout에서 설정한 100이 아닌 0으로 출력된다.
const get = url => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
// 서버의 응답을 반환
return JSON.parse(xhr.response);
}
console.log(error(`${xhr.status} ${xhr.statusText}`);
};
}
// id가 1인 post를 취득
const response = get('https://jsonplaceholder.typicode.com/posts/1');
console.log(response); // undefined 하지만 response는 undefined를 반환
onload 이벤트 핸들러는 비동기로 동작한다.get 함수 호출 되면 XMLHttpRequest 객체 생성 → HTTP 요청 초기화 → 요청 전송 → xhr.onload 이벤트 핸들러 프로퍼티에 이벤트 핸들러를 바인딩 → 종료get 함수에 명시적인 반환문(return)이 없으므로 get 함수는 undefined를 반환한다.xhr.onload 이벤트 핸들러 프로퍼티에 바인딩한 이벤트 핸들러는 언제나 (코드 최하단의)console.log가 종료한 이후에 호출된다.비동기 함수는 비동기 처리 결과를 외부에 반환할 수 없고, 상위 스코프의 변수에 할당할 수 없다.
⇒ 비동기 함수를 범용적으로 사용하기 위해 비동기 함수에 비동기 처리 결과에 대한 후속 처리를 수행하는 콜백 함수를 전달하는 것이 일반적이다.const get = (url, successCallback, failureCallback) => { const xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.send(); xhr.onload = () => { if (xhr.status === 200) { // 서버의 응답을 콜백 함수에 인수로 전달하면서 호출하여 응답에 대한 후속 처리를 함 successCallback(JSON.parse(xhr.response)); } else { // 에러 정보를 콜백 함수에 인수로 전달하면서 호출하여 에러 처리를 한다. failureCallback(xhr.status); } }; } // 서버의 응답에 대한 후속 처리를 위한 콜백 함수를 비동기 함수인 get에 전달 get('https://jsonplaceholder.typicode.com/posts/1', console.log, console.error);
콜백 헬의 대표적인 예시
get('/step1', a => { get(`/step2/${a}`, b => { get(`/step3/${b}`, c => { get(`/step4/${c}`, d => { console.log(d); }); }); }); });
위 두 문제를 극복하기 위해 ES6에서 프로미스(Promise)가 도입되었다.
// 프로미스 생성
const promise = new Promise((resolve, reject) => {
// Promise 함수의 콜백 함수 내부에서 비동기 처리를 수행
if (/* 비동기 처리 성공 */) {
resolve('result');
} else { /* 비동기 처리 실패 */
reject('failure reason');
}
});
| 프로미스의 상태 정보 | 의미 | 상태 변경 조건 |
|---|---|---|
pending | 비동기 처리가 아직 수행되지 않은 상태 | 프로미스가 생성된 직후 기본 상태 |
fulfilled | 비동기 처리가 수행된 상태(성공) | resolve 함수 호출 |
rejected | 비동기 처리가 수행된 상태(실패) | reject 함수 호출 |
settled 상태 = fulfilled 또는 rejected 상태 = pending이 아닌 상태 = 비동기 처리가 수행된 상태settled 상태가 되면 더는 다른 상태로 변화할 수 없다.프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체다.
Promise.prototype.thenthen 메서드fulfilled 상태(resolve 함수가 호출된 상태)가 되면 호출된다.rejected 상태 (reject 함수가 호출된 상태)가 되면 호출된다.// fulfilled
new Promise(resolve => resolve('fulfilled'))
.then(v => console.log(v), e => console.log(e)); // fulfilled
// rejected
new Promise((_, reject) => reject(new Error('rejected')))
.then(v => console.log(v), e => console.log(e)); // Error: rejected
then 메서드의 콜백 함수가 프로미스를 반환하면 ⇒ 그 프로미스를 그대로 반환하고Promise.prototype.catchcatch 메서드rejected 상태인 경우만 호출된다.// rejected
new Promise((_, reject) => reject(new Error('rejected')))
.catch(e => console.log(e)); // Error: rejected
then(undefined, onRejected)과 동일하게 동작한다.// rejected new Promise((_, reject) => reject(new Error('rejected'))) .then(undefined, e => console.log(e)); // Error: rejected
Promise.prototype.finallyfinally 메서드fulfilled) 또는 실패(rejected)와 상관없이 무조건 한번 호출된다.new Promise(() => {})
.finally(() => console.log('finally')); // finally
const promiseGet = url => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
// 성공적으로 응답을 전달받으면 resolve 함수를 호출
resolve(JSON.parse(xhr.reponse));
} else {
// 에러 처리를 위해 reject 함수를 호출
reject(new Error(xhr.status));
}
};
});
};
// promiseGet 함수는 프로미스를 반환
promiseGet('https://jsonplaceholder.typicode.com/posts/1')
.then(res => console.log(res))
.catch(err => cnsole.error(err))
.finally(() => console.log('Bye!'));
then 메서드의 두번째 콜백 함수로 처리const wrongUrl = 'https://jsonplaceholder.typicode.com/XXX/1';
// 부적절한 URL이 지정되었기 때문에 에러가 발생
promiseGet(wrongUrl.then(
res => console.log(res),
err => console.error(err)
); // Error: 404
catch 메서드를 사용해 처리const wrongUrl = 'https://jsonplaceholder.typicode.com/XXX/1';
// 부적절한 URL이 지정되었기 때문에 에러가 발생
promiseGet(wrongUrl
.then(res => console.log(res))
.catch(err => console.error(err)); // Error: 404
const wrongUrl = 'https://jsonplaceholder.typicode.com/XXX/1';
// 부적절한 URL이 지정되었기 때문에 에러가 발생
promiseGet(wrongUrl
.then(res => console.log(res))
.then(undefined, err => console.error(err)); // Error: 404
then 메서드의 두번째 콜백 함수는 첫번째 콜백 함수에서 발생한 에러를 캐치하지 못한다.catch 메서드를 모든 then 메서드를 호출한 이후에 호출하면 비동기 처리에서 발생한 에러 뿐만 아니라 then 메서드 내부에서 발생한 에러까지 모두 캐치할 수 있다.
then보다는catch메서드를 사용하는 것이 가독성이 좋고 명확하다.
then, catch, finally등의 후속 처리 메서드를 통해 콜백 헬을 해결한다.프로미스 체이닝 예시
const url = 'https://jsonplaceholder.typicode.com' promiseGet(`${url}/posts/1`) .then(({ userId }) => promiseGet(`${url}/users/${userId}`)) .then(userInfo => console.log(userInfo)) .catch(err => console.error(err));
후속 처리 메서드 콜백 함수의 인수 후속 처리 메서드의 반환값 thenpromiseGet함수가 반환한 프로미스가 resolve한 값(id가 1인post)콜백 함수가 반환한 프로미스 then첫 번째 then메서드가 반환한 프로미스가 resolve한 값(post의userId로 취득한user정보)콜백 함수가 반환한 값(undefined)을 resolve한 프로미스 catch(에러가 발생하지 않으면 호출되지 않음)promiseGet함수 또는 앞선 후속 처리 메서드가 반환한 프로미스가 reject한 값콜백 함수가 반환한 값(undefined)을 resolve한 프로미스
async/await을 통해 해결할 수 있다.async/await도 프로미스를 기반으로 동작한다.Promise.resolve/Promise.rejectPromise.resolveconst resolvedPromise = Promise.resolve([1, 2, 3]);
resolvedPromise.then(console.log); // [1, 2, 3]
위 예제와 아래 예제는 같은 동작을 한다.
const resolvedPromise = new Promise(resolve => resolve([1, 2, 3]));
resolvedPromise.then(console.log); // [1, 2, 3]
Promise.reject// 에러 객체를 reject하는 프로미스를 생성
const rejectedPromise = Promise.reject(new Error('Error!'));
rejectedPromise.catch(console.log); // Error: Error!
위 예제와 아래 예제는 같은 동작을 한다.
const rejectedPromise = new Promise((_, reject) => reject(new Error('Error!')));
rejectedPromise.catch(console.log); // Error: Error!
Promise.all순차 처리
const requestData1 = () => new Promise(resolve => setTimeout(() => reolve(1), 3000)); const requestData2 = () => new Promise(resolve => setTimeout(() => reolve(2), 2000)); const requestData3 = () => new Promise(resolve => setTimeout(() => reolve(3), 1000)); // 세 개의 비동기 처리를 순차적으로 처리 const res = []; requestData1() .then(data => { res.push(data); return requestData2(); }) .then(data => { res.push(data); return requestData3(); }) .then(data => { res.push(data); console.log(res); // [1, 2, 3] => 약 6초 소요 }) .catch(console.error);
병렬 처리 (
Promise.all사용)const requestData1 = () => new Promise(resolve => setTimeout(() => reolve(1), 3000)); const requestData2 = () => new Promise(resolve => setTimeout(() => reolve(2), 2000)); const requestData3 = () => new Promise(resolve => setTimeout(() => reolve(3), 1000)); // 세 개의 비동기 처리를 병렬로 처리 Promise.all([requestData1(), requestData2(), requestData3()]) .then(console.log) // [1, 2, 3] => 약 3초 소요 .catch(console.error);
fulfilled 상태가 되면 모든 처리 결과를 배열에 저장해 새로운 프로미스를 반환한다.fulfilled 상태가 되면 resolve된 처리 결과를 첫번째 프로미스가 resolve한 처리 결과부터 차례대로 배열에 저장해 그 배열을 resolve하는 새로운 프로미스를 반환한다// GET 요청을 위한 비동기 함수
const promiseGet = url => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.response));
} else {
reject(new Error(xhr.status));
}
};
});
};
const githubIds = ['jeresig', 'ahejlsberg', 'ungmo2'];
Promise.all(githubIds.map(id => promiseGet(`https://api.github.com/users/${id}`)))
// ['jeresig', 'ahejlsberg', 'ungmo2'] => Promise [userInfo, userInfo, userInfo]
.then(users => users.map(user => user.name))
// [userInfo, userInfo, userInfo]
// -> Promise ['John Resig', 'Anders Hejlsberg', 'Ungmo Lee']
.then(console.log)
.catch(console.error);
Promise.race프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 전달 받음
가장 먼저 fulfilled 상태가 된 프로미스의 처리 결과를 resolve하는 새로운 프로미스를 반환한다.
Promise.race([
new Promise(resolve => setTimeout(() => reolve(1), 3000));
new Promise(resolve => setTimeout(() => reolve(2), 2000));
new Promise(resolve => setTimeout(() => reolve(3), 1000));
])
.then(console.log) // 3
.catch(console.log);
전달된 프로미스가 하나라도 rejected 상태가 되면 에러를 reject하는 새로운 프로미스를 즉시 반환한다.
(Promise.all 메서드와 동일)
Promise.race([
new Promise((_, reject) => setTimeout(() => reject(newError('Error 1')), 3000));
new Promise((_, reject) => setTimeout(() => reject(newError('Error 2')), 3000));
new Promise((_, reject) => setTimeout(() => reject(newError('Error 3')), 3000));
])
.then(console.log)
.catch(console.log); // Error: Error 3
Promise.allSettledsettled 상태(비동기 처리가 수행된 상태, fulfilled 또는 rejected 상태)가 되면 처리 결과를 배열로 반환한다.fulfilled 또는 rejected 상태와 상관없이 Promise.allSettled 메서드가 인수로 전달받은 모든 프로미스들의 처리 결과가 모두 담겨 있다.Promise.allSettled([
new Promise(resolve => setTimeout(() => reolve(1), 2000));
new Promise((_, reject) => setTimeout(() => reject(newError('Error!')), 1000));
]).then(console.log);
/* 출력
[
{status: "fulfilled", value: 1},
{status: "rejected", reason: Error: Error! at <anonymous>:3:54}
]
*/
setTimeout(() => console.log(1), 0);
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
1 → 2 → 3 일것 같지만 사실 2 → 3 → 1로 출력된다.마이크로태스크 큐가 태스크 큐보다 우선순위가 높다.
pending, fulfilled, rejected의 상태 정보를 갖고, 이 상태가 바뀔 때 후속 처리 메서드 then, catch, finally를 활용하여 프로미스 체이닝을 구현할 수 있다.then의 두번째 인자보다 catch로 마지막에 해주는 것이 더 좋다.async/await을 통해 해결할 수 있다.resolve, reject, all, race, allSettled)프론트엔드 공부를 하면서, 그리고 프로젝트를 경험하면서 항상 머리속에서 구름처럼 두둥실 떠다니던 프로미스라는 것에 대해 확실하게 짚고 넘어갈 수 있었던 시간이었다. 대충 이런 객체라는 것은 알겠는데 프로미스가 왜 나오게 되었고, 프로미스 체이닝이 무엇이며 왜 좋은 것인지, 프로미스의 상태 정보나 동작 원리 등에 대해서 더 확실하게 알 수 있었다.